├── .coveragerc ├── .github └── workflows │ └── app-turbo.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── README.rst ├── demos ├── app-server │ ├── apps │ │ ├── __init__.py │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── app.py │ │ │ ├── base.py │ │ │ └── setting.py │ │ ├── base.py │ │ └── settings.py │ ├── main.py │ ├── setting.py │ └── templates │ │ └── app │ │ ├── index.html │ │ └── tag.html ├── chat-server │ ├── apps │ │ ├── __init__.py │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── app.py │ │ │ ├── base.py │ │ │ └── setting.py │ │ ├── base.py │ │ └── settings.py │ ├── main.py │ ├── setting.py │ ├── static │ │ ├── css │ │ │ └── pure-min.css │ │ └── js │ │ │ ├── chat.js │ │ │ └── jquery-3.0.0.min.js │ └── templates │ │ └── app │ │ ├── index.html │ │ └── message.html ├── conf │ ├── __init__.py │ ├── global_setting.py │ └── locale │ │ ├── __init__.py │ │ ├── en │ │ └── __init__.py │ │ └── zh_CN │ │ └── __init__.py ├── db-server │ ├── apps │ │ ├── __init__.py │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── app.py │ │ │ ├── base.py │ │ │ └── setting.py │ │ ├── base.py │ │ └── settings.py │ ├── main.py │ ├── setting.py │ └── templates │ │ └── app │ │ └── index.html ├── db │ ├── __init__.py │ ├── mongo_conn.py │ ├── mysql_conn.py │ └── setting.py ├── helpers │ ├── __init__.py │ ├── settings.py │ └── user │ │ ├── __init__.py │ │ └── user.py ├── jinja2-support │ ├── apps │ │ ├── __init__.py │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── app.py │ │ │ ├── base.py │ │ │ └── setting.py │ │ ├── base.py │ │ └── settings.py │ ├── main.py │ ├── setting.py │ └── templates │ │ └── app │ │ └── index.html ├── models │ ├── __init__.py │ ├── base.py │ ├── blog │ │ ├── __init__.py │ │ ├── base.py │ │ ├── model.py │ │ └── setting.py │ ├── settings.py │ └── user │ │ ├── __init__.py │ │ ├── base.py │ │ ├── model.py │ │ └── setting.py ├── script │ └── init.py ├── sql │ └── data.sql ├── store │ ├── __init__.py │ ├── actions.py │ ├── modules │ │ ├── __init__.py │ │ ├── chat.py │ │ ├── metric.py │ │ └── user.py │ └── mutation_types.py └── test │ ├── __init__.py │ ├── helper_test.py │ ├── realpath.py │ └── user_model_test.py ├── docs ├── about.md ├── api │ └── index.md ├── imgs │ └── turbo.png ├── index.md ├── intro.md ├── tutorial │ ├── app.md │ ├── db.md │ ├── helper.md │ ├── index.md │ ├── model.md │ ├── session.md │ └── turbo-admin.md ├── useage.md └── zh-CN │ ├── about.md │ ├── imgs │ └── turbo.png │ ├── index.md │ ├── intro.md │ ├── quickstart.md │ └── tutorial │ ├── app.md │ ├── db.md │ ├── helper.md │ ├── index.md │ └── model.md ├── mkdocs.yml ├── requirements.txt ├── requirements_dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── app_test.py ├── escape_test.py ├── flux_test.py ├── httputil_test.py ├── jinja2_test.py ├── log_test.py ├── model_test.py ├── runtests.py ├── session_test.py ├── util.py └── util_test.py ├── tox.ini ├── turbo ├── __init__.py ├── app.py ├── bin │ └── turbo-admin ├── conf.py ├── core │ ├── __init__.py │ └── exceptions.py ├── fake │ ├── __init__.py │ └── project_template │ │ ├── __init__.py │ │ ├── app-server │ │ ├── __init__.py │ │ ├── apps │ │ │ ├── __init__.py │ │ │ ├── app │ │ │ │ ├── __init__.py │ │ │ │ ├── api.py │ │ │ │ ├── app.py │ │ │ │ ├── base.py │ │ │ │ └── setting.py │ │ │ ├── base.py │ │ │ └── settings.py │ │ ├── main.py │ │ └── setting.py │ │ ├── config │ │ └── __init__.py │ │ ├── db │ │ ├── __init__.py │ │ ├── mongo_conn.py │ │ ├── mysql_conn.py │ │ └── setting.py │ │ ├── helpers │ │ ├── __init__.py │ │ ├── settings.py │ │ └── user │ │ │ ├── __init__.py │ │ │ └── user.py │ │ ├── models │ │ ├── __init__.py │ │ ├── base.py │ │ ├── settings.py │ │ └── user │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── model.py │ │ │ └── setting.py │ │ ├── service │ │ └── __init__.py │ │ ├── store │ │ ├── __init__.py │ │ ├── actions.py │ │ ├── modules │ │ │ ├── __init__.py │ │ │ └── user.py │ │ └── mutation_types.py │ │ └── utils │ │ └── __init__.py ├── flux.py ├── helper.py ├── httputil.py ├── log.py ├── model.py ├── mongo_model.py ├── register.py ├── session.py ├── template.py └── util.py └── zh-CN_README.md /.coveragerc: -------------------------------------------------------------------------------- 1 | # Test coverage configuration. 2 | # Usage: 3 | # pip install coverage 4 | # coverage erase # clears previous data if any 5 | # coverage run -m tests.runtests 6 | # coverage report # prints to stdout 7 | # coverage html # creates ./htmlcov/*.html including annotated source 8 | [run] 9 | branch = true 10 | source = turbo 11 | omit = 12 | turbo/fake/* 13 | turbo/bin/* 14 | 15 | [report] 16 | # Ignore missing source files, i.e. fake template-generated "files" 17 | ignore_errors = true -------------------------------------------------------------------------------- /.github/workflows/app-turbo.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: app-turbo 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["2.7", "3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 31 | if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | - python setup.py install 41 | - coverage run -m tests.runtests 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | node_modules 56 | *.~ 57 | .ssh 58 | .idea 59 | __test__ 60 | site 61 | *~ 62 | .DS_Store 63 | .python-version 64 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git@github.com:pre-commit/pre-commit-hooks 2 | sha: 97b88d9610bcc03982ddac33caba98bb2b751f5f 3 | hooks: 4 | - id: trailing-whitespace 5 | - id: autopep8-wrapper 6 | - id: flake8 7 | args: [--config=setup.cfg,] 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | services: 9 | - mongodb 10 | - redis-server 11 | 12 | # command to install dependencies 13 | install: 14 | - pip install pymongo 15 | - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then travis_retry pip install 'tornado<5.0.0'; fi 16 | - if [[ $TRAVIS_PYTHON_VERSION == '3.3' ]]; then travis_retry pip install 'tornado<5.0.0'; fi 17 | - if [[ $TRAVIS_PYTHON_VERSION == '3.4' ]]; then travis_retry pip install 'tornado<5.0.0'; fi 18 | - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then travis_retry pip install 'tornado<6.0.0'; fi 19 | - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then travis_retry pip install 'tornado>=6.0.0'; fi 20 | - pip install redis 21 | - pip install docopt 22 | - pip install codecov 23 | - pip install coverage 24 | # command to run tests 25 | script: 26 | - python setup.py install 27 | - coverage run -m tests.runtests 28 | after_success: 29 | - coverage xml 30 | - codecov -t dd6dbd11-3b2b-4930-9b81-34c313c74c8f 31 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | app-turbo changes log 2 | ===================== 3 | 4 | ## 0.5.0 5 | 6 | - fix file upload bug in Python3 7 | 8 | ## 0.4.8 9 | 10 | - Support Python3.4, Python3.5, Python3.6 11 | 12 | ## 0.4.7 13 | 14 | - Support turbo in Python 2.7 install correctly 15 | 16 | ## 0.4.5 17 | 18 | - Methods `get_as_column`,`create`, attribute `column` are removed from `BaseModel` class 19 | - Move tests outside turbo package 20 | - Rewrite insert,update,remove with the latest pymongo collection methods like `insert_one`,`insert_many` and so on. 21 | - `BaseModel` method `inc` add multi docs support 22 | - add `mongo_model` module for support [motor](http://motor.readthedocs.io/en/stable/) 23 | 24 | **warning** 25 | 26 | 4.5 从 `BaseModel` 中弃用了 `create` 方法,请使用 `insert` 代替,默认所有插入操`insert`,`insert_one`,`insert_many`,`save` 都会进行 `field` 属性中键的校验,可以使用关键字参数 `check=False` 跳过校验。 27 | 28 | 4.5 为了支持异步 mongo 驱动 `motor` pymongo 必须 >=3.2,请谨慎升级安装。 29 | 30 | ## 0.4.4 31 | 32 | - add jinja2 template support 33 | 34 | ## 0.4.3 35 | 36 | - add flux prgraming model inspired by reactjs 37 | - add mongodb build index command line tool 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include README.rst 3 | include requirements.txt 4 | include LICENSE -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test clean-log ## remove all build, test, coverage and Python artifacts 30 | 31 | 32 | clean-build: ## remove build artifacts 33 | rm -fr build/ 34 | rm -fr dist/ 35 | rm -fr .eggs/ 36 | find . -name '*.egg-info' -exec rm -fr {} + 37 | find . -name '*.egg' -exec rm -f {} + 38 | 39 | clean-pyc: ## remove Python file artifacts 40 | find . -name '*.pyc' -exec rm -f {} + 41 | find . -name '*.pyo' -exec rm -f {} + 42 | find . -name '*~' -exec rm -f {} + 43 | find . -name '__pycache__' -exec rm -fr {} + 44 | 45 | clean-test: ## remove test and coverage artifacts 46 | rm -fr .tox/ 47 | rm -f .coverage 48 | rm -fr htmlcov/ 49 | 50 | clean-log: 51 | rm -f tests/*.log 52 | rm -f *.log 53 | 54 | lint: ## check style with flake8 55 | flake8 turbo tests --config=setup.cfg 56 | 57 | test: ## run tests quickly with the default Python 58 | 59 | python -m tests.runtests 60 | make clean-log 61 | 62 | test-all: ## run tests on every Python version with tox 63 | tox 64 | 65 | coverage: ## check code coverage quickly with the default Python 66 | coverage erase 67 | coverage run -m tests.runtests 68 | 69 | coverage report -m 70 | coverage html 71 | $(BROWSER) htmlcov/index.html 72 | 73 | release: clean ## package and upload a release 74 | python setup.py sdist bdist_wheel 75 | # python setup.py bdist_wheel 76 | twine upload dist/* 77 | 78 | dist: clean ## builds source and wheel package 79 | python setup.py sdist 80 | python setup.py bdist_wheel 81 | ls -l dist 82 | 83 | install: clean ## install the package to the active Python's site-packages 84 | python setup.py install 85 | 86 | hooks-install: 87 | pre-commit install 88 | 89 | hooks-run: 90 | pre-commit run --all-files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | turbo 2 | ========= 3 | 4 | [中文文档](https://github.com/wecatch/app-turbo/blob/master/zh-CN_README.md) 5 | 6 | [![pypi](https://img.shields.io/pypi/v/turbo.svg)](https://pypi.python.org/pypi/turbo) 7 | [![app-turbo](https://github.com/wecatch/app-turbo/actions/workflows/app-turbo.yml/badge.svg?branch=master)](https://github.com/wecatch/app-turbo/actions/workflows/app-turbo.yml) 8 | [![codecov](https://codecov.io/github/wecatch/app-turbo/coverage.svg?branch=master)](https://codecov.io/github/wecatch/app-turbo?branch=master) 9 | [![readthedocs](https://readthedocs.org/projects/app-turbo/badge/?version=latest)](https://app-turbo.readthedocs.io/en/latest/) 10 | 11 | 12 | Turbo is a framework for fast building web site and RESTFul api, based on tornado. 13 | 14 | 15 | - Easily scale up and maintain 16 | - Rapid development for RESTFul api and web site 17 | - Django or flask application structure 18 | - Easily customizable 19 | - Simple ORM for MongoDB 20 | - Logger 21 | - Session(storage support for redis, disk and so on) 22 | - Support MongoDB, MySQL, PostgreSQL and so on 23 | - Support MongoDB asynchronous driver [Motor](http://motor.readthedocs.io/en/stable/) base on [turbo-motor](https://github.com/wecatch/turbo-motor) 24 | - Support Python3 25 | 26 | ## Getting started 27 | 28 | ``` 29 | pip install turbo 30 | turbo-admin startproject 31 | cd /app-server 32 | touch __test__ 33 | python main.py 34 | ``` 35 | 36 | ## Documentation 37 | 38 | Documentation and links to additional resources are available at [http://app-turbo.readthedocs.org/](http://app-turbo.readthedocs.org/) 39 | 40 | ## Tutorial 41 | 42 | - [让 turbo 支持异步调用 MongoDB](http://sanyuesha.com/2018/04/11/turbo-motor/) 43 | - [turbo 的诞生记](http://sanyuesha.com/2016/07/23/why-did-i-make-turbo/) 44 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | app-turbo 2 | ========= 3 | 4 | .. image:: https://img.shields.io/pypi/v/turbo.svg 5 | :alt: pip 6 | :target: https://pypi.python.org/pypi/turbo 7 | 8 | .. image:: https://travis-ci.org/wecatch/app-turbo.svg?branch=master 9 | :alt: Build Status 10 | :target: https://travis-ci.org/wecatch/app-turbo 11 | 12 | .. image:: https://codecov.io/github/wecatch/app-turbo/coverage.svg?branch=master 13 | :alt: codecov 14 | :target: https://codecov.io/github/wecatch/app-turbo?branch=master 15 | 16 | .. image:: https://readthedocs.org/projects/app-turbo/badge/?version=latest 17 | :alt: readthedocs 18 | :target: https://app-turbo.readthedocs.io/en/latest/ 19 | 20 | 21 | `Turbo `_ is a web framework for fast building web site and RESTFul api, based on tornado, mongodb, redis. 22 | 23 | 24 | - Easily scale up and maintain 25 | - Rapid development for RESTFul api and web site 26 | - Django or flask application structure 27 | - Easily customizable 28 | - Simple ORM for mongodb 29 | - Logger 30 | - Session(storage support for redis, disk and so on) 31 | - support MongoDB, MySQL, PostgreSQL and so on 32 | - support MongoDB asynchronous driver `Motor `_ 33 | 34 | 35 | Getting started 36 | ---------------- 37 | 38 | .. code-block:: bash 39 | 40 | pip install turbo 41 | turbo-admin startproject 42 | cd /app-server 43 | touch __test__ 44 | python main.py 45 | 46 | 47 | Documentation 48 | -------------- 49 | 50 | Documentation and links to additional resources are available at http://app-turbo.readthedocs.org -------------------------------------------------------------------------------- /demos/app-server/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/app-server/apps/__init__.py -------------------------------------------------------------------------------- /demos/app-server/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from turbo import register 3 | 4 | from . import app 5 | 6 | 7 | register.register_group_urls('', [ 8 | ('/', app.HomeHandler), 9 | ('/index', app.HomeHandler, 'index'), 10 | ('/hello', app.HomeHandler, 'home'), 11 | ('/motor', app.AsynHandler), 12 | ]) 13 | 14 | register.register_url('/v1/hello', app.ApiHandler) 15 | -------------------------------------------------------------------------------- /demos/app-server/apps/app/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | 4 | import turbo.log 5 | from turbo.flux import state as turbo_state 6 | import tornado.gen 7 | 8 | from . import base 9 | from helpers import user as user_helper 10 | from models.user import model as user_model 11 | 12 | BaseHandler = base.BaseHandler 13 | logger = turbo.log.getLogger(__file__) 14 | 15 | 16 | class HomeHandler(BaseHandler): 17 | 18 | def get(self, *args, **kwargs): 19 | self.render('index.html', metric=turbo_state.metric) 20 | 21 | 22 | class AsynHandler(BaseHandler): 23 | 24 | @tornado.gen.coroutine 25 | def get(self): 26 | result = yield user_model.Tag().find_many(limit=10) 27 | self.render('tag.html', result=result) 28 | 29 | 30 | class ApiHandler(BaseHandler): 31 | 32 | def GET(self, *args, **kwargs): 33 | self._data = { 34 | 'msg': 'hello turbo world' 35 | } 36 | -------------------------------------------------------------------------------- /demos/app-server/apps/app/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from turbo.util import basestring_type as basestring 4 | 5 | from apps import base 6 | 7 | from . import setting 8 | 9 | 10 | 11 | 12 | class BaseHandler(base.BaseHandler): 13 | 14 | _get_required_params = [('who', str, None)] 15 | 16 | def initialize(self): 17 | super(BaseHandler, self).initialize() 18 | self.template_path = setting.TEMPLATE_PATH 19 | -------------------------------------------------------------------------------- /demos/app-server/apps/app/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # sub app setting 4 | # try not to include function or class 5 | 6 | TEMPLATE_PATH = 'app/' 7 | -------------------------------------------------------------------------------- /demos/app-server/apps/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from .settings import ( 3 | LANG as _LANG, 4 | ) 5 | 6 | import turbo.app 7 | 8 | from store import actions 9 | 10 | 11 | class MixinHandler(turbo.app.BaseHandler): 12 | pass 13 | 14 | 15 | class BaseHandler(MixinHandler): 16 | 17 | _session = None 18 | 19 | def initialize(self): 20 | super(BaseHandler, self).initialize() 21 | self._params = self.parameter 22 | self._skip = 0 23 | self._limit = 0 24 | 25 | def prepare(self): 26 | super(BaseHandler, self).prepare() 27 | self._skip = abs(self._params['skip']) if self._params.get( 28 | 'skip', None) else 0 29 | self._limit = abs(self._params['limit']) if self._params.get( 30 | 'limit', None) else 20 31 | actions.inc_qps() 32 | -------------------------------------------------------------------------------- /demos/app-server/apps/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # installed app list 4 | INSTALLED_APPS = ( 5 | 'app', 6 | ) 7 | 8 | # language 9 | LANG = 'zh_CN' 10 | -------------------------------------------------------------------------------- /demos/app-server/main.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from tornado.options import define, options 4 | import tornado.options 5 | 6 | import setting 7 | import turbo.register 8 | import turbo.app 9 | import store 10 | 11 | turbo.register.register_app(setting.SERVER_NAME, setting.TURBO_APP_SETTING, 12 | setting.WEB_APPLICATION_SETTING, __file__, globals()) 13 | 14 | define("port", default=8888, type=int) 15 | 16 | if __name__ == '__main__': 17 | tornado.options.parse_command_line() 18 | turbo.app.start(options.port) 19 | -------------------------------------------------------------------------------- /demos/app-server/setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from tornado.util import ObjectDict 4 | 5 | # server name 6 | SERVER_NAME = 'helloworld' 7 | 8 | # server dir 9 | SERVER_DIR = os.path.dirname(os.path.abspath(__file__)) 10 | # project dir 11 | PROJECT_DIR = os.path.dirname(SERVER_DIR) 12 | sys.path.append(PROJECT_DIR) 13 | 14 | # tornado web application settings 15 | # details in 16 | # http://www.tornadoweb.org/en/stable/web.html#tornado.web.Application.settings 17 | WEB_APPLICATION_SETTING = ObjectDict( 18 | static_path=os.path.join(SERVER_DIR, "static"), 19 | template_path=os.path.join(SERVER_DIR, "templates"), 20 | xsrf_cookies=True, 21 | cookie_secret="3%$334ma?asdf2987^%23&^%$2", 22 | debug=False 23 | ) 24 | 25 | # turbo app setting 26 | TURBO_APP_SETTING = ObjectDict( 27 | log=ObjectDict( 28 | log_path=os.path.join("", SERVER_NAME + '.log'), 29 | log_size=500 * 1024 * 1024, 30 | log_count=3, 31 | ), 32 | session_config=ObjectDict({ 33 | 'name': 'session-id', 34 | 'secret_key': 'o387xn4ma?adfasdfa83284&^%$2' 35 | }), 36 | ) 37 | 38 | 39 | if os.path.exists(os.path.join(SERVER_DIR, '__test__')): 40 | # check if app start in debug 41 | WEB_APPLICATION_SETTING['debug'] = True 42 | TURBO_APP_SETTING.log.log_path = os.path.join("", SERVER_NAME + '.log') 43 | -------------------------------------------------------------------------------- /demos/app-server/templates/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello turbo world 6 | 7 | 8 |

hello turbo world

9 |

{{metric.qps}}

10 |

11 | hello motor asynchronous 12 |

13 |

14 | hello RESTFul api 15 |

16 | 17 | -------------------------------------------------------------------------------- /demos/app-server/templates/app/tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello turbo world 6 | 7 | 8 |

hello motor world

9 |
    10 | {% for i in result %} 11 |
  • {{i['name']}}
  • 12 | {% end %} 13 |
14 | 15 | -------------------------------------------------------------------------------- /demos/chat-server/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/chat-server/apps/__init__.py -------------------------------------------------------------------------------- /demos/chat-server/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from turbo import register 3 | 4 | from . import app 5 | 6 | 7 | register.register_group_urls('', [ 8 | (r"/", app.MainHandler), 9 | (r"/a/message/new", app.MessageNewHandler), 10 | (r"/a/message/updates", app.MessageUpdatesHandler), 11 | ]) 12 | -------------------------------------------------------------------------------- /demos/chat-server/apps/app/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os.path 4 | import uuid 5 | 6 | import tornado.escape 7 | import tornado.web 8 | from tornado import gen 9 | 10 | import turbo.log 11 | from turbo.flux import state as turbo_state 12 | 13 | from . import base 14 | 15 | BaseHandler = base.BaseHandler 16 | logger = turbo.log.getLogger(__file__) 17 | 18 | 19 | global_message_buffer = turbo_state.chat.message_buffer 20 | 21 | 22 | class MainHandler(BaseHandler): 23 | 24 | def get(self): 25 | self.render("index.html", messages=global_message_buffer.cache) 26 | 27 | 28 | class MessageNewHandler(BaseHandler): 29 | 30 | def post(self): 31 | message = { 32 | "id": str(uuid.uuid4()), 33 | "body": self.get_argument("body"), 34 | } 35 | # to_basestring is necessary for Python 3's json encoder, 36 | # which doesn't accept byte strings. 37 | message["html"] = tornado.escape.to_basestring( 38 | self.render_string("message.html", message=message)) 39 | if self.get_argument("next", None): 40 | self.redirect(self.get_argument("next")) 41 | else: 42 | self.write(message) 43 | global_message_buffer.new_messages([message]) 44 | 45 | 46 | class MessageUpdatesHandler(BaseHandler): 47 | 48 | @gen.coroutine 49 | def post(self): 50 | cursor = self.get_argument("cursor", None) 51 | # Save the future returned by wait_for_messages so we can cancel 52 | # it in wait_for_messages 53 | self.future = global_message_buffer.wait_for_messages(cursor=cursor) 54 | messages = yield self.future 55 | if self.request.connection.stream.closed(): 56 | return 57 | self.write(dict(messages=messages)) 58 | 59 | def on_connection_close(self): 60 | global_message_buffer.cancel_wait(self.future) 61 | -------------------------------------------------------------------------------- /demos/chat-server/apps/app/base.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from apps import base 4 | 5 | from . import setting 6 | 7 | 8 | class BaseHandler(base.BaseHandler): 9 | 10 | def initialize(self): 11 | super(BaseHandler, self).initialize() 12 | self.template_path = setting.TEMPLATE_PATH 13 | -------------------------------------------------------------------------------- /demos/chat-server/apps/app/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # sub app setting 4 | # try not to include function or class 5 | 6 | TEMPLATE_PATH = 'app/' 7 | -------------------------------------------------------------------------------- /demos/chat-server/apps/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import tornado.web 4 | import turbo.app 5 | from turbo import app_config 6 | from turbo.core.exceptions import ResponseError, ResponseMsg 7 | # start use session from here 8 | # from lib.session import SessionStore, SessionObject 9 | 10 | 11 | class MixinHandler(turbo.app.BaseHandler): 12 | pass 13 | 14 | 15 | class BaseHandler(MixinHandler): 16 | 17 | # session_initializer = { 18 | # 'uid': None, 19 | # 'avatar': None, 20 | # 'nickname': None, 21 | # } 22 | # session_object = SessionObject 23 | # session_store = SessionStore() 24 | 25 | def initialize(self): 26 | super(BaseHandler, self).initialize() 27 | self._params = self.parameter 28 | 29 | def prepare(self): 30 | super(BaseHandler, self).prepare() 31 | 32 | def response_msg(self, msg='', code=1): 33 | raise ResponseMsg(code, msg) 34 | 35 | def response_error(self, msg='', code=1): 36 | raise ResponseError(code, msg) 37 | 38 | def http_error(self, status_code=404): 39 | raise tornado.web.HTTPError(status_code) 40 | 41 | def write_error(self, status_code, **kwargs): 42 | """Override to implement custom error pages. 43 | http://tornado.readthedocs.org/en/stable/_modules/tornado/web.html#RequestHandler.write_error 44 | """ 45 | super(BaseHandler, self).write_error(status_code, **kwargs) 46 | 47 | 48 | class ErrorHandler(BaseHandler): 49 | 50 | def initialize(self, status_code): 51 | super(ErrorHandler, self).initialize() 52 | self.set_status(status_code) 53 | 54 | def prepare(self): 55 | if not self.is_ajax(): 56 | if self.get_status() == 404: 57 | raise self.http_error(404) 58 | else: 59 | self.wo_resp({'code': 1, 'msg': 'Api Not found'}) 60 | self.finish() 61 | return 62 | 63 | def check_xsrf_cookie(self): 64 | # POSTs to an ErrorHandler don't actually have side effects, 65 | # so we don't need to check the xsrf token. This allows POSTs 66 | # to the wrong url to return a 404 instead of 403. 67 | pass 68 | 69 | 70 | from turbo.conf import app_config 71 | app_config.error_handler = ErrorHandler 72 | -------------------------------------------------------------------------------- /demos/chat-server/apps/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # installed app list 4 | INSTALLED_APPS = ( 5 | 'app', 6 | ) 7 | 8 | # language 9 | LANG = 'en' 10 | -------------------------------------------------------------------------------- /demos/chat-server/main.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from tornado.options import define, options 4 | import tornado.options 5 | 6 | import setting 7 | import turbo.register 8 | import turbo.app 9 | import store 10 | 11 | turbo.register.register_app(setting.SERVER_NAME, setting.TURBO_APP_SETTING, 12 | setting.WEB_APPLICATION_SETTING, __file__, globals()) 13 | 14 | define("port", default=8888, type=int) 15 | 16 | if __name__ == '__main__': 17 | tornado.options.parse_command_line() 18 | turbo.app.start(options.port) 19 | -------------------------------------------------------------------------------- /demos/chat-server/setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from tornado.util import ObjectDict 4 | 5 | # server name 6 | SERVER_NAME = 'chat-server' 7 | 8 | # server dir 9 | SERVER_DIR = os.path.dirname(os.path.abspath(__file__)) 10 | # project dir 11 | PROJECT_DIR = os.path.dirname(SERVER_DIR) 12 | sys.path.append(PROJECT_DIR) 13 | 14 | # tornado web application settings 15 | # details in 16 | # http://www.tornadoweb.org/en/stable/web.html#tornado.web.Application.settings 17 | WEB_APPLICATION_SETTING = ObjectDict( 18 | static_path=os.path.join(SERVER_DIR, "static"), 19 | template_path=os.path.join(SERVER_DIR, "templates"), 20 | xsrf_cookies=True, 21 | cookie_secret="3%$334ma?asdf2987^%23&^%$2", 22 | ) 23 | 24 | # turbo app setting 25 | TURBO_APP_SETTING = ObjectDict( 26 | log=ObjectDict( 27 | log_path=os.path.join("", SERVER_NAME + '.log'), 28 | log_size=500 * 1024 * 1024, 29 | log_count=3, 30 | ), 31 | session_config=ObjectDict({ 32 | 'name': 'session-id', 33 | 'secret_key': 'o387xn4ma?adfasdfa83284&^%$2' 34 | }), 35 | ) 36 | 37 | # check if app start in debug 38 | if os.path.exists(os.path.join(SERVER_DIR, '__test__')): 39 | WEB_APPLICATION_SETTING['debug'] = True 40 | TURBO_APP_SETTING.log.log_path = os.path.join("", SERVER_NAME + '.log') 41 | -------------------------------------------------------------------------------- /demos/chat-server/static/js/chat.js: -------------------------------------------------------------------------------- 1 | // Copyright 2009 FriendFeed 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | $(document).ready(function() { 16 | if (!window.console) window.console = {}; 17 | if (!window.console.log) window.console.log = function() {}; 18 | 19 | $("#messageform").on("submit", function() { 20 | newMessage($(this)); 21 | return false; 22 | }); 23 | $("#messageform").on("keypress", function(e) { 24 | if (e.keyCode == 13) { 25 | newMessage($(this)); 26 | return false; 27 | } 28 | }); 29 | $("#message").select(); 30 | updater.poll(); 31 | }); 32 | 33 | function newMessage(form) { 34 | var message = form.formToDict(); 35 | var submit = form.find("input[type=submit]"); 36 | submit.disable(); 37 | $.postJSON("/a/message/new", message, function(response) { 38 | updater.showMessage(response); 39 | if (message.id) { 40 | form.parent().remove(); 41 | } else { 42 | form.find("input[type=text]").val("").select(); 43 | submit.enable(); 44 | } 45 | }); 46 | } 47 | 48 | function getCookie(name) { 49 | var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); 50 | return r ? r[1] : undefined; 51 | } 52 | 53 | jQuery.postJSON = function(url, args, callback) { 54 | args._xsrf = getCookie("_xsrf"); 55 | $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST", 56 | success: function(response) { 57 | if (callback) callback(eval("(" + response + ")")); 58 | }, error: function(response) { 59 | console.log("ERROR:", response) 60 | }}); 61 | }; 62 | 63 | jQuery.fn.formToDict = function() { 64 | var fields = this.serializeArray(); 65 | var json = {} 66 | for (var i = 0; i < fields.length; i++) { 67 | json[fields[i].name] = fields[i].value; 68 | } 69 | if (json.next) delete json.next; 70 | return json; 71 | }; 72 | 73 | jQuery.fn.disable = function() { 74 | this.enable(false); 75 | return this; 76 | }; 77 | 78 | jQuery.fn.enable = function(opt_enable) { 79 | if (arguments.length && !opt_enable) { 80 | this.attr("disabled", "disabled"); 81 | } else { 82 | this.removeAttr("disabled"); 83 | } 84 | return this; 85 | }; 86 | 87 | var updater = { 88 | errorSleepTime: 500, 89 | cursor: null, 90 | 91 | poll: function() { 92 | var args = {"_xsrf": getCookie("_xsrf")}; 93 | if (updater.cursor) args.cursor = updater.cursor; 94 | $.ajax({url: "/a/message/updates", type: "POST", dataType: "text", 95 | data: $.param(args), success: updater.onSuccess, 96 | error: updater.onError}); 97 | }, 98 | 99 | onSuccess: function(response) { 100 | try { 101 | updater.newMessages(eval("(" + response + ")")); 102 | } catch (e) { 103 | updater.onError(); 104 | return; 105 | } 106 | updater.errorSleepTime = 500; 107 | window.setTimeout(updater.poll, 0); 108 | }, 109 | 110 | onError: function(response) { 111 | updater.errorSleepTime *= 2; 112 | console.log("Poll error; sleeping for", updater.errorSleepTime, "ms"); 113 | window.setTimeout(updater.poll, updater.errorSleepTime); 114 | }, 115 | 116 | newMessages: function(response) { 117 | if (!response.messages) return; 118 | updater.cursor = response.cursor; 119 | var messages = response.messages; 120 | updater.cursor = messages[messages.length - 1].id; 121 | console.log(messages.length, "new messages, cursor:", updater.cursor); 122 | for (var i = 0; i < messages.length; i++) { 123 | updater.showMessage(messages[i]); 124 | } 125 | }, 126 | 127 | showMessage: function(message) { 128 | var existing = $("#m" + message.id); 129 | if (existing.length > 0) return; 130 | var node = $(message.html); 131 | node.hide(); 132 | $("#inbox").append(node); 133 | node.slideDown(); 134 | } 135 | }; 136 | -------------------------------------------------------------------------------- /demos/chat-server/templates/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello turbo world 6 | 7 | 8 | 9 |
10 |
11 | {% for message in messages %} 12 | {% module Template("message.html", message=message) %} 13 | {% end %} 14 |
15 |
16 |
17 |
18 | 19 | 21 | {% module xsrf_form_html() %} 22 |
23 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /demos/chat-server/templates/app/message.html: -------------------------------------------------------------------------------- 1 |
{% module linkify(message["body"]) %}
2 | -------------------------------------------------------------------------------- /demos/conf/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/conf/__init__.py -------------------------------------------------------------------------------- /demos/conf/global_setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'zhyq' 3 | 4 | import os 5 | 6 | # app path 7 | CUR_PATH = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | # app start status 10 | DEBUG = False 11 | 12 | if os.path.exists(os.path.join(CUR_PATH, '__test__')): 13 | DEBUG = True 14 | -------------------------------------------------------------------------------- /demos/conf/locale/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import en 4 | import zh_CN 5 | 6 | """ 7 | code range describe the response message from the server 8 | 9 | 0~999: define error code 定义错误码 10 | 11 | 0: success 12 | 13 | 1~99: base response error 基础错误 用于调试,正式上线不对外开放 14 | 1: '未知错误', 15 | 2: 'url 找不到', 16 | 3: '缺少必要的参数', 17 | 4: '参数类型错误', 18 | 5: '无数据' 19 | 20 | """ 21 | 22 | 23 | LANG_MESSAGE = { 24 | 'zh_CN': zh_CN.MESSAGE, 25 | 'en': en.MESSAGE, 26 | } 27 | -------------------------------------------------------------------------------- /demos/conf/locale/en/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | __author__ = 'zhyq' 3 | 4 | MESSAGE = { 5 | 0: 'success', 6 | 1: 'An error has occurred.', 7 | } 8 | -------------------------------------------------------------------------------- /demos/conf/locale/zh_CN/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | __author__ = 'zhyq' 3 | 4 | MESSAGE = { 5 | 0: 'success', 6 | 1: u'未知错误', 7 | } 8 | -------------------------------------------------------------------------------- /demos/db-server/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/db-server/apps/__init__.py -------------------------------------------------------------------------------- /demos/db-server/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from turbo import register 3 | 4 | from . import api 5 | from . import app 6 | 7 | register.register_group_urls('', [ 8 | ('/', app.HomeHandler), 9 | ('/plus', app.IncHandler), 10 | ('/minus', app.MinusHandler), 11 | ]) 12 | 13 | register.register_group_urls('/v1', [ 14 | ('', api.HomeHandler), 15 | ]) 16 | -------------------------------------------------------------------------------- /demos/db-server/apps/app/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import turbo.log 4 | 5 | from . import base 6 | from models.blog.model import Blog 7 | 8 | BaseHandler = base.BaseHandler 9 | logger = turbo.log.getLogger(__file__) 10 | 11 | 12 | class HomeHandler(BaseHandler): 13 | 14 | def GET(self): 15 | self._data = [{'id': i.id, 'title': i.title} for i in self.db.query(Blog).add_columns( 16 | Blog.id, Blog.title).all()] 17 | -------------------------------------------------------------------------------- /demos/db-server/apps/app/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import turbo.log 4 | 5 | from models.blog.model import Blog 6 | 7 | from . import base 8 | from store import actions 9 | 10 | BaseHandler = base.BaseHandler 11 | logger = turbo.log.getLogger(__file__) 12 | 13 | 14 | class HomeHandler(BaseHandler): 15 | 16 | def get(self): 17 | result = self.db.query(Blog).add_columns( 18 | Blog.id, Blog.text).all() 19 | self.render('index.html', result=result) 20 | 21 | 22 | class IncHandler(BaseHandler): 23 | 24 | _get_params = { 25 | 'option': [ 26 | ('value', int, 0) 27 | ] 28 | } 29 | 30 | def get(self): 31 | self._data = actions.increase(self._params['value']) 32 | self.write(str(self._data)) 33 | 34 | 35 | class MinusHandler(BaseHandler): 36 | 37 | _get_params = { 38 | 'option': [ 39 | ('value', int, 0) 40 | ] 41 | } 42 | 43 | def get(self): 44 | self._data = actions.decrease(self._params['value']) 45 | self.write(str(self._data)) 46 | -------------------------------------------------------------------------------- /demos/db-server/apps/app/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from . import setting 4 | from apps import base 5 | 6 | 7 | class BaseHandler(base.BaseHandler): 8 | 9 | def initialize(self): 10 | super(BaseHandler, self).initialize() 11 | self.template_path = setting.TEMPLATE_PATH 12 | -------------------------------------------------------------------------------- /demos/db-server/apps/app/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # sub app setting 4 | # try not to include function or class 5 | 6 | TEMPLATE_PATH = 'app/' 7 | -------------------------------------------------------------------------------- /demos/db-server/apps/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import tornado.web 4 | import turbo.app 5 | from turbo import app_config 6 | from turbo.core.exceptions import ResponseError, ResponseMsg 7 | 8 | from db.mysql_conn import DBSession 9 | 10 | # start use session from here 11 | # from lib.session import SessionStore, SessionObject 12 | # from turbo.template import turbo_jinja2 13 | 14 | 15 | class MixinHandler(turbo.app.BaseHandler): 16 | pass 17 | 18 | 19 | class BaseHandler(MixinHandler): 20 | 21 | # session_initializer = { 22 | # 'uid': None, 23 | # 'avatar': None, 24 | # 'nickname': None, 25 | # } 26 | # session_object = SessionObject 27 | # session_store = SessionStore() 28 | 29 | def initialize(self): 30 | super(BaseHandler, self).initialize() 31 | self._params = self.parameter 32 | self.db = DBSession() 33 | 34 | # @turbo_jinja2 35 | # def render_string(self, template_name, **kwargs): 36 | # pass 37 | 38 | def on_finish(self): 39 | super(BaseHandler, self).on_finish() 40 | self.db.commit() 41 | self.db.close() 42 | 43 | def prepare(self): 44 | super(BaseHandler, self).prepare() 45 | 46 | def response_msg(self, msg='', code=1): 47 | raise ResponseMsg(code, msg) 48 | 49 | def response_error(self, msg='', code=1): 50 | raise ResponseError(code, msg) 51 | 52 | def http_error(self, status_code=404): 53 | raise tornado.web.HTTPError(status_code) 54 | 55 | def write_error(self, status_code, **kwargs): 56 | """Override to implement custom error pages. 57 | http://tornado.readthedocs.org/en/stable/_modules/tornado/web.html#RequestHandler.write_error 58 | """ 59 | super(BaseHandler, self).write_error(status_code, **kwargs) 60 | 61 | 62 | class ErrorHandler(BaseHandler): 63 | 64 | def initialize(self, status_code): 65 | super(ErrorHandler, self).initialize() 66 | self.set_status(status_code) 67 | 68 | def prepare(self): 69 | if not self.is_ajax(): 70 | if self.get_status() == 404: 71 | raise self.http_error(404) 72 | else: 73 | self.wo_resp({'code': 1, 'msg': 'Api Not found'}) 74 | self.finish() 75 | return 76 | 77 | def check_xsrf_cookie(self): 78 | # POSTs to an ErrorHandler don't actually have side effects, 79 | # so we don't need to check the xsrf token. This allows POSTs 80 | # to the wrong url to return a 404 instead of 403. 81 | pass 82 | 83 | 84 | app_config.error_handler = ErrorHandler 85 | -------------------------------------------------------------------------------- /demos/db-server/apps/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # installed app list 4 | INSTALLED_APPS = ( 5 | 'app', 6 | ) 7 | 8 | # language 9 | LANG = 'en' 10 | -------------------------------------------------------------------------------- /demos/db-server/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import tornado.options 4 | import turbo.app 5 | import turbo.register 6 | from tornado.options import define, options 7 | 8 | import setting 9 | 10 | # uncomment this to init state manager: store 11 | # import store 12 | 13 | turbo.register.register_app( 14 | setting.SERVER_NAME, 15 | setting.TURBO_APP_SETTING, 16 | setting.WEB_APPLICATION_SETTING, 17 | __file__, 18 | globals() 19 | ) 20 | 21 | define("port", default=8888, type=int) 22 | 23 | if __name__ == '__main__': 24 | tornado.options.parse_command_line() 25 | turbo.app.start(options.port) 26 | -------------------------------------------------------------------------------- /demos/db-server/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | from tornado.util import ObjectDict 7 | 8 | # server name 9 | SERVER_NAME = 'db-server' 10 | 11 | # server dir 12 | SERVER_DIR = os.path.dirname(os.path.abspath(__file__)) 13 | # project dir 14 | PROJECT_DIR = os.path.dirname(SERVER_DIR) 15 | sys.path.append(PROJECT_DIR) 16 | 17 | # tornado web application settings 18 | # details in 19 | # http://www.tornadoweb.org/en/stable/web.html#tornado.web.Application.settings 20 | WEB_APPLICATION_SETTING = ObjectDict( 21 | static_path=os.path.join(SERVER_DIR, "static"), 22 | template_path=os.path.join(SERVER_DIR, "templates"), 23 | xsrf_cookies=True, 24 | cookie_secret="3%$334ma?asdf2987^%23&^%$2", 25 | ) 26 | 27 | # turbo app setting 28 | TURBO_APP_SETTING = ObjectDict( 29 | log=ObjectDict( 30 | log_path=os.path.join("/var/log/", SERVER_NAME + '.log'), 31 | log_size=500 * 1024 * 1024, 32 | log_count=3, 33 | ), 34 | session_config=ObjectDict({ 35 | 'name': 'session-id', 36 | 'secret_key': 'o387xn4ma?adfasdfa83284&^%$2' 37 | }), 38 | template='', 39 | ) 40 | 41 | # check if app start in debug 42 | if os.path.exists(os.path.join(SERVER_DIR, '__test__')): 43 | WEB_APPLICATION_SETTING['debug'] = True 44 | TURBO_APP_SETTING.log.log_path = os.path.join("", SERVER_NAME + '.log') 45 | -------------------------------------------------------------------------------- /demos/db-server/templates/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello turbo world 6 | 7 | 8 |

hello turbo world

9 |
    10 | {% for d in result %} 11 |
  • 12 |

    {{d.id}}

    13 |

    14 | {{d.text}} 15 |

    16 |
  • 17 | {% end %} 18 |
19 | 20 | -------------------------------------------------------------------------------- /demos/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/db/__init__.py -------------------------------------------------------------------------------- /demos/db/mongo_conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | 5 | 6 | from pymongo import ( 7 | MongoReplicaSetClient, 8 | MongoClient, 9 | read_preferences 10 | ) 11 | from motor import MotorClient 12 | 13 | import gridfs 14 | 15 | 16 | mc = MongoClient(host='localhost') 17 | motor_client = MotorClient(host='localhost') 18 | 19 | # test 20 | test = mc['test'] 21 | test_files = gridfs.GridFS(mc['test_files']) 22 | 23 | # user 24 | user = mc['turbo_app'] 25 | user_files = gridfs.GridFS(mc['turbo_app_files']) 26 | 27 | 28 | tag = motor_client['turbo_app'] 29 | -------------------------------------------------------------------------------- /demos/db/mysql_conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | from sqlalchemy.engine.url import URL 6 | 7 | from .setting import DB_SETTING, DRIVERNAME 8 | 9 | 10 | # mysql blog 11 | blog_engine = create_engine( 12 | URL(DRIVERNAME, **DB_SETTING), encoding='utf8', echo=True) 13 | DBSession = sessionmaker(bind=blog_engine) 14 | -------------------------------------------------------------------------------- /demos/db/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | 4 | REPL_SET_NAME = 'rs0' 5 | 6 | HOSTS = ['server1:27017', 'server2:27017', 'server3:27017'] 7 | 8 | DB_SETTING = { 9 | 'username': 'root', 10 | 'password': '', 11 | 'host': 'localhost', 12 | 'port': 3306, 13 | 'database': 'blog' 14 | } 15 | 16 | DRIVERNAME = 'mysql+pymysql' 17 | -------------------------------------------------------------------------------- /demos/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .settings import INSTALLED_HELPERS 4 | 5 | import turbo.helper 6 | 7 | turbo.helper.install_helper(INSTALLED_HELPERS, globals()) 8 | -------------------------------------------------------------------------------- /demos/helpers/settings.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | # installed helper list 4 | INSTALLED_HELPERS = ( 5 | 'user', 6 | ) 7 | -------------------------------------------------------------------------------- /demos/helpers/user/__init__.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | # used by helpers install helper 4 | __all__ = ['user', ] 5 | -------------------------------------------------------------------------------- /demos/helpers/user/user.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from datetime import datetime, timedelta 4 | 5 | from pymongo import DESCENDING, ASCENDING 6 | 7 | from models.user import model as user_model 8 | 9 | from helpers import settings 10 | 11 | 12 | MODEL_SLOTS = ['User'] 13 | 14 | 15 | class User(user_model.User): 16 | 17 | def hello_user(self): 18 | self.instance('user.User').find_one() 19 | -------------------------------------------------------------------------------- /demos/jinja2-support/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/jinja2-support/apps/__init__.py -------------------------------------------------------------------------------- /demos/jinja2-support/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from turbo import register 3 | 4 | import app 5 | import api 6 | 7 | 8 | register.register_group_urls('', [ 9 | ('/', app.HomeHandler), 10 | ('/plus', app.IncHandler), 11 | ('/minus', app.MinusHandler), 12 | ]) 13 | 14 | register.register_group_urls('/v1', [ 15 | ('', api.HomeHandler), 16 | ]) 17 | -------------------------------------------------------------------------------- /demos/jinja2-support/apps/app/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import turbo.log 4 | 5 | from . import base 6 | 7 | BaseHandler = base.BaseHandler 8 | logger = turbo.log.getLogger(__file__) 9 | 10 | 11 | class HomeHandler(BaseHandler): 12 | 13 | def GET(self): 14 | pass 15 | -------------------------------------------------------------------------------- /demos/jinja2-support/apps/app/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import turbo.log 4 | 5 | from store import actions 6 | 7 | from . import base 8 | 9 | BaseHandler = base.BaseHandler 10 | logger = turbo.log.getLogger(__file__) 11 | 12 | 13 | class HomeHandler(BaseHandler): 14 | 15 | def get(self): 16 | self.render('index.html') 17 | 18 | 19 | class IncHandler(BaseHandler): 20 | 21 | _get_params = { 22 | 'option': [ 23 | ('value', int, 0) 24 | ] 25 | } 26 | 27 | def get(self): 28 | self._data = actions.increase(self._params['value']) 29 | self.write(str(self._data)) 30 | 31 | 32 | class MinusHandler(BaseHandler): 33 | 34 | _get_params = { 35 | 'option': [ 36 | ('value', int, 0) 37 | ] 38 | } 39 | 40 | def get(self): 41 | self._data = actions.decrease(self._params['value']) 42 | self.write(str(self._data)) 43 | -------------------------------------------------------------------------------- /demos/jinja2-support/apps/app/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from apps import base 4 | 5 | from . import setting 6 | 7 | 8 | class BaseHandler(base.BaseHandler): 9 | 10 | def initialize(self): 11 | super(BaseHandler, self).initialize() 12 | self.template_path = setting.TEMPLATE_PATH 13 | -------------------------------------------------------------------------------- /demos/jinja2-support/apps/app/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # sub app setting 4 | # try not to include function or class 5 | 6 | TEMPLATE_PATH = 'app/' 7 | -------------------------------------------------------------------------------- /demos/jinja2-support/apps/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import tornado.web 4 | import turbo.app 5 | from turbo import app_config 6 | from turbo.core.exceptions import ResponseError, ResponseMsg 7 | # start use session from here 8 | # from lib.session import SessionStore, SessionObject 9 | from turbo.template import turbo_jinja2 10 | 11 | 12 | class MixinHandler(turbo.app.BaseHandler): 13 | pass 14 | 15 | 16 | class BaseHandler(MixinHandler): 17 | 18 | # session_initializer = { 19 | # 'uid': None, 20 | # 'avatar': None, 21 | # 'nickname': None, 22 | # } 23 | # session_object = SessionObject 24 | # session_store = SessionStore() 25 | 26 | def initialize(self): 27 | super(BaseHandler, self).initialize() 28 | self._params = self.parameter 29 | 30 | @turbo_jinja2 31 | def render_string(self, template_name, **kwargs): 32 | pass 33 | 34 | def prepare(self): 35 | super(BaseHandler, self).prepare() 36 | 37 | def response_msg(self, msg='', code=1): 38 | raise ResponseMsg(code, msg) 39 | 40 | def response_error(self, msg='', code=1): 41 | raise ResponseError(code, msg) 42 | 43 | def http_error(self, status_code=404): 44 | raise tornado.web.HTTPError(status_code) 45 | 46 | def write_error(self, status_code, **kwargs): 47 | """Override to implement custom error pages. 48 | http://tornado.readthedocs.org/en/stable/_modules/tornado/web.html#RequestHandler.write_error 49 | """ 50 | super(BaseHandler, self).write_error(status_code, **kwargs) 51 | 52 | 53 | class ErrorHandler(BaseHandler): 54 | 55 | def initialize(self, status_code): 56 | super(ErrorHandler, self).initialize() 57 | self.set_status(status_code) 58 | 59 | def prepare(self): 60 | if not self.is_ajax(): 61 | if self.get_status() == 404: 62 | raise self.http_error(404) 63 | else: 64 | self.wo_resp({'code': 1, 'msg': 'Api Not found'}) 65 | self.finish() 66 | return 67 | 68 | def check_xsrf_cookie(self): 69 | # POSTs to an ErrorHandler don't actually have side effects, 70 | # so we don't need to check the xsrf token. This allows POSTs 71 | # to the wrong url to return a 404 instead of 403. 72 | pass 73 | 74 | 75 | from turbo.conf import app_config 76 | app_config.error_handler = ErrorHandler 77 | -------------------------------------------------------------------------------- /demos/jinja2-support/apps/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # installed app list 4 | INSTALLED_APPS = ( 5 | 'app', 6 | ) 7 | 8 | # language 9 | LANG = 'en' 10 | -------------------------------------------------------------------------------- /demos/jinja2-support/main.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | from tornado.options import define, options 4 | import tornado.options 5 | 6 | import setting 7 | import turbo.register 8 | import turbo.app 9 | # uncomment this to init state manager: store 10 | #import store 11 | 12 | turbo.register.register_app(setting.SERVER_NAME, setting.TURBO_APP_SETTING, 13 | setting.WEB_APPLICATION_SETTING, __file__, globals()) 14 | 15 | define("port", default=8888, type=int) 16 | 17 | if __name__ == '__main__': 18 | tornado.options.parse_command_line() 19 | turbo.app.start(options.port) 20 | -------------------------------------------------------------------------------- /demos/jinja2-support/setting.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from tornado.util import ObjectDict 4 | 5 | # server name 6 | SERVER_NAME = 'jinja2-support' 7 | 8 | # server dir 9 | SERVER_DIR = os.path.dirname(os.path.abspath(__file__)) 10 | # project dir 11 | PROJECT_DIR = os.path.dirname(SERVER_DIR) 12 | sys.path.append(PROJECT_DIR) 13 | 14 | # tornado web application settings 15 | # details in 16 | # http://www.tornadoweb.org/en/stable/web.html#tornado.web.Application.settings 17 | WEB_APPLICATION_SETTING = ObjectDict( 18 | static_path=os.path.join(SERVER_DIR, "static"), 19 | template_path=os.path.join(SERVER_DIR, "templates"), 20 | xsrf_cookies=True, 21 | cookie_secret="3%$334ma?asdf2987^%23&^%$2", 22 | ) 23 | 24 | # turbo app setting 25 | TURBO_APP_SETTING = ObjectDict( 26 | log=ObjectDict( 27 | log_path=os.path.join("", SERVER_NAME + '.log'), 28 | log_size=500 * 1024 * 1024, 29 | log_count=3, 30 | ), 31 | session_config=ObjectDict({ 32 | 'name': 'session-id', 33 | 'secret_key': 'o387xn4ma?adfasdfa83284&^%$2' 34 | }), 35 | template='jinja2', 36 | ) 37 | 38 | # check if app start in debug 39 | if os.path.exists(os.path.join(SERVER_DIR, '__test__')): 40 | WEB_APPLICATION_SETTING['debug'] = True 41 | TURBO_APP_SETTING.log.log_path = os.path.join("", SERVER_NAME + '.log') 42 | -------------------------------------------------------------------------------- /demos/jinja2-support/templates/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hello turbo world 6 | 7 | 8 |

{{ 'hello turbo world' | upper }}

9 | 10 | -------------------------------------------------------------------------------- /demos/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/models/__init__.py -------------------------------------------------------------------------------- /demos/models/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from datetime import datetime 4 | import time 5 | 6 | from bson.objectid import ObjectId 7 | from pymongo import ASCENDING, DESCENDING 8 | from sqlalchemy.ext.declarative import declarative_base 9 | from sqlalchemy.orm import sessionmaker 10 | 11 | # todo change name 12 | import turbo.model 13 | import turbo.util 14 | import turbo_motor.model 15 | 16 | from .settings import ( 17 | MONGO_DB_MAPPING as _MONGO_DB_MAPPING, 18 | DB_ENGINE_MAPPING as _DB_ENGINE_MAPPING, 19 | ) 20 | 21 | 22 | class BaseModel(turbo.model.BaseModel): 23 | 24 | package_space = globals() 25 | 26 | def __init__(self, db_name='test'): 27 | super(BaseModel, self).__init__(db_name, _MONGO_DB_MAPPING) 28 | 29 | def get_count(self, spec=None): 30 | return self.find(spec=spec).count() 31 | 32 | 33 | class SqlBaseModel(object): 34 | 35 | def __init__(self, db_name='test'): 36 | engine = _DB_ENGINE_MAPPING[db_name] 37 | self.Base = declarative_base(bind=engine) 38 | self.Session = self.create_session(engine) 39 | 40 | def create_session(self, engine): 41 | return sessionmaker(bind=engine) 42 | 43 | 44 | class MotorBaseModel(turbo_motor.model.BaseModel): 45 | 46 | package_space = globals() 47 | 48 | def __init__(self, db_name='test'): 49 | super(MotorBaseModel, self).__init__(db_name, _MONGO_DB_MAPPING) 50 | -------------------------------------------------------------------------------- /demos/models/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/models/blog/__init__.py -------------------------------------------------------------------------------- /demos/models/blog/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from models.base import SqlBaseModel 3 | 4 | 5 | class Model(SqlBaseModel): 6 | 7 | def __init__(self): 8 | super(Model, self).__init__(db_name='blog') 9 | 10 | 11 | Base = Model().Base 12 | -------------------------------------------------------------------------------- /demos/models/blog/model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from .base import Base 4 | from sqlalchemy import Column, Integer, Text, VARCHAR, BLOB, DateTime 5 | 6 | 7 | class Blog(Base): 8 | 9 | __tablename__ = 'entries' 10 | id = Column(Integer, primary_key=True) 11 | title = Column(BLOB, nullable=False) 12 | text = Column(BLOB, nullable=False) 13 | dig_count = Column(Integer) 14 | uid = Column(Integer) 15 | comment_count = Column(Integer) 16 | atime = Column(DateTime, default=datetime.now, nullable=False) 17 | -------------------------------------------------------------------------------- /demos/models/blog/setting.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/models/blog/setting.py -------------------------------------------------------------------------------- /demos/models/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from db.mongo_conn import ( 4 | test as _test, 5 | user as _user, 6 | test_files as _test_files, 7 | user_files as _user_files, 8 | tag as _tag 9 | ) 10 | 11 | from db.mysql_conn import blog_engine 12 | 13 | MONGO_DB_MAPPING = { 14 | 'db': { 15 | 'test': _test, 16 | 'user': _user, 17 | 'tag': _tag, 18 | }, 19 | 'db_file': { 20 | 'test': _test_files, 21 | 'user': _user_files, 22 | } 23 | } 24 | 25 | 26 | DB_ENGINE_MAPPING = { 27 | 'blog': blog_engine, 28 | } 29 | -------------------------------------------------------------------------------- /demos/models/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/models/user/__init__.py -------------------------------------------------------------------------------- /demos/models/user/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from models.base import * 4 | 5 | 6 | class Model(BaseModel): 7 | 8 | def __init__(self): 9 | super(Model, self).__init__(db_name='user') 10 | 11 | 12 | class MotorModel(MotorBaseModel): 13 | def __init__(self): 14 | super(MotorModel, self).__init__(db_name='tag') 15 | -------------------------------------------------------------------------------- /demos/models/user/model.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .base import * 4 | 5 | 6 | class User(Model): 7 | 8 | """ 9 | email: user account 10 | passwd: user account passwd 11 | atime: added time 12 | """ 13 | name = 'user' 14 | 15 | field = { 16 | 'email': (str, ''), 17 | 'passwd': (str, ''), 18 | 'atime': (datetime, None), 19 | } 20 | 21 | 22 | class Tag(MotorModel): 23 | 24 | """ 25 | name: tag name 26 | atime: added time 27 | """ 28 | name = 'tag' 29 | 30 | field = { 31 | 'name': (str, ''), 32 | 'atime': (datetime, None), 33 | } 34 | -------------------------------------------------------------------------------- /demos/models/user/setting.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/models/user/setting.py -------------------------------------------------------------------------------- /demos/script/init.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import random 4 | 5 | import tornado.gen 6 | 7 | sys.path.insert(0, 8 | os.path.dirname( 9 | os.path.dirname(os.path.abspath(__file__)) 10 | ) 11 | ) 12 | 13 | from models.user.model import Tag 14 | 15 | 16 | @tornado.gen.coroutine 17 | def init_tag(): 18 | tb_tag = Tag() 19 | for i in range(10): 20 | result = yield tb_tag.insert({'name': random.choice('abcdefghjklpoiuytrewq')}) 21 | print(result) 22 | 23 | 24 | if __name__ == '__main__': 25 | tornado.ioloop.IOLoop.current().run_sync(lambda: init_tag()) 26 | 27 | -------------------------------------------------------------------------------- /demos/store/__init__.py: -------------------------------------------------------------------------------- 1 | from .modules import user, metric, chat 2 | -------------------------------------------------------------------------------- /demos/store/actions.py: -------------------------------------------------------------------------------- 1 | from turbo.flux import Mutation, register, dispatch, register_dispatch 2 | 3 | from . import mutation_types 4 | 5 | 6 | @register_dispatch('user', mutation_types.INCREASE) 7 | def increase(rank): 8 | pass 9 | 10 | 11 | def decrease(rank): 12 | return dispatch('user', mutation_types.DECREASE, rank) 13 | 14 | 15 | @register_dispatch('metric', 'inc_qps') 16 | def inc_qps(): 17 | pass 18 | -------------------------------------------------------------------------------- /demos/store/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/demos/store/modules/__init__.py -------------------------------------------------------------------------------- /demos/store/modules/chat.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import logging 4 | 5 | from tornado.concurrent import Future 6 | from turbo.flux import Mutation, register, State 7 | 8 | mutation = Mutation(__file__) 9 | state = State(__file__) 10 | 11 | 12 | class MessageBuffer(object): 13 | 14 | def __init__(self): 15 | self.waiters = set() 16 | self.cache = [] 17 | self.cache_size = 200 18 | 19 | def wait_for_messages(self, cursor=None): 20 | # Construct a Future to return to our caller. This allows 21 | # wait_for_messages to be yielded from a coroutine even though 22 | # it is not a coroutine itself. We will set the result of the 23 | # Future when results are available. 24 | result_future = Future() 25 | if cursor: 26 | new_count = 0 27 | for msg in reversed(self.cache): 28 | if msg["id"] == cursor: 29 | break 30 | new_count += 1 31 | if new_count: 32 | result_future.set_result(self.cache[-new_count:]) 33 | return result_future 34 | self.waiters.add(result_future) 35 | return result_future 36 | 37 | def cancel_wait(self, future): 38 | self.waiters.remove(future) 39 | # Set an empty result to unblock any coroutines waiting. 40 | future.set_result([]) 41 | 42 | def new_messages(self, messages): 43 | logging.info("Sending new message to %r listeners", len(self.waiters)) 44 | for future in self.waiters: 45 | future.set_result(messages) 46 | self.waiters = set() 47 | self.cache.extend(messages) 48 | if len(self.cache) > self.cache_size: 49 | self.cache = self.cache[-self.cache_size:] 50 | 51 | 52 | state.message_buffer = MessageBuffer() 53 | -------------------------------------------------------------------------------- /demos/store/modules/metric.py: -------------------------------------------------------------------------------- 1 | from turbo.flux import Mutation, register, State 2 | 3 | mutation = Mutation(__file__) 4 | state = State(__file__) 5 | 6 | state.qps = 0 7 | 8 | 9 | @register(mutation) 10 | def inc_qps(): 11 | state.qps += 1 12 | -------------------------------------------------------------------------------- /demos/store/modules/user.py: -------------------------------------------------------------------------------- 1 | from turbo.flux import Mutation, register, State 2 | 3 | mutation = Mutation(__file__) 4 | state = State(__file__) 5 | 6 | 7 | @register(mutation) 8 | def increase_rank(rank): 9 | return rank + 1 10 | 11 | 12 | @register(mutation) 13 | def dec_rank(rank): 14 | return rank - 1 15 | -------------------------------------------------------------------------------- /demos/store/mutation_types.py: -------------------------------------------------------------------------------- 1 | INCREASE = 'increase_rank' 2 | DECREASE = 'decrease_rank' 3 | -------------------------------------------------------------------------------- /demos/test/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | root_path = [os.path.dirname( 5 | os.path.dirname( 6 | os.path.abspath(__file__) 7 | ) 8 | )] 9 | 10 | sys.path += root_path 11 | -------------------------------------------------------------------------------- /demos/test/helper_test.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import unittest 4 | import inspect 5 | 6 | import realpath 7 | 8 | from helpers import user as wallpaper 9 | 10 | import helpers 11 | 12 | import helpers.user as db_wallpaper 13 | 14 | 15 | class HelperTest(unittest.TestCase): 16 | 17 | def test_model(self): 18 | self.assertTrue(callable(wallpaper['user'].find_one)) 19 | 20 | def test_import_helper(self): 21 | self.assertTrue(callable(helpers.user['user'].find_one)) 22 | 23 | def test_import_helper_two(self): 24 | self.assertTrue(callable(db_wallpaper['user'].find_one)) 25 | 26 | def test_import_helper_class(self): 27 | self.assertTrue(inspect.isclass(db_wallpaper.User)) 28 | self.assertTrue(not inspect.isclass(db_wallpaper.user)) 29 | 30 | if __name__ == '__main__': 31 | unittest.main() 32 | -------------------------------------------------------------------------------- /demos/test/realpath.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | import os 3 | import sys 4 | 5 | reload(sys) 6 | sys.setdefaultencoding('utf-8') 7 | 8 | # load app path into sys.path 9 | 10 | 11 | def app_path_load(dir_level_num=2): 12 | app_root_path = os.path.abspath(__file__) 13 | for i in xrange(0, dir_level_num): 14 | app_root_path = os.path.dirname(app_root_path) 15 | 16 | sys.path.append(app_root_path) 17 | 18 | 19 | app_path_load() 20 | -------------------------------------------------------------------------------- /demos/test/user_model_test.py: -------------------------------------------------------------------------------- 1 | #-*- coding:utf-8 -*- 2 | 3 | import os 4 | import sys 5 | import unittest 6 | import datetime 7 | import json 8 | import StringIO 9 | 10 | import realpath 11 | 12 | from models.user import model 13 | from bson.objectid import ObjectId 14 | 15 | 16 | class AdeskModelTest(unittest.TestCase): 17 | 18 | def setUp(self): 19 | self.m = model.User() 20 | 21 | def tearDown(self): 22 | del self.m 23 | 24 | def test_create(self): 25 | self.assertTrue(isinstance(self.m.create( 26 | {'email': 'test@test.com'}), ObjectId)) 27 | 28 | def test_insert(self): 29 | pass 30 | 31 | def test_save(self): 32 | pass 33 | 34 | def test_find_one(self): 35 | pass 36 | 37 | def test_find(self): 38 | pass 39 | 40 | def test_update(self): 41 | pass 42 | 43 | def test_remove(self): 44 | pass 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/docs/about.md -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | #### TODO -------------------------------------------------------------------------------- /docs/imgs/turbo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/docs/imgs/turbo.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | [![turbo](imgs/turbo.png)](http://wecatch.me/turbo.html) 3 | 4 | Turbo is a web framwork, based on tornado, developed for rapidly building web site and RESTFul api. 5 | 6 | **Prerequisites**: Turbo now only runs on Python 2.x, Python 3 support will be added in future. 7 | 8 | # Quickly start 9 | 10 | 11 | ```bash 12 | pip install turbo 13 | ``` 14 | 15 | # License 16 | 17 | 18 | Licensed under the Apache License, Version 2.0 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Turbo is developed for web site that based on [tornado](http://tornado.readthedocs.org/en/stable/) and [mongodb](https://www.mongodb.org/) to build rapidly and easily to scale up and maintain. 4 | 5 | 6 | Turbo has support for: 7 | 8 | - Easily scale up and maintain 9 | - Rapid development for RESTFul api and web site 10 | - Django or flask style application structure 11 | - Easily customizing 12 | - Simple ORM for mongodb 13 | - Logger 14 | - Session 15 | 16 | In addtion to the above, turbo has a command line utility `turbo-admin` for fast build application structure. 17 | 18 | 19 | ## Demo 20 | 21 | 22 | ```sh 23 | git clone https://github.com/wecatch/app-turbo.git 24 | cd app-turbo/demos/helloword/app-server 25 | python main.py 26 | ``` 27 | 28 | Open your brower and visit [http://localhost:8888](http://localhost:8888) 29 | 30 | 31 | ## Install 32 | 33 | First be sure you have `MongoDB` and `redis` installed. 34 | 35 | 36 | ```sh 37 | pip install turbo 38 | ``` 39 | 40 | Install the latest 41 | 42 | ```sh 43 | git clone https://github.com/wecatch/app-turbo.git 44 | cd app-turbo 45 | python setup.py install 46 | ``` 47 | 48 | ## Hello, world 49 | 50 | 51 | ```bash 52 | turbo-admin startproject my_turbo_app 53 | cd my_turbo_app 54 | cd app-server 55 | python main.py 56 | ``` 57 | 58 | Open your broswer and visite [http://localhost:8888](http://localhost:8888) 59 | 60 | Server start on port 8888 default, you can change this `python main.py --port=8890` 61 | -------------------------------------------------------------------------------- /docs/tutorial/app.md: -------------------------------------------------------------------------------- 1 | #### What is app-server 2 | 3 | App-server is a web application, be made of one or more sub app, each sub app easily migrited and resue 4 | 5 | 6 | `app-server` directory skeleton 7 | 8 | 9 | ``` sh 10 | 11 | app-server/ 12 | ├── apps 13 | │   ├── base.py 14 | │   ├── __init__.py 15 | │   ├── settings.py 16 | │   └── user # sub app user 17 | │   ├── app.py 18 | │   ├── base.py 19 | │   ├── __init__.py 20 | │   ├── setting.py 21 | ├── main.py # entry 22 | ├── setting.py 23 | ├── templates 24 | │   └── user 25 | │   └── index.html 26 | ├── static 27 | │   └── js 28 | │   └── jquery.js 29 | 30 | 31 | ``` 32 | 33 | 34 | #### Create app-server 35 | 36 | 37 | ```sh 38 | turbo-admin startserver app-server 39 | cd app-server 40 | python main.py 41 | ``` 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/tutorial/db.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/docs/tutorial/db.md -------------------------------------------------------------------------------- /docs/tutorial/helper.md: -------------------------------------------------------------------------------- 1 | #### What is `helper` 2 | 3 | Helper is a layer of business logic. Each package, in helpers, is a helper. 4 | 5 | 6 | `helpers` directory skeleton 7 | 8 | ``` 9 | 10 | helpers/ 11 | ├── wallpaper # each package in helpers represents one mongodb databse instance 12 | │   ├── album.py 13 | │   ├── category.py 14 | │   ├── image.py 15 | │   ├── img2tag.py 16 | │   ├── __init__.py 17 | │   └── tag.py 18 | ├── __init__.py 19 | ├── settings.py # here is package needed to be installed automatically 20 | 21 | ``` 22 | 23 | #### Create helper 24 | 25 | To create a helper, first create a package in helpers, for example named with `wallpaper` , and define '__all__' list attribute explicitly in `__init__.py` file. The attribute `__all__`, it's job is to include all modules needed to be used. 26 | 27 | ```python 28 | __all__ = ['img2tag'] 29 | 30 | ``` 31 | 32 | 33 | Install helper in `helpers/setting.py` 34 | 35 | ``` 36 | 37 | INSTALLED_HELPERS = ( 38 | 'wallpaper', 39 | ) 40 | 41 | ``` 42 | 43 | 44 | #### How to use helper 45 | 46 | 47 | import helper like this 48 | 49 | ``` 50 | from helpers import wallpaper as wallpaper_helper 51 | 52 | ``` 53 | 54 | #### How turbo instantiate helper 55 | 56 | Each helper is a python package. Put code bellow in package `__init__.py` file. `__all__` list includes module need to be instantiated. 57 | 58 | 59 | ```python 60 | #wallpaper/__init__.py 61 | 62 | __all__ = ['album', 'img2tag'] 63 | 64 | ``` 65 | 66 | In each module included in `__all__`, put `Class` that need to be used into list `MODEL_SLOTS` 67 | 68 | 69 | ```python 70 | 71 | #wallpaper/album.py 72 | 73 | MODEL_SLOTS = ['Album', 'FavorAlbum'] 74 | 75 | ``` 76 | 77 | Each `Class`, for example like `Album`, both it's `Class` type and `instance` exist in helper `wallpaper` namespace. 78 | 79 | 80 | ```python 81 | 82 | wallpaper_helper.favor_album # import instance variable 83 | wallpaper_helper.FavorAlbum # import Class type 84 | 85 | ``` 86 | 87 | 88 | -------------------------------------------------------------------------------- /docs/tutorial/index.md: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ================= 3 | 4 | Turbo application directory tree skeleton is below: 5 | 6 | ``` 7 | ├── app-server 8 | ├── conf 9 | ├── db 10 | ├── helpers 11 | ├── models 12 | └── test 13 | ``` 14 | 15 | [models](model) is made of datebase sechema, each package in `models` represents one mongodb databse instance. 16 | 17 | [app-server](app-server) is a web app, not python package, turbo application can have one or many app-server, each with different name. 18 | 19 | [helpers](helpers) is model instance, responsible for business logic. 20 | 21 | `db` is support for mongodb connections 22 | -------------------------------------------------------------------------------- /docs/tutorial/model.md: -------------------------------------------------------------------------------- 1 | #### What is `model` 2 | 3 | 4 | Model has two meanings, one means `model Class`, the other means package which encapsulates `model.py` 5 | 6 | `model Class` represents mongodb collection and defines the collection schema. 7 | 8 | 9 | `models` directory skeleton 10 | 11 | ``` 12 | models 13 | ├── __init__.py 14 | ├── settings.py # global setting for all models 15 | ├── base.py # mongodb database instance mappings 16 | ├── user 17 | │   ├── __init__.py 18 | │   ├── base.py 19 | │   ├── model.py # all model Class, each represents one mongodb collection 20 | │   └── setting.py # setting for user model 21 | 22 | ``` 23 | 24 | 25 | #### Create model 26 | 27 | 28 | To create a model, first create a package below in models, for example named with `user`. 29 | 30 | 31 | ```bash 32 | user 33 | ├──__init__.py 34 | ├── base.py 35 | ├── model.py 36 | └── setting.py 37 | 38 | ``` 39 | 40 | 41 | Put code in `base.py` 42 | 43 | ```python 44 | 45 | # base.py 46 | 47 | from models.base import * 48 | 49 | class Model(BaseModel): 50 | 51 | def __init__(self): 52 | super(Model, self).__init__(db_name='user') 53 | 54 | ``` 55 | 56 | 57 | Create `Class` inherited from `turbo.model.BaseModel` 58 | 59 | ``` 60 | from base import * # import BaseModel from base 61 | 62 | class User2img(Model): 63 | 64 | name = 'user2img' # collection 的名字 65 | 66 | field = { 67 | 'uid': (ObjectId, None) , 68 | 'imgid': (ObjectId, None) , 69 | 'atime': (datetime, None) , 70 | 'atime': (datetime, None) , 71 | } 72 | 73 | ``` 74 | 75 | #### Use model in helper 76 | 77 | ```python 78 | #helpers/user/user.py 79 | from models.user import model 80 | 81 | class User2img(model.User2img):pass 82 | 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/tutorial/session.md: -------------------------------------------------------------------------------- 1 | Turbo has built-in session support. You can easily to customize session store and control the life cycle of a session. 2 | 3 | 4 | #### The life cycle of a session 5 | 6 | By default, the `BaseBaseHandler` class has a `property` called `session`. When tornado server 7 | prepares to serve a user request, if you call `self.seesion` by yourself in `prepare` hooks or somewhere else before `on_finish` hooks called, a `session_id` will be added to response headers, default in cookie. 8 | -------------------------------------------------------------------------------- /docs/tutorial/turbo-admin.md: -------------------------------------------------------------------------------- 1 | 2 | Turbo has a command line tool `turbo-admin` for developer to build application quickly. 3 | 4 | 5 | ##### Getting help 6 | 7 | ```bash 8 | turbo-admin -h 9 | ``` 10 | 11 | 12 | ##### Init turbo project 13 | 14 | ```bash 15 | turbo-admin startproject 16 | ``` 17 | 18 | ##### Init turbo app-server 19 | 20 | ```bash 21 | cd 22 | turbo-admin startserver 23 | ``` 24 | 25 | 26 | ##### Init sub app 27 | 28 | 29 | ```bash 30 | cd 31 | turbo-admin startapp 32 | ``` -------------------------------------------------------------------------------- /docs/useage.md: -------------------------------------------------------------------------------- 1 | ## who uses turbo 2 | 3 | - [git-star](https://git-star.com) 4 | - [adesk.com](http://adesk.com) 5 | - [我的家乡菜](http://wodejiaxiangcai.com) -------------------------------------------------------------------------------- /docs/zh-CN/about.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/docs/zh-CN/about.md -------------------------------------------------------------------------------- /docs/zh-CN/imgs/turbo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/docs/zh-CN/imgs/turbo.png -------------------------------------------------------------------------------- /docs/zh-CN/index.md: -------------------------------------------------------------------------------- 1 | 2 | [![turbo](imgs/turbo.png)](http://wecatch.me/turbo.html) 3 | 4 | 5 | # [turbo](http://wecatch.me/turbo.html) 6 | 7 | app-turbo 是一个用以加速建立普通 web 站点和 RESTful api 的后端解决方案,基于 tornado 和 mongodb。 8 | 9 | 10 | # 安装 11 | 12 | 13 | 快速安装 14 | 15 | ```sh 16 | 17 | pip install turbo 18 | 19 | ``` 20 | 21 | 安装最新版 22 | 23 | ```sh 24 | 25 | git clone https://github.com/wecatch/app-turbo.git 26 | cd app-turbo 27 | python setup.py install 28 | 29 | ``` 30 | 31 | 32 | # 开源许可 33 | 34 | 35 | Licensed under the Apache License, Version 2.0 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/zh-CN/intro.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | app-turbo 的诞生旨在为基于 [tornado](http://tornado.readthedocs.org/en/stable/) 和 [mongodb](https://www.mongodb.org/) 的应用开发提供**快速构建**,**便于扩展**,**易于维护**的最佳实践方案。app-turbo 包含以下特性 4 | 5 | - 简易的 ORM (基于 mongodb ) 6 | - 快速构建 RESTful api 7 | - 易于扩展和维护的 app 结构 8 | - 灵活的 log 9 | - 简单可增强的 session 10 | 11 | # demo 12 | 13 | 14 | ```sh 15 | 16 | git clone https://github.com/wecatch/app-turbo.git 17 | 18 | cd app-turbo/demos/helloword/app-server 19 | 20 | python main.py 21 | 22 | ``` 23 | 24 | 打开浏览器,访问[http://localhost:8888](http://localhost:8888) 25 | -------------------------------------------------------------------------------- /docs/zh-CN/quickstart.md: -------------------------------------------------------------------------------- 1 | # 快速开始 2 | 3 | ## 安装 4 | 5 | 快速安装 6 | 7 | ```sh 8 | 9 | pip install turbo 10 | 11 | ``` 12 | 13 | 安装最新版 14 | 15 | ```sh 16 | 17 | git clone https://github.com/wecatch/app-turbo.git 18 | cd app-turbo 19 | python setup.py install 20 | 21 | ``` 22 | 23 | ## 构建 24 | 25 | 26 | 构建项目 27 | 28 | ``` 29 | 30 | turbo-admin startproject project_name 31 | cd project_name 32 | cd app_name 33 | python main.py 34 | 35 | ``` 36 | 37 | 38 | 打开浏览器访问 39 | 40 | ``` 41 | http://localhost:8888 42 | ``` 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/zh-CN/tutorial/app.md: -------------------------------------------------------------------------------- 1 | #### 什么是 app-server 2 | 3 | app-server 是 web 应用程序,由一个或多个子 app 组成,每个 app 结构独立,可移植,可复用 4 | 5 | 6 | #### app-server 的目录结构示例 7 | 8 | 9 | ``` sh 10 | 11 | app-server/ 12 | ├── apps 13 | │   ├── base.py 14 | │   ├── __init__.py 15 | │   ├── settings.py # base.py 使用的配置 16 | │   └── user # 子 app user 17 | │   ├── app.py 18 | │   ├── base.py 19 | │   ├── __init__.py 20 | │   ├── setting.py 21 | ├── main.py # 入口 22 | ├── setting.py # 应用配置 23 | ├── templates # 模板 24 | │   └── user 25 | │   └── index.html 26 | ├── static # 静态文件 27 | │   └── js 28 | │   └── jquery.js 29 | 30 | 31 | ``` 32 | 33 | 34 | #### 建立 model 35 | 36 | 37 | 使用 turbo 的命令行工具建立 app-server 38 | 39 | ```sh 40 | 41 | turbo-admin startserver app-server 42 | 43 | ``` 44 | 45 | 46 | #### 启动app-server 47 | 48 | ``` 49 | 50 | python main.py 51 | 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /docs/zh-CN/tutorial/db.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/docs/zh-CN/tutorial/db.md -------------------------------------------------------------------------------- /docs/zh-CN/tutorial/helper.md: -------------------------------------------------------------------------------- 1 | #### 什么是**helper** 2 | 3 | helper是提供业务逻辑的层次,它继承了基础 model,在已经封装好的 pymongo api(find, find_one, insert, inc等)上进行相应的业务逻辑封装。 4 | 5 | 只需要按照规则进行配置,helper 就可以实现自动化声明和导入。 6 | 7 | #### **helper**的目录结构示例 8 | 9 | ``` 10 | 11 | helpers/ 12 | ├── wallpaper # 相应db的package,即一个具体的helper 13 | │   ├── album.py # 封装了对应model业务逻辑的模块 14 | │   ├── category.py 15 | │   ├── image.py 16 | │   ├── img2tag.py 17 | │   ├── __init__.py # 模块的内建属性__all__ 中配置需要导入的module 18 | │   └── tag.py 19 | ├── __init__.py # 自动导入helper 目录下面的 helper 20 | ├── settings.py # helper 中用到的全局公有变量和helper安装列表 21 | 22 | ``` 23 | 24 | #### helper 使用说明 25 | 26 | 应用程序启动之后,在app-server的任意地方,以下面的方式导入helper 27 | 28 | 29 | ``` 30 | from helpers import wallpaper as wallpaper_helper 31 | 32 | ``` 33 | 34 | #### helper 是如何实现在外部被引用的 35 | 36 | 在 helpers 目录下的每个的 helper 中,例如 wallpaper, 其 **__init__.py** 的 **__all__** 属性中指定所有需要导出的模块,例如 37 | 38 | ```python 39 | 40 | # __init__.py 41 | 42 | __all__ = ['album', 'img2tag'] # 导出 album.py 和 img2tag.py 43 | 44 | ``` 45 | 46 | 在要导出的模块中通过 **MODEL_SLOTS** 属性指定导出的类, 例如 47 | 48 | 49 | ```python 50 | 51 | #album.py 52 | 53 | MODEL_SLOTS = ['Album', 'FavorAlbum'] # 导出 album.py 中的 Album 和 FavorAlbum 类 54 | 55 | ``` 56 | 57 | 58 | 每个导出的类都将以**实例**和**类**变量两种形式存在于所在的 helper 的命名空间中,实例变量的的名称被自动转换为 underscore 模式,例如 59 | 60 | 61 | ```python 62 | 63 | wallpaper_helper.favor_album # 访问实例变量 64 | wallpaper_helper.FavorAlbum # 访问类变量 65 | 66 | ``` 67 | 68 | 在 helpers/setting.py 中指定导出的helper 69 | 70 | 71 | ``` 72 | 73 | INSTALLED_HELPERS = ( 74 | 'wallpaper', 75 | ) 76 | 77 | ``` 78 | 79 | 至此,导出一个helper 完成 80 | -------------------------------------------------------------------------------- /docs/zh-CN/tutorial/index.md: -------------------------------------------------------------------------------- 1 | app-turbo Tutorial 2 | ================= 3 | 4 | app-turbo 应用的主要结构分为 5 | 6 | - [models](model) 7 | - [helpers](helper) 8 | - [app-server](app-server) 9 | - [db](db) 10 | 11 | 12 | models 包含每个 db 实例对应的 collection,models 由一个到多个model 组成,每个model 对应一个db。 13 | 14 | app-server 是独立的应用 server,非 package,根据需要可以有一个到多个 app-server。 15 | 16 | helpers 由一个到多个 helper 组成,helper 是对 db 业务的实现,helper 中继承和实例化 model 层,执行对每个 collection 的具体操作,供 app-server 使用。 17 | 18 | db 是 mongodb 的连接配置 19 | -------------------------------------------------------------------------------- /docs/zh-CN/tutorial/model.md: -------------------------------------------------------------------------------- 1 | #### 什么是model 2 | 3 | model 是 mongodb collection 的简单反射,它包含了 collection 中每个 record 字段和类型的简单描述,方便在开发过程中对其进行查阅和更改。model 继承自turbo.model 的 BaseModel,BaseModel 封装了 pymongo 的基础操作和 api。 4 | 5 | 6 | #### model的目录结构 7 | 8 | ``` 9 | models 10 | ├── __init__.py 11 | ├── settings.py # model 的全局配置, 数据库连接等的配置 12 | ├── base.py # 各个db 所需要的数据库映射配置初始化, 继承model所需要的 collection 操作等 13 | ├── user 14 | │   ├── __init__.py 15 | │   ├── base.py # 每个 model 连接的 db 指定 16 | │   ├── model.py # model 对应 db 的所有 colletion 结构 17 | │   └── setting.py # 每个 model 具体的配置 18 | 19 | ``` 20 | 21 | 22 | #### 建立 model 23 | 24 | 在 models 目录下面建立model对应的package, 包含以下模块 25 | 26 | * __init__.py 27 | * base.py 28 | * setting.py 29 | * model.py 30 | 31 | 32 | 在 **base.py** 中 建立 model 对应的 db 配置 33 | 34 | ```python 35 | 36 | # base.py 37 | 38 | from models.base import * 39 | 40 | class Model(BaseModel): 41 | 42 | def __init__(self): 43 | super(Model, self).__init__(db_name='user') 44 | 45 | ``` 46 | 47 | 48 | 在 model.py 中声明相应的model 即可 49 | 50 | ``` 51 | from base import * 52 | 53 | class User2img(Model): 54 | 55 | name = 'user2img' # collection 的名字 56 | 57 | field = { 58 | 'uid': (ObjectId, None) , 59 | 'imgid': (ObjectId, None) , 60 | 'atime': (datetime, None) , 61 | 'atime': (datetime, None) , 62 | } 63 | 64 | ``` 65 | 66 | #### 在 helper 中使用 model 67 | 68 | ```python 69 | 70 | from models.user import model 71 | 72 | class User2img(model.User2img):pass 73 | 74 | ``` 75 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: turbo 2 | pages: 3 | - [index.md, Home] 4 | - [intro.md, Introduction] 5 | - [tutorial/index.md, Tutorial] 6 | - [tutorial/model.md, 'Tutorial', 'Model'] 7 | - [tutorial/helper.md, 'Tutorial', 'Helper'] 8 | - [tutorial/app.md, 'Tutorial', 'App-server'] 9 | - [tutorial/session.md, 'Tutorial', 'Session'] 10 | - [tutorial/turbo-admin.md, 'Tutorial', 'Turbo-admin'] 11 | - [api/index.md, 'API Reference'] 12 | 13 | 14 | theme: readthedocs 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymongo>=3.2 2 | requests 3 | redis 4 | docopt 5 | jinja2 6 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | tox 3 | codecov 4 | redis 5 | docopt 6 | coverage 7 | pymongo 8 | sqlalchemy -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = D203,E731,N802,C901,N801 3 | exclude = .git,__pycache__,docs/conf.py,old,build,dist,docs,demos,turbo/fake 4 | max-complexity = 10 5 | max-line-length=120 6 | format=pylint 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys 3 | import os 4 | 5 | 6 | version = __import__('turbo').version 7 | 8 | 9 | install_requires = [ 10 | 11 | ] 12 | 13 | for k in ['pymongo', 'requests', 'redis', 'docopt', 'jinja2']: 14 | try: 15 | _m = __import__(k) 16 | except ImportError: 17 | if k == 'pymongo': 18 | k = 'pymongo>=3.2' 19 | install_requires.append(k) 20 | 21 | kwargs = {} 22 | 23 | readme_path = os.path.abspath(os.path.dirname(__file__)) 24 | with open(os.path.join(readme_path, 'README.rst')) as f: 25 | kwargs['long_description'] = f.read() 26 | 27 | if sys.version_info < (2, 7): 28 | install_requires.append('unittest2') 29 | install_requires.append('tornado<=4.3.0') 30 | install_requires.append('futures') 31 | elif sys.version_info > (2, 7) and sys.version_info < (2, 7, 9): 32 | install_requires.append('tornado<5.0.0') 33 | elif sys.version_info >= (2, 7, 9) and sys.version_info < (3, 5, 2): 34 | install_requires.append('tornado<6.0.0') 35 | elif sys.version_info >= (3, 5, 2): 36 | install_requires.append('tornado>=6.0.0') 37 | 38 | 39 | setup( 40 | name="turbo", 41 | version=version, 42 | author="Wecatch.me", 43 | author_email="wecatch.me@gmail.com", 44 | url="http://github.com/wecatch/app-turbo", 45 | license="http://www.apache.org/licenses/LICENSE-2.0", 46 | description="turbo is a web framework for fast web development based in tornado, mongodb, redis", 47 | keywords='web framework tornado mongodb', 48 | packages=find_packages(), 49 | install_requires=install_requires, 50 | scripts=['turbo/bin/turbo-admin'], 51 | classifiers=[ 52 | 'License :: OSI Approved :: Apache Software License', 53 | 'Programming Language :: Python :: 2.6', 54 | 'Programming Language :: Python :: 2.7', 55 | 'Programming Language :: Python :: 3.3', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Programming Language :: Python :: 3.6', 59 | 'Programming Language :: Python :: 3.7', 60 | ], 61 | **kwargs 62 | ) 63 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | import os 3 | import sys 4 | 5 | 6 | sys.path.insert(0, os.path.abspath('..')) 7 | -------------------------------------------------------------------------------- /tests/app_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | import multiprocessing 4 | import os 5 | import signal 6 | 7 | from bson.objectid import ObjectId 8 | import requests 9 | from turbo import app 10 | from turbo import register 11 | from turbo.conf import app_config 12 | from turbo.util import ( 13 | basestring_type as basestring, 14 | file_types 15 | ) 16 | 17 | from util import unittest, port_is_used, get_free_tcp_port 18 | 19 | app_config.app_name = 'app_test' 20 | app_config.web_application_setting = { 21 | 'xsrf_cookies': False, 22 | 'cookie_secret': 'adasfd' 23 | } 24 | 25 | 26 | class HomeHandler(app.BaseHandler): 27 | 28 | def get(self): 29 | assert not self.is_ajax() 30 | self.write('get') 31 | 32 | def post(self): 33 | self.write('post') 34 | 35 | def put(self): 36 | self.write('put') 37 | 38 | 39 | class ApiHandler(app.BaseHandler): 40 | _get_required_params = [ 41 | ('skip', int, 0), 42 | ('limit', int, 20), 43 | ] 44 | 45 | _get_params = { 46 | 'need': [ 47 | ('action', None), 48 | ], 49 | 'option': [ 50 | ('who', basestring, 'python'), 51 | ('str_key', str, 'python'), 52 | ('bool', bool, False), 53 | ('int', int, 0), 54 | ('float', float, 0), 55 | ('objectid', ObjectId, None), 56 | ('list', list, []), 57 | ] 58 | } 59 | 60 | _post_required_params = [ 61 | ('skip', int, 0), 62 | ('limit', int, 20), 63 | ] 64 | 65 | _post_params = { 66 | 'need': [ 67 | ('who', basestring), 68 | ('app_test', file_types) 69 | ], 70 | } 71 | 72 | def GET(self): 73 | self._params = self.parameter 74 | 75 | assert self._params['skip'] == 0 76 | assert self._params['limit'] == 20 77 | assert self._params['who'] == 'python' 78 | assert self._params['action'] is None 79 | assert self._params['list'] == [] 80 | assert self._params['str_key'] == 'python' 81 | assert self.is_ajax() 82 | 83 | self._data = { 84 | 'value': self._params['who'] 85 | } 86 | 87 | def POST(self): 88 | self._params = self.parameter 89 | 90 | assert self._params['skip'] == 0 91 | assert self._params['limit'] == 10 92 | 93 | self._data = { 94 | 'value': self._params['who'], 95 | 'file_name': self._params['app_test'][0].filename 96 | } 97 | 98 | def PUT(self): 99 | self._data = { 100 | 'api': { 101 | 'put': 'value' 102 | } 103 | } 104 | 105 | def DELETE(self): 106 | raise Exception('value error') 107 | 108 | def wo_json(self, data): 109 | self.write(self.json_encode(data, indent=4)) 110 | 111 | 112 | PID = None 113 | URL = None 114 | 115 | 116 | def run_server(port): 117 | register.register_url('/', HomeHandler) 118 | register.register_url('', HomeHandler) 119 | register.register_url('/api', ApiHandler) 120 | app.start(port) 121 | 122 | 123 | def setUpModule(): 124 | port = get_free_tcp_port() 125 | server = multiprocessing.Process(target=run_server, args=(port,)) 126 | server.start() 127 | global PID, URL 128 | URL = 'http://localhost:%s' % port 129 | PID = server.pid 130 | 131 | 132 | def tearDownModule(): 133 | os.kill(PID, signal.SIGKILL) 134 | 135 | 136 | class AppTest(unittest.TestCase): 137 | 138 | def setUp(self): 139 | global URL 140 | self.home_url = URL 141 | self.api_url = URL + '/api' 142 | 143 | def test_get(self): 144 | resp = requests.get(self.home_url) 145 | self.assertEqual(resp.status_code, 200) 146 | 147 | def test_post(self): 148 | resp = requests.post(self.home_url) 149 | self.assertEqual(resp.status_code, 200) 150 | 151 | def test_get_api(self): 152 | resp = requests.get(self.api_url, headers={ 153 | 'X-Requested-With': 'XMLHttpRequest'}) 154 | self.assertEqual(resp.status_code, 200) 155 | self.assertEqual(resp.json()['res']['value'], 'python') 156 | 157 | def test_delete_api(self): 158 | resp = requests.delete(self.api_url, headers={ 159 | 'X-Requested-With': 'XMLHttpRequest'}) 160 | self.assertEqual(resp.status_code, 200) 161 | self.assertEqual(resp.json()['msg'], 'Unknown Error') 162 | 163 | def test_post_api(self): 164 | with open(os.path.abspath(__file__), 'rb') as file: 165 | resp = requests.post(self.api_url, headers={ 166 | 'X-Requested-With': 'XMLHttpRequest'}, 167 | data={'limit': 10, 'who': 'ruby'}, files={'app_test': file}) 168 | self.assertEqual(resp.status_code, 200) 169 | self.assertEqual(resp.json()['res']['value'], 'ruby') 170 | self.assertEqual(resp.json()['res']['file_name'], 'app_test.py') 171 | 172 | def test_404(self): 173 | resp = requests.get(self.home_url + '/hello') 174 | self.assertTrue(resp.content.find(b'404') != -1) 175 | 176 | def test_context(self): 177 | """TODO test context 178 | """ 179 | pass 180 | 181 | 182 | if __name__ == '__main__': 183 | unittest.main() 184 | -------------------------------------------------------------------------------- /tests/escape_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, with_statement 3 | 4 | import copy 5 | from datetime import datetime 6 | import logging 7 | import time 8 | 9 | from bson.objectid import ObjectId 10 | from turbo.util import escape as es, camel_to_underscore, basestring_type as basestring, unicode_type 11 | from util import unittest 12 | 13 | 14 | class EscapeTest(unittest.TestCase): 15 | 16 | def setUp(self): 17 | child = { 18 | 'id': ObjectId(), 19 | 'atime': datetime.now(), 20 | 'number': 10, 21 | 'name': 'hello world', 22 | 'mail': None, 23 | } 24 | self.record = { 25 | 'id': ObjectId(), 26 | 'atime': datetime.now(), 27 | 'number': 10, 28 | 'name': 'hello world', 29 | 'child': child, 30 | 'childs': [copy.deepcopy(child) for i in range(3)], 31 | } 32 | 33 | self.values = [copy.deepcopy(self.record) for i in range(3)] 34 | 35 | def tearDown(self): 36 | del self.record 37 | del self.values 38 | 39 | def test_to_dict_str(self): 40 | self.check_value_type(es.to_dict_str(self.record)) 41 | 42 | def test_default_encode(self): 43 | now = datetime.now() 44 | objid = ObjectId() 45 | number = 10 46 | self.assertEqual(es.default_encode(now), time.mktime(now.timetuple())) 47 | self.assertEqual(es.default_encode(objid), unicode_type(objid)) 48 | self.assertEqual(es.default_encode(number), number) 49 | 50 | def test_recursive_to_str(self): 51 | now = datetime.now() 52 | objid = ObjectId() 53 | number = 10 54 | self.check_value_type(es.to_str(self.record)) 55 | self.check_value_type(es.to_str(self.values)) 56 | self.check_value_type(es.to_str(now)) 57 | self.check_value_type(es.to_str(objid)) 58 | self.check_value_type(es.to_str(number)) 59 | 60 | def test_no_attribute(self): 61 | with self.assertRaises(AttributeError): 62 | es.create_objectid() 63 | 64 | def test_tobjectid(self): 65 | es.to_int('s') 66 | 67 | def test_json_encode(self): 68 | self.assertTrue(es.json_encode( 69 | es.to_str(self.values), indent=4) is not None) 70 | 71 | def test_json_decode(self): 72 | self.assertTrue(type(es.json_decode( 73 | es.json_encode(es.to_str(self.values), indent=4) 74 | )).__name__ == 'list' 75 | ) 76 | 77 | # error test 78 | self.assertTrue(type(es.json_decode( 79 | es.json_encode(es.to_str(self.values), invlaid=4) 80 | )).__name__ == 'NoneType' 81 | ) 82 | 83 | def test_to_list_str(self): 84 | [self.check_value_type(v) for v in es.to_list_str(self.values)] 85 | 86 | def test_camel_to_underscore(self): 87 | self.assertEqual(camel_to_underscore('HelloWorld'), 'hello_world') 88 | 89 | def check_value_type(self, v): 90 | if isinstance(v, list): 91 | [self.check_value_type(v1) for v1 in v] 92 | return 93 | 94 | if isinstance(v, dict): 95 | [self.check_value_type(v1) for k1, v1 in v.items()] 96 | return 97 | 98 | self.assertTrue(self.check_base_value_type(v)) 99 | 100 | def log(self, msg): 101 | logging.info(msg) 102 | 103 | def check_base_value_type(self, v): 104 | return isinstance(v, int) or isinstance(v, float) or isinstance(v, basestring) or v is None 105 | 106 | 107 | if __name__ == '__main__': 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /tests/flux_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | from util import unittest 4 | 5 | from turbo.flux import Mutation, register, dispatch, register_dispatch, State, state 6 | 7 | mutation = Mutation('flux_test') 8 | 9 | 10 | @register(mutation) 11 | def increase_rank(rank): 12 | return rank + 1 13 | 14 | 15 | @register(mutation) 16 | def decrease_rank(rank): 17 | return rank - 1 18 | 19 | 20 | @register_dispatch('flux_test', 'increase_rank') 21 | def increase(rank): 22 | pass 23 | 24 | 25 | def decrease(rank): 26 | return dispatch('flux_test', 'decrease_rank', rank) 27 | 28 | tstate = State('test') 29 | tstate.count = 0 30 | 31 | 32 | class FluxTest(unittest.TestCase): 33 | 34 | def setUp(self): 35 | pass 36 | 37 | def tearDown(self): 38 | pass 39 | 40 | def test_state(self): 41 | tstate.count += 1 42 | self.assertEqual(tstate.count, 1) 43 | self.assertEqual(state.test.count, 1) 44 | tstate.count += 1 45 | self.assertEqual(tstate.count, 2) 46 | self.assertEqual(state.test.count, 2) 47 | state.test.count += 1 48 | self.assertEqual(tstate.count, 3) 49 | self.assertEqual(state.test.count, 3) 50 | 51 | def test_increment(self): 52 | self.assertEqual(increase(10), 11) 53 | 54 | def test_decrease(self): 55 | self.assertEqual(decrease(10), 9) 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /tests/httputil_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, with_statement 3 | 4 | import turbo.httputil as hu 5 | from util import unittest 6 | 7 | 8 | class HttpUtilTest(unittest.TestCase): 9 | 10 | def setUp(self): 11 | pass 12 | 13 | def tearDown(self): 14 | pass 15 | 16 | def test_encode_http_params(self): 17 | keyword = '美女' 18 | paras = hu.encode_http_params(k=10, h=2, key='ass', keyword=keyword, empty='') 19 | print(paras) 20 | self.assertEqual(sorted(paras.split('&')), [ 21 | 'h=2', 'k=10', 'key=ass', 'keyword=%s' % hu.quote(keyword)]) 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /tests/jinja2_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | import multiprocessing 4 | import os 5 | import shutil 6 | import signal 7 | import socket 8 | import tempfile 9 | import time 10 | 11 | import requests 12 | from turbo import app 13 | from turbo import register 14 | from turbo.conf import app_config 15 | from turbo.template import turbo_jinja2 16 | from util import unittest 17 | 18 | 19 | sess = requests.session() 20 | sess.keep_alive = False 21 | 22 | app_config.app_name = 'app_test' 23 | app_config.app_setting['template'] = 'jinja2' 24 | tmp_source = tempfile.mkdtemp() 25 | TEMPLATE_PATH = os.path.join(tmp_source, "templates") 26 | app_config.web_application_setting = { 27 | 'xsrf_cookies': False, 28 | 'cookie_secret': 'adasfd', 29 | 'template_path': TEMPLATE_PATH, 30 | 'debug': True, 31 | } 32 | 33 | 34 | class HomeHandler(app.BaseHandler): 35 | 36 | def get(self): 37 | self.render('index.html') 38 | 39 | @turbo_jinja2 40 | def render_string(self, *args, **kwargs): 41 | pass 42 | 43 | 44 | PID = None 45 | URL = None 46 | 47 | 48 | def run_server(port): 49 | register.register_url('/', HomeHandler) 50 | register.register_url('', HomeHandler) 51 | app.start(port) 52 | 53 | 54 | def is_used(port): 55 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 56 | if sock.connect_ex(('localhost', port)) == 0: 57 | return True 58 | 59 | return False 60 | 61 | 62 | def setUpModule(): 63 | port = 10000 64 | while True: 65 | if not is_used(port): 66 | break 67 | port += 1 68 | 69 | server = multiprocessing.Process(target=run_server, args=(port,)) 70 | server.start() 71 | global PID, URL 72 | URL = 'http://localhost:%s' % port 73 | PID = server.pid 74 | 75 | 76 | def tearDownModule(): 77 | os.kill(PID, signal.SIGKILL) 78 | 79 | 80 | HTML = """{{'abcdefg' | upper}}""" 81 | 82 | 83 | class AppTest(unittest.TestCase): 84 | 85 | def setUp(self): 86 | global URL 87 | self.home_url = URL 88 | try: 89 | shutil.rmtree(TEMPLATE_PATH) 90 | except: 91 | pass 92 | os.makedirs(TEMPLATE_PATH) 93 | with open(os.path.join(TEMPLATE_PATH, 'index.html'), 'w') as f: 94 | f.write(HTML) 95 | 96 | def test_get(self): 97 | time.sleep(3) 98 | resp = sess.get(self.home_url) 99 | self.assertEqual(resp.text.strip().isupper(), True) 100 | 101 | def tearDown(self): 102 | try: 103 | shutil.rmtree(TEMPLATE_PATH) 104 | except: 105 | pass 106 | finally: 107 | os.removedirs(tmp_source) 108 | 109 | 110 | if __name__ == '__main__': 111 | unittest.main() 112 | -------------------------------------------------------------------------------- /tests/log_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, with_statement 3 | 4 | import os 5 | import logging 6 | 7 | from turbo import log 8 | from util import unittest 9 | 10 | test_log = log.getLogger(__file__) 11 | 12 | base_path = os.path.abspath(__file__) 13 | base_name = os.path.basename(base_path) 14 | test_log_name = '.'.join( 15 | base_path.split('/')[1:-1] + [base_name.split('.')[0]]) 16 | 17 | 18 | class GetLoggerTest(unittest.TestCase): 19 | 20 | def setUp(self): 21 | pass 22 | 23 | def tearDown(self): 24 | pass 25 | 26 | def test_get_logger(self): 27 | # module file log 28 | self.assertEqual(test_log.name, test_log_name) 29 | 30 | # root logger 31 | self.assertEqual(log.getLogger(), logging.root) 32 | self.assertEqual(len(log.getLogger(log_path='root.log').handlers), 2) 33 | self.assertEqual(len(log.getLogger(log_path='root2.log').handlers), 2) 34 | self.assertEqual( 35 | len(log.getLogger(level=logging.WARNING, log_path='root3.log').handlers), 3) 36 | 37 | # normal logger no handlers 38 | logger_one = log.getLogger('logger.one') 39 | self.assertEqual(logger_one.name, 'logger.one') 40 | self.assertEqual(len(logger_one.handlers), 0) 41 | 42 | # file handler one level only have only handler 43 | logger_two = log.getLogger( 44 | 'logger.two', logging.DEBUG, 'logger_test.log') 45 | self.assertEqual(logger_two.level, logging.DEBUG) 46 | self.assertEqual(len(logger_two.handlers), 1) 47 | 48 | logger_two_2 = log.getLogger( 49 | 'logger.two', logging.DEBUG, 'logger_test2.log') 50 | self.assertEqual(logger_two_2.level, logging.DEBUG) 51 | self.assertEqual(len(logger_two.handlers), 1) 52 | 53 | logger_two_3 = log.getLogger( 54 | 'logger.two', logging.ERROR, 'logger_test3.log') 55 | self.assertEqual(logger_two_3.level, logging.ERROR) 56 | self.assertEqual(len(logger_two.handlers), 2) 57 | 58 | 59 | if __name__ == '__main__': 60 | unittest.main() 61 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import ( 3 | absolute_import, 4 | division, 5 | print_function, 6 | with_statement 7 | ) 8 | import sys 9 | import os 10 | 11 | sys.path.insert( 12 | 0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 13 | 14 | from tests.util import unittest # noqa E402 15 | 16 | 17 | def main(): 18 | testSuite = unittest.TestSuite() 19 | suite = unittest.TestLoader().discover('tests', pattern='*_test.py') 20 | testSuite.addTest(suite) 21 | 22 | return testSuite 23 | 24 | 25 | if __name__ == '__main__': 26 | unittest.TextTestRunner(verbosity=2).run(main()) 27 | -------------------------------------------------------------------------------- /tests/session_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, with_statement 3 | 4 | import signal 5 | import time 6 | import requests 7 | import multiprocessing 8 | import os 9 | 10 | from turbo import app 11 | from turbo.conf import app_config 12 | from turbo import register 13 | from turbo.session import RedisStore 14 | 15 | from util import unittest, port_is_used 16 | 17 | app_config.app_name = 'app_test' 18 | app_config.web_application_setting = { 19 | 'xsrf_cookies': False, 20 | 'cookie_secret': 'asdf/asdfiw872*&^2/' 21 | } 22 | 23 | 24 | PORT = None 25 | 26 | 27 | class HomeHandler(app.BaseHandler): 28 | 29 | session_initializer = { 30 | 'time': time.time(), 31 | 'uid': None, 32 | } 33 | 34 | def get(self): 35 | assert self.session.uid is None 36 | assert self.session.session_id is not None 37 | self.write('get') 38 | 39 | def post(self): 40 | self.session.uid = '7787' 41 | self.write('post') 42 | 43 | def put(self): 44 | assert self.session.uid == '7787' 45 | self.write('put') 46 | 47 | 48 | class RedisStoreHandler(app.BaseHandler): 49 | 50 | session_initializer = { 51 | 'time': time.time(), 52 | 'uid': None, 53 | } 54 | 55 | session_store = RedisStore(timeout=3600) 56 | 57 | def get(self): 58 | assert self.session.uid is None 59 | assert self.session.session_id is not None 60 | self.write('get') 61 | 62 | def post(self): 63 | self.session.uid = '7787' 64 | self.write('post') 65 | 66 | def put(self): 67 | assert self.session.uid == '7787' 68 | self.write('put') 69 | 70 | 71 | def setUpModule(): 72 | global PORT 73 | PORT = 8888 74 | while True: 75 | if not port_is_used(PORT): 76 | break 77 | PORT += 1 78 | 79 | 80 | def run_server(): 81 | global PORT 82 | register.register_url('/', HomeHandler) 83 | register.register_url('/redis', RedisStoreHandler) 84 | app.start(PORT) 85 | 86 | 87 | class SessionTest(unittest.TestCase): 88 | 89 | def setUp(self): 90 | global PORT 91 | server = multiprocessing.Process(target=run_server) 92 | server.start() 93 | self.home_url = 'http://localhost:%s' % PORT 94 | self.redis_url = 'http://localhost:%s/redis' % PORT 95 | self.pid = server.pid 96 | time.sleep(1) 97 | 98 | def tearDown(self): 99 | os.kill(self.pid, signal.SIGKILL) 100 | 101 | def test_session(self): 102 | global PORT 103 | resp = requests.get( 104 | self.home_url, headers={'refer': 'http://127.0.0.1:%s' % PORT}) 105 | self.assertEqual(resp.status_code, 200) 106 | cookies = resp.cookies 107 | resp = requests.post(self.home_url, cookies=cookies) 108 | self.assertEqual(resp.status_code, 200) 109 | 110 | resp = requests.put(self.home_url, cookies=cookies) 111 | self.assertEqual(resp.status_code, 200) 112 | 113 | def test_redis_store_session(self): 114 | global PORT 115 | resp = requests.get( 116 | self.redis_url, headers={'refer': 'http://127.0.0.1:%s' % PORT}) 117 | self.assertEqual(resp.status_code, 200) 118 | cookies = resp.cookies 119 | resp = requests.post(self.redis_url, cookies=cookies) 120 | self.assertEqual(resp.status_code, 200) 121 | 122 | resp = requests.put(self.redis_url, cookies=cookies) 123 | self.assertEqual(resp.status_code, 200) 124 | 125 | 126 | if __name__ == '__main__': 127 | unittest.main() 128 | -------------------------------------------------------------------------------- /tests/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, with_statement 3 | 4 | import sys 5 | import socket 6 | 7 | from bson.objectid import ObjectId 8 | 9 | if sys.version_info < (2, 7): 10 | import unittest2 as unittest # noqa E401 11 | else: 12 | import unittest # noqa E401 13 | 14 | 15 | def port_is_used(port): 16 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | if sock.connect_ex(('localhost', port)) == 0: 18 | return True 19 | 20 | return False 21 | 22 | 23 | def get_free_tcp_port(): 24 | tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 25 | tcp.bind(('', 0)) 26 | addr, port = tcp.getsockname() 27 | tcp.close() 28 | return port 29 | 30 | 31 | fake_ids = [ 32 | ObjectId('586a01b6ed80083a5087c7d7'), 33 | ObjectId('586a01b6ed80083a5087c7d8'), 34 | ObjectId('586a01b6ed80083a5087c7d9'), 35 | ObjectId('586a01b6ed80083a5087c7da'), 36 | ObjectId('586a01b6ed80083a5087c7dc'), 37 | ObjectId('586a01b6ed80083a5087c7dd'), 38 | ObjectId('586a01b6ed80083a5087c7de'), 39 | ObjectId('586a01b6ed80083a5087c7df'), 40 | ObjectId('586a01b6ed80083a5087c7e0'), 41 | ObjectId('586a01b6ed80083a5087c7e1'), 42 | ObjectId('586a01b6ed80083a5087c7e2'), 43 | ObjectId('586a01b6ed80083a5087c7e3'), 44 | ObjectId('586a01b6ed80083a5087c7e5'), 45 | ObjectId('586a01b6ed80083a5087c7e6'), 46 | ObjectId('586a01b6ed80083a5087c7e7'), 47 | ObjectId('586a01b6ed80083a5087c7e8'), 48 | ObjectId('586a01b6ed80083a5087c7ea'), 49 | ObjectId('586a01b6ed80083a5087c7eb'), 50 | ObjectId('586a01b6ed80083a5087c7ec'), 51 | ObjectId('586a01b6ed80083a5087c7ed'), 52 | ObjectId('586a01b6ed80083a5087c7ee'), 53 | ObjectId('586a01b6ed80083a5087c7ef'), 54 | ObjectId('586a01b6ed80083a5087c7f0'), 55 | ObjectId('586a01b6ed80083a5087c7f1'), 56 | ObjectId('586a01b6ed80083a5087c7f3'), 57 | ObjectId('586a01b6ed80083a5087c7f4'), 58 | ObjectId('586a01b6ed80083a5087c7f5'), 59 | ObjectId('586a01b6ed80083a5087c7f6'), 60 | ObjectId('586a01b6ed80083a5087c7f8'), 61 | ObjectId('586a01b6ed80083a5087c7f9'), 62 | ObjectId('586a01b6ed80083a5087c7fa'), 63 | ObjectId('586a01b6ed80083a5087c7fb'), 64 | ObjectId('586a01b6ed80083a5087c7fc'), 65 | ObjectId('586a01b6ed80083a5087c7fd'), 66 | ObjectId('586a01b6ed80083a5087c7fe'), 67 | ObjectId('586a01b6ed80083a5087c7ff'), 68 | ObjectId('586a01b6ed80083a5087c801'), 69 | ObjectId('586a01b6ed80083a5087c802'), 70 | ObjectId('586a01b6ed80083a5087c803'), 71 | ObjectId('586a01b6ed80083a5087c804'), 72 | ObjectId('586a01b6ed80083a5087c806'), 73 | ObjectId('586a01b6ed80083a5087c807'), 74 | ObjectId('586a01b6ed80083a5087c808') 75 | ] 76 | 77 | fake_ids_2 = [ 78 | ObjectId('586a09f9ed80083a5087c809'), 79 | ObjectId('586a09f9ed80083a5087c80a'), 80 | ] 81 | -------------------------------------------------------------------------------- /tests/util_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import absolute_import, division, print_function, with_statement 3 | 4 | import datetime 5 | import json 6 | from copy import deepcopy 7 | 8 | from bson.objectid import ObjectId 9 | from turbo.util import escape, basestring_type 10 | 11 | from util import unittest 12 | 13 | 14 | class EscapeTest(unittest.TestCase): 15 | 16 | def test_inc(self): 17 | pass 18 | 19 | def test_to_str(self): 20 | data = { 21 | 'v1': 10, 22 | 'v2': datetime.datetime.now(), 23 | 'v3': ObjectId(), 24 | 'v4': 'value', 25 | } 26 | 27 | self.assertTrue(isinstance(json.dumps(escape.to_str( 28 | [deepcopy(data) for i in range(10)])), basestring_type)) 29 | self.assertTrue(isinstance(json.dumps( 30 | escape.to_str(deepcopy(data))), basestring_type)) 31 | 32 | def test_to_str_encode(self): 33 | data = { 34 | 'v1': 10, 35 | 'v2': datetime.datetime.now(), 36 | 'v3': ObjectId(), 37 | 'v4': 'value', 38 | } 39 | 40 | v = escape.to_str(data) 41 | 42 | self.assertTrue(isinstance(v['v1'], int)) 43 | self.assertTrue(isinstance(v['v2'], float)) 44 | self.assertTrue(isinstance(v['v3'], basestring_type)) 45 | self.assertTrue(isinstance(v['v4'], basestring_type)) 46 | 47 | def encode(v): 48 | return str(v) 49 | 50 | v = escape.to_str(data, encode) 51 | self.assertTrue(isinstance(v['v1'], basestring_type)) 52 | self.assertTrue(isinstance(v['v2'], basestring_type)) 53 | self.assertTrue(isinstance(v['v3'], basestring_type)) 54 | self.assertTrue(isinstance(v['v4'], basestring_type)) 55 | 56 | 57 | if __name__ == '__main__': 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py3,py33,py34,py35,py36,py37 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir}:{toxinidir}/turbo 7 | 8 | commands = python -m tests.runtests 9 | -------------------------------------------------------------------------------- /turbo/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2014 Wecatch 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """The App turbo server and tools.""" 18 | 19 | from __future__ import absolute_import, division, print_function, with_statement 20 | 21 | from .conf import app_config # noqa E401 22 | 23 | # version is a human-readable version number. 24 | 25 | # version_info is a four-tuple for programmatic comparison. The first 26 | # three numbers are the components of the version number. The fourth 27 | # is zero for an official release, positive for a development branch, 28 | # or negative for a release candidate or beta (after the base version 29 | # number has been incremented) 30 | version = '0.5.0' 31 | version_info = (0, 5, 0, 0) 32 | -------------------------------------------------------------------------------- /turbo/bin/turbo-admin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=utf-8 3 | 4 | """ 5 | 命令行处理差异工具 6 | """ 7 | from __future__ import absolute_import, print_function 8 | 9 | import os 10 | from docopt import docopt 11 | from turbo import fake 12 | from turbo.util import remove_extension 13 | import shutil 14 | 15 | HTML_TEMPLATE = """ 16 | 17 | 18 | 19 | 20 | hello turbo world 21 | 22 | 23 |

hello turbo world

24 | 25 | 26 | """ 27 | 28 | SERVER_NAME = 'app-server' 29 | APP_NAME = 'app' 30 | 31 | 32 | def build_project(project_template_path, project): 33 | global SERVER_NAME, APP_NAME, HTML_TEMPLATE 34 | project_path = os.path.abspath(os.path.join('.', project)) 35 | 36 | if os.path.exists(project_path): 37 | print('%s project existed' % project) 38 | return 39 | 40 | shutil.copytree(project_template_path, project_path, ignore=shutil.ignore_patterns(SERVER_NAME)) 41 | try: 42 | os.remove(os.path.join(project_path, '__init__.py')) 43 | except: 44 | pass 45 | 46 | remove_extension(project_path, '.pyc') 47 | build_server(project_template_path, SERVER_NAME, project_path) 48 | 49 | 50 | def build_server(project_template_path, server, cur_path='.', template_name='default'): 51 | global SERVER_NAME, HTML_TEMPLATE 52 | server_path = os.path.abspath(os.path.join(cur_path, server)) 53 | 54 | if os.path.exists(server_path): 55 | print('%s server existed' % server) 56 | return 57 | 58 | shutil.copytree(os.path.join(project_template_path, SERVER_NAME), server_path) 59 | 60 | try: 61 | os.remove(os.path.join(server_path, '__init__.py')) 62 | except: 63 | pass 64 | 65 | remove_extension(server_path, '.pyc') 66 | os.makedirs(os.path.join(server_path, 'templates/%s' % APP_NAME)) 67 | os.mkdir(os.path.join(server_path, 'static')) 68 | with open(os.path.join(server_path, 'templates/%s/index.html' % APP_NAME), 'w') as index: 69 | index.write(HTML_TEMPLATE.strip()) 70 | 71 | change_server_name(server_path, server, template_name) 72 | change_app_name(os.path.join(server_path, 'apps/%s' % APP_NAME), APP_NAME) 73 | 74 | 75 | def change_server_name(server_path, server, template_name): 76 | pydata = None 77 | pypath = os.path.join(server_path, 'setting.py') 78 | with open(pypath, 'r') as setting_file: 79 | pydata = setting_file.read().replace('{{server_name}}', server).replace('{{template_name}}', template_name) 80 | 81 | with open(pypath, 'w') as setting_file: 82 | setting_file.write(pydata) 83 | 84 | 85 | def change_app_name(app_path, app): 86 | pydata = None 87 | pypath = os.path.join(app_path, 'setting.py') 88 | with open(pypath, 'r') as setting_file: 89 | pydata = setting_file.read().replace('{{app_name}}', app) 90 | 91 | with open(pypath, 'w') as setting_file: 92 | setting_file.write(pydata) 93 | 94 | 95 | def build_app(project_template_path, app, cur_path='.'): 96 | global SERVER_NAME, APP_NAME 97 | app_path = os.path.abspath(os.path.join(cur_path, 'apps/%s' % app)) 98 | 99 | if os.path.exists(app_path): 100 | print('%s app existed' % app) 101 | return 102 | 103 | shutil.copytree(os.path.join(project_template_path, '%s/apps/%s' % (SERVER_NAME, APP_NAME)), app_path) 104 | remove_extension(app_path, '.pyc') 105 | change_app_name(app_path, app) 106 | 107 | 108 | def build_index(name): 109 | import sys 110 | import inspect 111 | from turbo.util import import_object, build_index as bi 112 | model_path = os.path.abspath('.') 113 | sys.path.insert(0, model_path) 114 | 115 | model = import_object('models.%s.model' % name) 116 | bi([model]) 117 | 118 | 119 | if __name__ == '__main__': 120 | project_template_path = os.path.dirname(fake.__file__) 121 | project_template_path = os.path.join(project_template_path, 'project_template') 122 | helpdoc = """turbo init project, server or app. 123 | Usage: 124 | turbo-admin (-h | --help) 125 | turbo-admin startproject 126 | turbo-admin startserver [--template=] 127 | turbo-admin startapp 128 | turbo-admin index 129 | 130 | Options: 131 | -h, --help Show help document 132 | --template= Template engine in turbo app: default or jinja2 [default: default]. 133 | """ 134 | rgs = docopt(helpdoc) 135 | if rgs.get('startproject'): 136 | project = rgs.get('') 137 | if project: 138 | build_project(project_template_path, project) 139 | else: 140 | print("turbo-admin startproject ") 141 | 142 | if rgs.get('startserver'): 143 | server = rgs.get('') 144 | template_name = rgs.get('--template') 145 | if server: 146 | build_server(project_template_path, server, template_name=template_name or 'default') 147 | else: 148 | print("turbo-admin startserver ") 149 | 150 | if rgs.get('startapp'): 151 | app = rgs.get('') 152 | if app: 153 | build_app(project_template_path, app) 154 | else: 155 | print("turbo-admin startapp ") 156 | 157 | if rgs.get('index'): 158 | model_name = rgs.get('') 159 | if model_name: 160 | build_index(model_name) 161 | -------------------------------------------------------------------------------- /turbo/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | 4 | class ObjectDict(dict): 5 | """Makes a dictionary behave like an object, with attribute-style access. 6 | """ 7 | 8 | def __getattr__(self, name): 9 | try: 10 | return self[name] 11 | except KeyError: 12 | raise AttributeError(name) 13 | 14 | def __setattr__(self, name, value): 15 | self[name] = value 16 | 17 | 18 | class AppConfig(object): 19 | 20 | _cookie_session_config = ObjectDict( 21 | name='session_id', 22 | cookie_domain=None, 23 | cookie_path='/', 24 | cookie_expires=86400, # cookie expired 24 hours in seconds 25 | secure=True, 26 | secret_key='fLjUfxqXtfNoIldA0A0J', # generate session id, 27 | timeout=86400, # session timeout 24 hours in seconds 28 | ) 29 | 30 | _store_config = ObjectDict( 31 | diskpath='/tmp/session', 32 | ) 33 | 34 | def __init__(self): 35 | self.app_name = '' 36 | self.urls = [] 37 | self.error_handler = None 38 | self.app_setting = {} 39 | self.web_application_setting = {'debug': False} 40 | self.project_name = None 41 | self.session_config = self._cookie_session_config 42 | self.store_config = self._store_config 43 | 44 | @property 45 | def log_level(self): 46 | import logging 47 | level = self.app_setting.get('log', {}).get('log_level') 48 | if level is None: 49 | return logging.INFO 50 | 51 | return level 52 | 53 | 54 | app_config = AppConfig() 55 | -------------------------------------------------------------------------------- /turbo/core/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | -------------------------------------------------------------------------------- /turbo/core/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | 4 | class ResponseMsg(Exception): 5 | 6 | def __init__(self, code='', msg=''): 7 | self.code = code 8 | self.msg = msg 9 | super(ResponseMsg, self).__init__(msg) 10 | 11 | def __str__(self): 12 | return '%s %s' % (self.code, self.msg) 13 | 14 | 15 | class ResponseError(ResponseMsg): 16 | 17 | def __init__(self, code='', msg=''): 18 | super(ResponseError, self).__init__(code, msg) 19 | -------------------------------------------------------------------------------- /turbo/fake/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/app-server/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/app-server/apps/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from turbo import register 3 | 4 | from . import api 5 | from . import app 6 | 7 | register.register_group_urls('', [ 8 | ('/', app.HomeHandler), 9 | ('/plus', app.IncHandler), 10 | ('/minus', app.MinusHandler), 11 | ]) 12 | 13 | register.register_group_urls('/v1', [ 14 | ('', api.HomeHandler), 15 | ]) 16 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/app/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import turbo.log 4 | 5 | from .base import BaseHandler 6 | 7 | logger = turbo.log.getLogger(__file__) 8 | 9 | 10 | class HomeHandler(BaseHandler): 11 | 12 | def GET(self): 13 | pass 14 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/app/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import turbo.log 4 | 5 | from .base import BaseHandler 6 | from store import actions 7 | 8 | logger = turbo.log.getLogger(__file__) 9 | 10 | 11 | class HomeHandler(BaseHandler): 12 | 13 | def get(self): 14 | self.render('index.html') 15 | 16 | 17 | class IncHandler(BaseHandler): 18 | 19 | _get_params = { 20 | 'option': [ 21 | ('value', int, 0) 22 | ] 23 | } 24 | 25 | def get(self): 26 | self._data = actions.increase(self._params['value']) 27 | self.write(str(self._data)) 28 | 29 | 30 | class MinusHandler(BaseHandler): 31 | 32 | _get_params = { 33 | 'option': [ 34 | ('value', int, 0) 35 | ] 36 | } 37 | 38 | def get(self): 39 | self._data = actions.decrease(self._params['value']) 40 | self.write(str(self._data)) 41 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/app/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from . import setting 4 | from apps import base 5 | 6 | 7 | class BaseHandler(base.BaseHandler): 8 | 9 | def initialize(self): 10 | super(BaseHandler, self).initialize() 11 | self.template_path = setting.TEMPLATE_PATH 12 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/app/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # sub app setting 4 | # try not to include function or class 5 | 6 | TEMPLATE_PATH = '{{app_name}}/' 7 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import tornado.web 4 | import turbo.app 5 | from turbo import app_config 6 | from turbo.core.exceptions import ResponseError, ResponseMsg 7 | 8 | 9 | # start use session from here 10 | # from lib.session import SessionStore, SessionObject 11 | # from turbo.template import turbo_jinja2 12 | 13 | 14 | class MixinHandler(turbo.app.BaseHandler): 15 | pass 16 | 17 | 18 | class BaseHandler(MixinHandler): 19 | 20 | # session_initializer = { 21 | # 'uid': None, 22 | # 'avatar': None, 23 | # 'nickname': None, 24 | # } 25 | # session_object = SessionObject 26 | # session_store = SessionStore() 27 | 28 | def initialize(self): 29 | super(BaseHandler, self).initialize() 30 | self._params = self.parameter 31 | 32 | # @turbo_jinja2 33 | # def render_string(self, template_name, **kwargs): 34 | # pass 35 | 36 | def prepare(self): 37 | super(BaseHandler, self).prepare() 38 | 39 | def response_msg(self, msg='', code=1): 40 | raise ResponseMsg(code, msg) 41 | 42 | def response_error(self, msg='', code=1): 43 | raise ResponseError(code, msg) 44 | 45 | def http_error(self, status_code=404): 46 | raise tornado.web.HTTPError(status_code) 47 | 48 | def write_error(self, status_code, **kwargs): 49 | """Override to implement custom error pages. 50 | http://tornado.readthedocs.org/en/stable/_modules/tornado/web.html#RequestHandler.write_error 51 | """ 52 | super(BaseHandler, self).write_error(status_code, **kwargs) 53 | 54 | 55 | class ErrorHandler(BaseHandler): 56 | 57 | def initialize(self, status_code): 58 | super(ErrorHandler, self).initialize() 59 | self.set_status(status_code) 60 | 61 | def prepare(self): 62 | if not self.is_ajax(): 63 | if self.get_status() == 404: 64 | raise self.http_error(404) 65 | else: 66 | self.wo_resp({'code': 1, 'msg': 'Api Not found'}) 67 | self.finish() 68 | return 69 | 70 | def check_xsrf_cookie(self): 71 | # POSTs to an ErrorHandler don't actually have side effects, 72 | # so we don't need to check the xsrf token. This allows POSTs 73 | # to the wrong url to return a 404 instead of 403. 74 | pass 75 | 76 | 77 | app_config.error_handler = ErrorHandler 78 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/apps/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # installed app list 4 | INSTALLED_APPS = ( 5 | 'app', 6 | ) 7 | 8 | # language 9 | LANG = 'en' 10 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import tornado.options 4 | import turbo.app 5 | import turbo.register 6 | from tornado.options import define, options 7 | 8 | import setting 9 | 10 | # uncomment this to init state manager: store 11 | # import store 12 | 13 | turbo.register.register_app( 14 | setting.SERVER_NAME, 15 | setting.TURBO_APP_SETTING, 16 | setting.WEB_APPLICATION_SETTING, 17 | __file__, 18 | globals() 19 | ) 20 | 21 | define("port", default=8888, type=int) 22 | 23 | if __name__ == '__main__': 24 | tornado.options.parse_command_line() 25 | turbo.app.start(options.port) 26 | -------------------------------------------------------------------------------- /turbo/fake/project_template/app-server/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | from tornado.util import ObjectDict 7 | 8 | # server name 9 | SERVER_NAME = '{{server_name}}' 10 | 11 | # server dir 12 | SERVER_DIR = os.path.dirname(os.path.abspath(__file__)) 13 | # project dir 14 | PROJECT_DIR = os.path.dirname(SERVER_DIR) 15 | sys.path.append(PROJECT_DIR) 16 | 17 | # tornado web application settings 18 | # details in 19 | # http://www.tornadoweb.org/en/stable/web.html#tornado.web.Application.settings 20 | WEB_APPLICATION_SETTING = ObjectDict( 21 | static_path=os.path.join(SERVER_DIR, "static"), 22 | template_path=os.path.join(SERVER_DIR, "templates"), 23 | xsrf_cookies=True, 24 | cookie_secret="3%$334ma?asdf2987^%23&^%$2", 25 | ) 26 | 27 | # turbo app setting 28 | TURBO_APP_SETTING = ObjectDict( 29 | log=ObjectDict( 30 | log_path=os.path.join("/var/log/", SERVER_NAME + '.log'), 31 | log_size=500 * 1024 * 1024, 32 | log_count=3, 33 | ), 34 | session_config=ObjectDict({ 35 | 'name': 'session-id', 36 | 'secret_key': 'o387xn4ma?adfasdfa83284&^%$2' 37 | }), 38 | template='{{template_name}}', 39 | ) 40 | 41 | # check if app start in debug 42 | if os.path.exists(os.path.join(SERVER_DIR, '__test__')): 43 | WEB_APPLICATION_SETTING['debug'] = True 44 | TURBO_APP_SETTING.log.log_path = os.path.join("", SERVER_NAME + '.log') 45 | -------------------------------------------------------------------------------- /turbo/fake/project_template/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/config/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/db/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/db/mongo_conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import os 4 | 5 | import gridfs 6 | from pymongo import MongoClient, MongoReplicaSetClient, read_preferences 7 | 8 | from . import setting 9 | 10 | mc = MongoClient(host='localhost') 11 | 12 | # test 13 | test = mc['test'] 14 | test_files = gridfs.GridFS(mc['test_files']) 15 | 16 | # user 17 | user = mc['user'] 18 | user_files = gridfs.GridFS(mc['user_files']) 19 | -------------------------------------------------------------------------------- /turbo/fake/project_template/db/mysql_conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | from sqlalchemy.engine.url import URL 6 | 7 | from .setting import DB_SETTING, DRIVERNAME 8 | 9 | 10 | # mysql blog 11 | blog_engine = create_engine( 12 | URL(DRIVERNAME, **DB_SETTING), encoding='utf8', echo=True) 13 | DBSession = sessionmaker(bind=blog_engine) 14 | -------------------------------------------------------------------------------- /turbo/fake/project_template/db/setting.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | 4 | REPL_SET_NAME = 'rs0' 5 | 6 | HOSTS = ['server1:27017', 'server2:27017', 'server3:27017'] 7 | 8 | DB_SETTING = { 9 | 'username': 'root', 10 | 'password': '', 11 | 'host': 'localhost', 12 | 'port': 3306, 13 | 'database': 'blog' 14 | } 15 | 16 | DRIVERNAME = 'mysql+pymysql' -------------------------------------------------------------------------------- /turbo/fake/project_template/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .settings import INSTALLED_HELPERS 4 | 5 | import turbo.helper 6 | 7 | turbo.helper.install_helper(INSTALLED_HELPERS, globals()) 8 | -------------------------------------------------------------------------------- /turbo/fake/project_template/helpers/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # installed helper list 4 | INSTALLED_HELPERS = ( 5 | 'user', 6 | ) 7 | -------------------------------------------------------------------------------- /turbo/fake/project_template/helpers/user/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | # used by helpers install helper 4 | __all__ = ['user', ] 5 | -------------------------------------------------------------------------------- /turbo/fake/project_template/helpers/user/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from datetime import datetime, timedelta 4 | 5 | from pymongo import ASCENDING, DESCENDING 6 | 7 | from helpers import settings 8 | from models.user import model as user_model 9 | 10 | MODEL_SLOTS = ['User'] 11 | 12 | 13 | class User(user_model.User): 14 | 15 | def hello_user(self): 16 | self.instance('user.User').find_one() 17 | -------------------------------------------------------------------------------- /turbo/fake/project_template/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/models/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/models/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import time 4 | from datetime import datetime 5 | 6 | import turbo.model 7 | from bson.objectid import ObjectId 8 | from pymongo import ASCENDING, DESCENDING 9 | 10 | from .settings import MONGO_DB_MAPPING as _MONGO_DB_MAPPING 11 | 12 | _PACKAGE_SPACE = globals() 13 | 14 | 15 | class BaseModel(turbo.model.BaseModel): 16 | 17 | # for import_model work 18 | package_space = _PACKAGE_SPACE 19 | 20 | def __init__(self, db_name='test'): 21 | super(BaseModel, self).__init__(db_name, _MONGO_DB_MAPPING) 22 | -------------------------------------------------------------------------------- /turbo/fake/project_template/models/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from db.mongo_conn import ( 4 | test as _test, 5 | user as _user, 6 | 7 | test_files as _test_files, 8 | user_files as _user_files, 9 | ) 10 | 11 | MONGO_DB_MAPPING = { 12 | 'db': { 13 | 'test': _test, 14 | 'user': _user, 15 | }, 16 | 'db_file': { 17 | 'test': _test_files, 18 | 'user': _user_files, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /turbo/fake/project_template/models/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/models/user/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/models/user/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from models.base import * 4 | 5 | 6 | class Model(BaseModel): 7 | 8 | def __init__(self): 9 | super(Model, self).__init__(db_name='user') 10 | -------------------------------------------------------------------------------- /turbo/fake/project_template/models/user/model.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .base import * 4 | 5 | 6 | class User(Model): 7 | 8 | """ 9 | email: user account 10 | passwd: user account passwd 11 | atime: added time 12 | """ 13 | index = [ 14 | tuple([('email', 1)]) 15 | ] 16 | name = 'user' 17 | 18 | field = { 19 | 'email': (str, ''), 20 | 'passwd': (str, ''), 21 | 'atime': (datetime, None), 22 | } 23 | -------------------------------------------------------------------------------- /turbo/fake/project_template/models/user/setting.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/models/user/setting.py -------------------------------------------------------------------------------- /turbo/fake/project_template/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/service/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/store/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from .modules import user 4 | -------------------------------------------------------------------------------- /turbo/fake/project_template/store/actions.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from turbo.flux import Mutation, dispatch, register, register_dispatch 4 | 5 | from . import mutation_types 6 | 7 | 8 | @register_dispatch('user', mutation_types.INCREASE) 9 | def increase(rank): 10 | pass 11 | 12 | 13 | def decrease(rank): 14 | return dispatch('user', mutation_types.DECREASE, rank) 15 | -------------------------------------------------------------------------------- /turbo/fake/project_template/store/modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/store/modules/__init__.py -------------------------------------------------------------------------------- /turbo/fake/project_template/store/modules/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | from turbo.flux import Mutation, State, register 4 | 5 | mutation = Mutation(__file__) 6 | state = State(__file__) 7 | 8 | 9 | @register(mutation) 10 | def increase_rank(rank): 11 | return rank + 1 12 | 13 | 14 | @register(mutation) 15 | def decrease_rank(rank): 16 | return rank - 1 17 | -------------------------------------------------------------------------------- /turbo/fake/project_template/store/mutation_types.py: -------------------------------------------------------------------------------- 1 | INCREASE = 'increase_rank' 2 | DECREASE = 'decrease_rank' 3 | -------------------------------------------------------------------------------- /turbo/fake/project_template/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wecatch/app-turbo/809c6fdc54fac18441b1d7730ed2c7c75344d705/turbo/fake/project_template/utils/__init__.py -------------------------------------------------------------------------------- /turbo/flux.py: -------------------------------------------------------------------------------- 1 | import os 2 | import functools 3 | import inspect 4 | import weakref 5 | 6 | from turbo.util import get_func_name 7 | 8 | _mutation = {} 9 | 10 | 11 | def register(state): 12 | def outwrapper(func): 13 | state.register(func) 14 | 15 | @functools.wraps(func) 16 | def wrapper(*args, **kwargs): 17 | return func(*args, **kwargs) 18 | 19 | return wrapper 20 | return outwrapper 21 | 22 | 23 | class CallFuncAsAttr(object): 24 | 25 | class __CallObject(object): 26 | 27 | def __init__(self, func): 28 | self.func = func 29 | 30 | def __call__(self, *args, **kwargs): 31 | return self.func(*args, **kwargs) 32 | 33 | def __init__(self, file_attr): 34 | name = file_attr 35 | filepath = os.path.abspath(file_attr) 36 | if os.path.isfile(filepath): 37 | name, ext = os.path.splitext(os.path.basename(filepath)) 38 | 39 | setattr(self, self._name, {}) 40 | _mutation[name] = weakref.ref(self) 41 | 42 | @property 43 | def __get_func(self): 44 | return getattr(self, self._name) 45 | 46 | def register(self, func): 47 | if not inspect.isfunction(func): 48 | raise TypeError("argument expect function, now is '%s'" % func) 49 | 50 | name = get_func_name(func) 51 | lambda_name = get_func_name(lambda x: x) 52 | if name == lambda_name: 53 | raise TypeError('lambda is not allowed') 54 | 55 | self.__get_func[name] = self.__CallObject(func) 56 | 57 | def __getattr__(self, name): 58 | if name not in self.__get_func: 59 | raise AttributeError("%s object has no attribute '%s'" % 60 | (self.__class__.__name__, name)) 61 | 62 | return self.__get_func[name] 63 | 64 | 65 | class ObjectDict(dict): 66 | """Makes a dictionary behave like an object, with attribute-style access. 67 | """ 68 | 69 | def __getattr__(self, name): 70 | try: 71 | return self[name] 72 | except KeyError: 73 | raise AttributeError(name) 74 | 75 | def __setattr__(self, name, value): 76 | self[name] = value 77 | 78 | 79 | state = ObjectDict() 80 | 81 | 82 | class State(object): 83 | 84 | __slots__ = ['_state'] 85 | 86 | def __init__(self, file_attr): 87 | name = file_attr 88 | filepath = os.path.abspath(file_attr) 89 | if os.path.isfile(filepath): 90 | name, ext = os.path.splitext(os.path.basename(filepath)) 91 | 92 | if name in state: 93 | raise KeyError('state %s has already existed' % name) 94 | 95 | self._state = ObjectDict() 96 | state[name] = self._state 97 | 98 | def __setattr__(self, name, value): 99 | if name in self.__slots__: 100 | return super(State, self).__setattr__(name, value) 101 | 102 | self._state[name] = value 103 | 104 | def __getattr__(self, name): 105 | if name not in self._state: 106 | raise AttributeError("%s object has no attribute '%s'" % 107 | (self.__class__.__name__, name)) 108 | 109 | return self._state[name] 110 | 111 | 112 | class Mutation(CallFuncAsAttr): 113 | 114 | @property 115 | def _name(self): 116 | return 'mutation_%s' % id(self) 117 | 118 | 119 | def dispatch(name, type_name, *args, **kwargs): 120 | if name not in _mutation: 121 | raise ValueError('%s mutation module not found' % name) 122 | 123 | return getattr(_mutation[name](), type_name)(*args, **kwargs) 124 | 125 | 126 | def register_dispatch(name, type_name): 127 | def outwrapper(func): 128 | @functools.wraps(func) 129 | def wrapper(*args, **kwargs): 130 | return dispatch(name, type_name, *args, **kwargs) 131 | 132 | return wrapper 133 | return outwrapper 134 | -------------------------------------------------------------------------------- /turbo/helper.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | 4 | import sys 5 | 6 | from turbo.log import helper_log 7 | from turbo.util import import_object, camel_to_underscore 8 | 9 | 10 | class _HelperObjectDict(dict): 11 | 12 | def __setitem__(self, name, value): 13 | return super(_HelperObjectDict, self).setdefault(name, value) 14 | 15 | def __getattr__(self, name): 16 | try: 17 | return self[name] 18 | except KeyError: 19 | raise ValueError(name) 20 | 21 | 22 | def install_helper(installing_helper_list, package_space): 23 | for item in installing_helper_list: 24 | # db model package 25 | package = import_object('.'.join(['helpers', item]), package_space) 26 | package_space[item] = _HelperObjectDict() 27 | # all py files included by package 28 | all_modules = getattr(package, '__all__', []) 29 | for m in all_modules: 30 | try: 31 | module = import_object( 32 | '.'.join(['helpers', item, m]), package_space) 33 | except: # noqa 34 | helper_log.error('module helpers.%s.%s Import Error' % 35 | (item, m), exc_info=True) 36 | sys.exit(0) 37 | 38 | for model_name in getattr(module, 'MODEL_SLOTS', []): 39 | model = getattr(module, model_name, None) 40 | if model: 41 | camel_name = model.__name__ 42 | underscore_name = camel_to_underscore(camel_name) 43 | 44 | package_space[item][underscore_name] = model() 45 | package_space[item][camel_name] = model 46 | -------------------------------------------------------------------------------- /turbo/httputil.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | from turbo.util import basestring_type, unicode_type, PY3, to_basestring 4 | 5 | if PY3: 6 | from urllib.parse import quote 7 | else: 8 | from urllib import quote 9 | 10 | 11 | def is_empty(v): 12 | if isinstance(v, basestring_type): 13 | if not v: 14 | return True 15 | if v is None: 16 | return True 17 | 18 | return False 19 | 20 | 21 | def utf8(v): 22 | return v.encode('utf-8') if isinstance(v, unicode_type) else str(v) 23 | 24 | 25 | def encode_http_params(**kw): 26 | ''' 27 | url paremeter encode 28 | ''' 29 | try: 30 | _fo = lambda k, v: '{name}={value}'.format( 31 | name=k, value=to_basestring(quote(v))) 32 | except: # noqa 33 | _fo = lambda k, v: '%s=%s' % (k, to_basestring(quote(v))) 34 | 35 | _en = utf8 36 | 37 | return '&'.join([_fo(k, _en(v)) for k, v in kw.items() if not is_empty(v)]) 38 | -------------------------------------------------------------------------------- /turbo/log.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | 4 | import os 5 | import sys 6 | import logging 7 | import logging.handlers 8 | 9 | from turbo.conf import app_config 10 | 11 | PY3 = sys.version_info >= (3,) 12 | 13 | _formatter = logging.Formatter( 14 | '%(levelname)s:%(asctime)s %(name)s:%(lineno)d:%(funcName)s %(message)s') 15 | 16 | if PY3: 17 | basestring_type = str 18 | else: 19 | basestring_type = basestring # noqa: F821 20 | 21 | 22 | def _init_file_logger(logger, level, log_path, log_size, log_count): 23 | """ 24 | one logger only have one level RotatingFileHandler 25 | """ 26 | if level not in [logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL]: 27 | level = logging.DEBUG 28 | 29 | for h in logger.handlers: 30 | if isinstance(h, logging.handlers.RotatingFileHandler): 31 | if h.level == level: 32 | return 33 | 34 | fh = logging.handlers.RotatingFileHandler( 35 | log_path, maxBytes=log_size, backupCount=log_count) 36 | fh.setLevel(level) 37 | fh.setFormatter(_formatter) 38 | logger.addHandler(fh) 39 | 40 | 41 | def _init_stream_logger(logger, level=None): 42 | ch = logging.StreamHandler() 43 | ch.setLevel(level or logging.DEBUG) 44 | ch.setFormatter(_formatter) 45 | logger.addHandler(ch) 46 | 47 | 48 | def _module_logger(path): 49 | file_name = os.path.basename(path) 50 | module_name = file_name[0:file_name.rfind('.py')] 51 | logger_name_list = [module_name] 52 | 53 | # find project root dir util find it or to root dir '/' 54 | while True: 55 | # root path check 56 | path = os.path.dirname(path) 57 | if path == '/': 58 | break 59 | 60 | # project root path 61 | dirname = os.path.basename(path) 62 | if dirname == app_config.project_name: 63 | break 64 | logger_name_list.append(dirname) 65 | 66 | logger_name_list.reverse() 67 | 68 | return logging.getLogger('.'.join(logger_name_list)) 69 | 70 | 71 | def getLogger(currfile=None, level=None, log_path=None, log_size=500 * 1024 * 1024, log_count=3): 72 | # init logger first 73 | logger = None 74 | 75 | # py module logger 76 | if currfile is not None: 77 | path = os.path.abspath(currfile) 78 | if os.path.isfile(path): 79 | logger = _module_logger(path) 80 | elif isinstance(currfile, basestring_type) and currfile.strip(): 81 | # normal logger 82 | logger = logging.getLogger(currfile) 83 | 84 | logger.setLevel(level or app_config.log_level) 85 | 86 | if not logger: 87 | logger = logging.getLogger() 88 | 89 | # keep the root logger at least have one streamhandler 90 | if not logger.root.handlers: 91 | _init_stream_logger(logger.root, level) 92 | 93 | if log_path: 94 | _init_file_logger(logger, level, log_path, log_size, log_count) 95 | 96 | return logger 97 | 98 | 99 | # Logger objects for internal turbo use 100 | app_log = getLogger('turbo.app') 101 | model_log = getLogger('turbo.model', level=logging.WARNING) 102 | util_log = getLogger('turbo.util') 103 | helper_log = getLogger('turbo.helper') 104 | session_log = getLogger('turbo.session') 105 | -------------------------------------------------------------------------------- /turbo/model.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import ( 3 | absolute_import, 4 | division, 5 | print_function, 6 | with_statement, 7 | ) 8 | import warnings 9 | 10 | from bson.objectid import ObjectId 11 | from pymongo import DESCENDING, collection 12 | 13 | from turbo import mongo_model 14 | from turbo.mongo_model import MixinModel # noqa E401 compatibility for turbo below 0.4.5 15 | 16 | 17 | class BaseBaseModel(mongo_model.AbstractModel): 18 | """class implement almost all mongodb collection method 19 | """ 20 | 21 | def __init__(self, db_name='test', _mongo_db_mapping=None): 22 | self.__collect, self.__gridfs = super( 23 | BaseBaseModel, self)._init(db_name, _mongo_db_mapping) 24 | 25 | def __getattr__(self, k): 26 | attr = getattr(self.__collect, k) 27 | if isinstance(attr, collection.Collection): 28 | raise AttributeError( 29 | "model object '%s' has not attribute '%s'" % (self.name, k)) 30 | return attr 31 | 32 | def sub_collection(self, name): 33 | return self.__collect[name] 34 | 35 | def insert(self, doc_or_docs, **kwargs): 36 | """Insert method 37 | """ 38 | check = kwargs.pop('check', True) 39 | if isinstance(doc_or_docs, dict): 40 | if check is True: 41 | doc_or_docs = self._valid_record(doc_or_docs) 42 | result = self.__collect.insert_one(doc_or_docs, **kwargs) 43 | return result.inserted_id 44 | else: 45 | if check is True: 46 | for d in doc_or_docs: 47 | d = self._valid_record(d) 48 | result = self.__collect.insert_many(doc_or_docs, **kwargs) 49 | return result.inserted_ids 50 | 51 | def save(self, to_save, **kwargs): 52 | """save method 53 | """ 54 | check = kwargs.pop('check', True) 55 | if check: 56 | self._valid_record(to_save) 57 | if '_id' in to_save: 58 | self.__collect.replace_one( 59 | {'_id': to_save['_id']}, to_save, **kwargs) 60 | return to_save['_id'] 61 | else: 62 | result = self.__collect.insert_one(to_save, **kwargs) 63 | return result.inserted_id 64 | 65 | def update(self, filter_, document, multi=False, **kwargs): 66 | """update method 67 | """ 68 | self._valide_update_document(document) 69 | if multi: 70 | return self.__collect.update_many(filter_, document, **kwargs) 71 | else: 72 | return self.__collect.update_one(filter_, document, **kwargs) 73 | 74 | def remove(self, filter_=None, **kwargs): 75 | """collection remove method 76 | warning: 77 | if you want to remove all documents, 78 | you must override _remove_all method to make sure 79 | you understand the result what you do 80 | """ 81 | if isinstance(filter_, dict) and filter_ == {}: 82 | raise ValueError('not allowed remove all documents') 83 | 84 | if filter_ is None: 85 | raise ValueError('not allowed remove all documents') 86 | 87 | if kwargs.pop('multi', False) is True: 88 | return self.__collect.delete_many(filter_, **kwargs) 89 | else: 90 | return self.__collect.delete_one(filter_, **kwargs) 91 | 92 | def insert_one(self, doc_or_docs, **kwargs): 93 | """Insert method 94 | """ 95 | check = kwargs.pop('check', True) 96 | if check is True: 97 | self._valid_record(doc_or_docs) 98 | 99 | return self.__collect.insert_one(doc_or_docs, **kwargs) 100 | 101 | def insert_many(self, doc_or_docs, **kwargs): 102 | """Insert method 103 | """ 104 | check = kwargs.pop('check', True) 105 | if check is True: 106 | for i in doc_or_docs: 107 | i = self._valid_record(i) 108 | 109 | return self.__collect.insert_many(doc_or_docs, **kwargs) 110 | 111 | def find_one(self, filter_=None, *args, **kwargs): 112 | """find_one method 113 | """ 114 | wrapper = kwargs.pop('wrapper', False) 115 | if wrapper is True: 116 | return self._wrapper_find_one(filter_, *args, **kwargs) 117 | 118 | return self.__collect.find_one(filter_, *args, **kwargs) 119 | 120 | def find(self, *args, **kwargs): 121 | """collection find method 122 | 123 | """ 124 | wrapper = kwargs.pop('wrapper', False) 125 | if wrapper is True: 126 | return self._wrapper_find(*args, **kwargs) 127 | 128 | return self.__collect.find(*args, **kwargs) 129 | 130 | @mongo_model.convert_to_record 131 | def _wrapper_find_one(self, filter_=None, *args, **kwargs): 132 | """Convert record to a dict that has no key error 133 | """ 134 | return self.__collect.find_one(filter_, *args, **kwargs) 135 | 136 | @mongo_model.convert_to_record 137 | def _wrapper_find(self, *args, **kwargs): 138 | """Convert record to a dict that has no key error 139 | """ 140 | return self.__collect.find(*args, **kwargs) 141 | 142 | def update_one(self, filter_, document, **kwargs): 143 | """update method 144 | """ 145 | self._valide_update_document(document) 146 | return self.__collect.update_one(filter_, document, **kwargs) 147 | 148 | def update_many(self, filter_, document, **kwargs): 149 | self._valide_update_document(document) 150 | return self.__collect.update_many(filter_, document, **kwargs) 151 | 152 | def delete_many(self, filter_): 153 | if isinstance(filter_, dict) and filter_ == {}: 154 | raise ValueError('not allowed remove all documents') 155 | 156 | if filter_ is None: 157 | raise ValueError('not allowed remove all documents') 158 | 159 | return self.__collect.delete_many(filter_) 160 | 161 | def find_by_id(self, _id, projection=None): 162 | """find record by _id 163 | """ 164 | if isinstance(_id, list) or isinstance(_id, tuple): 165 | return list(self.__collect.find( 166 | {'_id': {'$in': [self._to_primary_key(i) for i in _id]}}, projection)) 167 | 168 | document_id = self._to_primary_key(_id) 169 | 170 | if document_id is None: 171 | return None 172 | 173 | return self.__collect.find_one({'_id': document_id}, projection) 174 | 175 | def remove_by_id(self, _id): 176 | if isinstance(_id, list) or isinstance(_id, tuple): 177 | return self.__collect.delete_many( 178 | {'_id': {'$in': [self._to_primary_key(i) for i in _id]}}) 179 | 180 | return self.__collect.remove({'_id': self._to_primary_key(_id)}) 181 | 182 | def find_new_one(self, *args, **kwargs): 183 | cur = list(self.__collect.find( 184 | *args, **kwargs).limit(1).sort('_id', DESCENDING)) 185 | if cur: 186 | return cur[0] 187 | 188 | return None 189 | 190 | def get_as_dict(self, condition=None, projection=None, skip=0, limit=0, sort=None): 191 | as_list = self.__collect.find( 192 | condition, projection, skip=skip, limit=limit, sort=sort) 193 | 194 | as_dict, as_list = {}, [] 195 | for i in as_list: 196 | as_dict[i['_id']] = i 197 | as_list.append(i) 198 | 199 | return as_dict, as_list 200 | 201 | def inc(self, filter_, key, num=1, multi=False): 202 | if multi: 203 | self.__collect.update_many(filter_, {'$inc': {key: num}}) 204 | else: 205 | self.__collect.update_one(filter_, {'$inc': {key: num}}) 206 | 207 | def put(self, value, **kwargs): 208 | if value: 209 | return self.__gridfs.put(value, **kwargs) 210 | return None 211 | 212 | def delete(self, _id): 213 | return self.__gridfs.delete(self.to_objectid(_id)) 214 | 215 | def get(self, _id): 216 | return self.__gridfs.get(self.to_objectid(_id)) 217 | 218 | def read(self, _id): 219 | return self.__gridfs.get(self.to_objectid(_id)).read() 220 | 221 | def create(self, *args, **kwargs): 222 | warnings.warn("create is deprecated. Use insert or insert_one " 223 | "instead", DeprecationWarning, stacklevel=2) 224 | return self.insert(*args, **kwargs) 225 | 226 | 227 | class BaseModel(BaseBaseModel): 228 | """Business model 229 | """ 230 | 231 | @classmethod 232 | def create_model(cls, name, field=None): 233 | """dynamic create new model 234 | :args field table field, if field is None or {}, this model can not use create method 235 | """ 236 | if field: 237 | attrs = {'name': name, 'field': field} 238 | else: 239 | attrs = {'name': name, 'field': {'_id': ObjectId()}} 240 | 241 | return type(str(name), (cls, ), attrs)() 242 | -------------------------------------------------------------------------------- /turbo/mongo_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import ( 3 | absolute_import, 4 | division, 5 | print_function, 6 | with_statement, 7 | ) 8 | 9 | from collections import defaultdict 10 | from datetime import datetime 11 | import functools 12 | import time 13 | import sys 14 | 15 | from bson.objectid import ObjectId 16 | from turbo.log import model_log 17 | from turbo.util import escape as _es, import_object 18 | 19 | PY3 = sys.version_info >= (3,) 20 | 21 | if PY3: 22 | long = int 23 | 24 | 25 | def _record(x): 26 | return defaultdict(lambda: None, x) 27 | 28 | 29 | def convert_to_record(func): 30 | """Wrap mongodb record to a dict record with default value None 31 | """ 32 | @functools.wraps(func) 33 | def wrapper(self, *args, **kwargs): 34 | result = func(self, *args, **kwargs) 35 | if result is not None: 36 | if isinstance(result, dict): 37 | return _record(result) 38 | return (_record(i) for i in result) 39 | 40 | return result 41 | 42 | return wrapper 43 | 44 | 45 | class MixinModel(object): 46 | 47 | @staticmethod 48 | def utctimestamp(seconds=None): 49 | if seconds: 50 | return long(time.mktime(time.gmtime(seconds))) 51 | else: 52 | return long(time.mktime(time.gmtime())) 53 | 54 | @staticmethod 55 | def timestamp(): 56 | return long(time.time()) 57 | 58 | @staticmethod 59 | def datetime(dt=None): 60 | if dt: 61 | return datetime.strptime(dt, '%Y-%m-%d %H:%M') 62 | else: 63 | return datetime.now() 64 | 65 | @staticmethod 66 | def utcdatetime(dt=None): 67 | if dt: 68 | return datetime.strptime(dt, '%Y-%m-%d %H:%M') 69 | else: 70 | return datetime.utcnow() 71 | 72 | @classmethod 73 | def to_one_str(cls, value, *args, **kwargs): 74 | """Convert single record's values to str 75 | """ 76 | if kwargs.get('wrapper'): 77 | return cls._wrapper_to_one_str(value) 78 | 79 | return _es.to_dict_str(value) 80 | 81 | @classmethod 82 | def to_str(cls, values, callback=None): 83 | """Convert many records's values to str 84 | """ 85 | if callback and callable(callback): 86 | if isinstance(values, dict): 87 | return callback(_es.to_str(values)) 88 | 89 | return [callback(_es.to_str(i)) for i in values] 90 | return _es.to_str(values) 91 | 92 | @staticmethod 93 | @convert_to_record 94 | def _wrapper_to_one_str(value): 95 | return _es.to_dict_str(value) 96 | 97 | @staticmethod 98 | def default_encode(v): 99 | return _es.default_encode(v) 100 | 101 | @staticmethod 102 | def json_encode(v): 103 | return _es.json_encode(v) 104 | 105 | @staticmethod 106 | def json_decode(v): 107 | return _es.json_decode(v) 108 | 109 | @staticmethod 110 | def to_objectid(objid): 111 | return _es.to_objectid(objid) 112 | 113 | @staticmethod 114 | def create_objectid(): 115 | """Create new objectid 116 | """ 117 | return ObjectId() 118 | 119 | _instance = {} 120 | 121 | @classmethod 122 | def instance(cls, name): 123 | """Instantiate a model class according to import path 124 | args: 125 | name: class import path like `user.User` 126 | return: 127 | model instance 128 | """ 129 | if not cls._instance.get(name): 130 | model_name = name.split('.') 131 | ins_name = '.'.join( 132 | ['models', model_name[0], 'model', model_name[1]]) 133 | cls._instance[name] = cls.import_model(ins_name)() 134 | 135 | return cls._instance[name] 136 | 137 | @classmethod 138 | def import_model(cls, ins_name): 139 | """Import model class in models package 140 | """ 141 | try: 142 | package_space = getattr(cls, 'package_space') 143 | except AttributeError: 144 | raise ValueError('package_space not exist') 145 | else: 146 | return import_object(ins_name, package_space) 147 | 148 | @staticmethod 149 | def default_record(): 150 | """Generate one default record that return empty str when key not exist 151 | """ 152 | return defaultdict(lambda: '') 153 | 154 | 155 | def collection_method_call(turbo_connect_ins, name): 156 | def outwrapper(func): 157 | @functools.wraps(func) 158 | def wrapper(*args, **kwargs): 159 | if name in turbo_connect_ins._write_operators: 160 | turbo_connect_ins._model_ins.write_action_call( 161 | name, *args, **kwargs) 162 | 163 | if name in turbo_connect_ins._read_operators: 164 | turbo_connect_ins._model_ins.read_action_call( 165 | name, *args, **kwargs) 166 | 167 | return func(*args, **kwargs) 168 | 169 | return wrapper 170 | 171 | return outwrapper 172 | 173 | 174 | class MongoTurboConnect(object): 175 | 176 | _write_operators = frozenset([ 177 | 'insert', 178 | 'save', 179 | 'update', 180 | 'find_and_modify', 181 | 'bulk_write', 182 | 'insert_one', 183 | 'insert_many', 184 | 'replace_one', 185 | 'update_one', 186 | 'update_many', 187 | 'delete_one', 188 | 'delete_many', 189 | 'find_one_and_delete', 190 | 'find_one_and_replace', 191 | 'find_one_and_update', 192 | 'create_index', 193 | 'drop_index', 194 | 'create_indexes', 195 | 'drop_indexes', 196 | 'drop', 197 | 'remove', 198 | 'ensure_index', 199 | 'rename', 200 | ]) 201 | 202 | _read_operators = frozenset([ 203 | 'find', 204 | 'find_one', 205 | 'count', 206 | 'index_information', 207 | ]) 208 | 209 | def __init__(self, model_ins, db_collect=None): 210 | self._model_ins = model_ins 211 | self._collect = db_collect 212 | 213 | def __getattr__(self, name): 214 | collection_method = getattr(self._collect, name) 215 | if callable(collection_method): 216 | return collection_method_call(self, name)(collection_method) 217 | 218 | return collection_method 219 | 220 | def __getitem__(self, name): 221 | """Sub-collection 222 | """ 223 | return self._collect[name] 224 | 225 | 226 | class AbstractModel(MixinModel): 227 | """ 228 | name = None mongodb collection name 229 | field = None collection record map 230 | column = None need to query field 231 | index = [ 232 | tuple([('uid', 1)]) 233 | ] query index 234 | """ 235 | 236 | _operators = frozenset([ 237 | '$set', 238 | '$unset', 239 | '$rename', 240 | '$currentDate', 241 | '$inc', 242 | '$max', 243 | '$min', 244 | '$mul', 245 | '$setOnInsert', 246 | 247 | '$addToSet', 248 | '$pop', 249 | '$pushAll', 250 | '$push', 251 | '$pull']) 252 | 253 | PRIMARY_KEY_TYPE = ObjectId 254 | 255 | def _init(self, db_name, _mongo_db_mapping): 256 | if _mongo_db_mapping is None: 257 | raise Exception("db mapping is invalid") 258 | # databases 259 | db = _mongo_db_mapping['db'] 260 | # databases file 261 | db_file = _mongo_db_mapping['db_file'] 262 | 263 | # databse name 264 | if db_name not in db or db.get(db_name, None) is None: 265 | raise Exception('%s is invalid databse' % db_name) 266 | 267 | # collection name 268 | if not self.name: 269 | raise Exception('%s is invalid collection name' % self.name) 270 | 271 | # collection field 272 | if not self.field or not isinstance(self.field, dict): 273 | raise Exception('%s is invalid collection field' % self.field) 274 | 275 | # collect as private variable 276 | collect = getattr(db.get(db_name, object), self.name, None) 277 | if collect is None: 278 | raise Exception('%s is invalid collection' % self.name) 279 | 280 | # replace pymongo collect with custome connect 281 | _collect = MongoTurboConnect(self, collect) 282 | 283 | # gridfs as private variable 284 | _gridfs = db_file.get(db_name, None) 285 | if _gridfs is None: 286 | model_log.info('%s is invalid gridfs' % _gridfs) 287 | 288 | return _collect, _gridfs 289 | 290 | def _to_primary_key(self, _id): 291 | if self.PRIMARY_KEY_TYPE is ObjectId: 292 | return self.to_objectid(_id) 293 | 294 | return _id 295 | 296 | def __setitem__(self, k, v): 297 | setattr(self, k, v) 298 | 299 | def __getitem__(self, k): 300 | return getattr(self, k) 301 | 302 | def __str__(self): 303 | if isinstance(self.field, dict): 304 | return str(self.field) 305 | return None 306 | 307 | def sub_collection(self, name): 308 | raise NotImplementedError() 309 | 310 | def find_by_id(self, _id, column=None): 311 | raise NotImplementedError() 312 | 313 | def remove_by_id(self, _id): 314 | raise NotImplementedError() 315 | 316 | def find_new_one(self, *args, **kwargs): 317 | """return latest one record sort by _id 318 | """ 319 | raise NotImplementedError() 320 | 321 | def get_as_dict(self, condition=None, column=None, skip=0, limit=0, sort=None): 322 | raise NotImplementedError() 323 | 324 | def _valide_update_document(self, document): 325 | for opk in document.keys(): 326 | if not opk.startswith('$') or opk not in self._operators: 327 | raise ValueError('invalid document update operator') 328 | 329 | if not document: 330 | raise ValueError('empty document update not allowed') 331 | 332 | def _valid_record(self, record): 333 | if not isinstance(record, dict): 334 | raise Exception('%s record is not dict' % record) 335 | 336 | rset = set(record.keys()) 337 | fset = set(self.field.keys()) 338 | rset.discard('_id') 339 | fset.discard('_id') 340 | if not (fset ^ rset) <= fset: 341 | raise Exception('record keys is not equal to fields keys %s' % ( 342 | list((fset ^ rset) - fset))) 343 | 344 | for k, v in self.field.items(): 345 | if k not in record: 346 | if v[0] is datetime and not v[1]: 347 | record[k] = self.datetime() 348 | continue 349 | 350 | if v[0] is time and not v[1]: 351 | record[k] = self.timestamp() 352 | continue 353 | 354 | record[k] = v[1] 355 | 356 | return record 357 | 358 | def inc(self, spec_or_id, key, num=1): 359 | raise NotImplementedError() 360 | 361 | def put(self, value, **kwargs): 362 | """gridfs put method 363 | """ 364 | raise NotImplementedError() 365 | 366 | def delete(self, _id): 367 | """gridfs delete method 368 | """ 369 | raise NotImplementedError() 370 | 371 | def get(self, _id): 372 | """gridfs get method 373 | """ 374 | raise NotImplementedError() 375 | 376 | def read(self, _id): 377 | """gridfs read method 378 | """ 379 | raise NotImplementedError() 380 | 381 | def write_action_call(self, name, *args, **kwargs): 382 | """Execute when write action occurs, note: in this method write action must be called asynchronously 383 | """ 384 | pass 385 | 386 | def read_action_call(self, name, *args, **kwargs): 387 | """Execute when read action occurs, note: in this method read action must be called asynchronously 388 | """ 389 | pass 390 | -------------------------------------------------------------------------------- /turbo/register.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | import os 4 | 5 | from turbo.conf import app_config 6 | from turbo.util import get_base_dir, import_object 7 | 8 | 9 | def _install_app(package_space): 10 | for app in getattr(import_object('apps.settings', package_space), 'INSTALLED_APPS'): 11 | import_object('.'.join(['apps', app]), package_space) 12 | 13 | 14 | def register_app(app_name, app_setting, web_application_setting, mainfile, package_space): 15 | """insert current project root path into sys path 16 | """ 17 | from turbo import log 18 | app_config.app_name = app_name 19 | app_config.app_setting = app_setting 20 | app_config.project_name = os.path.basename(get_base_dir(mainfile, 2)) 21 | app_config.web_application_setting.update(web_application_setting) 22 | if app_setting.get('session_config'): 23 | app_config.session_config.update(app_setting['session_config']) 24 | log.getLogger(**app_setting.log) 25 | _install_app(package_space) 26 | 27 | 28 | def register_url(url, handler, name=None, kwargs=None): 29 | """insert url into tornado application handlers group 30 | 31 | :arg str url: url 32 | :handler object handler: url mapping handler 33 | :name reverse url name 34 | :kwargs dict tornado handler initlize args 35 | """ 36 | if name is None and kwargs is None: 37 | app_config.urls.append((url, handler)) 38 | return 39 | 40 | if name is None: 41 | app_config.urls.append((url, handler, kwargs)) 42 | return 43 | 44 | app_config.urls.append((url, handler, kwargs, name)) 45 | 46 | 47 | def register_group_urls(prefix, urls): 48 | for item in urls: 49 | url, handler = item[0:2] 50 | register_url(prefix + url, handler, *item[2:]) 51 | -------------------------------------------------------------------------------- /turbo/session.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | import os 4 | import time 5 | import base64 # noqa 6 | from copy import deepcopy 7 | try: 8 | import cPickle as pickle 9 | except ImportError: 10 | import pickle 11 | try: 12 | import hashlib 13 | sha1 = hashlib.sha1 14 | except ImportError: 15 | import sha 16 | sha1 = sha.new 17 | 18 | from tornado.util import ObjectDict 19 | 20 | from turbo.conf import app_config 21 | from turbo.log import session_log 22 | from turbo.util import utf8, to_basestring, encodebytes, decodebytes 23 | 24 | 25 | class Session(object): 26 | 27 | __slots__ = ['store', 'handler', 'app', '_data', '_dirty', '_config', 28 | '_initializer', 'session_id', '_session_object', '_session_name'] 29 | 30 | def __init__(self, app, handler, store, initializer, session_config=None, session_object=None): 31 | self.handler = handler 32 | self.app = app 33 | self.store = store or DiskStore(app_config.store_config.diskpath) 34 | 35 | self._config = deepcopy(app_config.session_config) 36 | if session_config: 37 | self._config.update(session_config) 38 | 39 | self._session_name = self._config.name 40 | 41 | self._session_object = (session_object or CookieObject)( 42 | app, handler, self.store, self._config) 43 | 44 | self._data = ObjectDict() 45 | self._initializer = initializer 46 | 47 | self.session_id = None 48 | 49 | self._dirty = False 50 | self._processor() 51 | 52 | def __getitem__(self, name): 53 | if name not in self._data: 54 | session_log.error('%s key not exist in %s session' % 55 | (name, self.session_id)) 56 | 57 | return self._data.get(name, None) 58 | 59 | def __setitem__(self, name, value): 60 | self._data[name] = value 61 | self._dirty = True 62 | 63 | def __delitem__(self, name): 64 | del self._data[name] 65 | self._dirty = True 66 | 67 | def __contains__(self, name): 68 | return name in self._data 69 | 70 | def __len__(self): 71 | return len(self._data) 72 | 73 | def __getattr__(self, name): 74 | return getattr(self._data, name) 75 | 76 | def __setattr__(self, name, value): 77 | if name in self.__slots__: 78 | super(Session, self).__setattr__(name, value) 79 | else: 80 | self._dirty = True 81 | setattr(self._data, name, value) 82 | 83 | def __delattr__(self, name): 84 | delattr(self._data, name) 85 | self._dirty = True 86 | 87 | def __iter__(self): 88 | for key in self._data: 89 | yield key 90 | 91 | def __repr__(self): 92 | return str(self._data) 93 | 94 | def _processor(self): 95 | """Application processor to setup session for every request""" 96 | self.store.cleanup(self._config.timeout) 97 | self._load() 98 | 99 | def _load(self): 100 | """Load the session from the store, by the id from cookie""" 101 | 102 | self.session_id = self._session_object.get_session_id() 103 | 104 | # protection against session_id tampering 105 | if self.session_id and not self._valid_session_id(self.session_id): 106 | self.session_id = None 107 | 108 | if self.session_id: 109 | d = self.store[self.session_id] 110 | if isinstance(d, dict) and d: 111 | self.update(d) 112 | 113 | if not self.session_id: 114 | self.session_id = self._session_object.generate_session_id() 115 | 116 | if not self._data: 117 | if self._initializer and isinstance(self._initializer, dict): 118 | self.update(deepcopy(self._initializer)) 119 | 120 | self._session_object.set_session_id(self.session_id) 121 | 122 | def save(self): 123 | if self._dirty: 124 | self.store[self.session_id] = self._data 125 | 126 | def _valid_session_id(self, session_id): 127 | return True 128 | 129 | def kill(self): 130 | """Kill the session, make it no longer available""" 131 | del self.store[self.session_id] 132 | 133 | def clear(self): 134 | del self.store[self.session_id] 135 | 136 | 137 | class SessionObject(object): 138 | 139 | def __init__(self, app, handler, store, session_config): 140 | self.app = app 141 | self.handler = handler 142 | self.store = store 143 | 144 | self._config = deepcopy(app_config.session_config) 145 | if session_config: 146 | self._config.update(session_config) 147 | 148 | self._session_name = self._config.name 149 | 150 | def get_session_id(self): 151 | raise NotImplementedError 152 | 153 | def set_session_id(self, session_id): 154 | raise NotImplementedError 155 | 156 | def clear_session_id(self): 157 | raise NotImplementedError 158 | 159 | def generate_session_id(self): 160 | """Generate a random id for session""" 161 | secret_key = self._config.secret_key 162 | while True: 163 | rand = os.urandom(16) 164 | now = time.time() 165 | session_id = sha1(utf8("%s%s%s%s" % ( 166 | rand, now, self.handler.request.remote_ip, secret_key))) 167 | session_id = session_id.hexdigest() 168 | if session_id not in self.store: 169 | break 170 | 171 | return session_id 172 | 173 | 174 | class CookieObject(SessionObject): 175 | 176 | def set_session_id(self, session_id): 177 | self._set_cookie(self._session_name, session_id) 178 | 179 | def get_session_id(self): 180 | return self._get_cookie(self._session_name) 181 | 182 | def clear_session_id(self): 183 | self.handler.clear_cookie(self._session_name) 184 | 185 | def _set_cookie(self, name, value): 186 | cookie_domain = self._config.cookie_domain 187 | cookie_path = self._config.cookie_path 188 | cookie_expires = self._config.cookie_expires 189 | if self._config.secure: 190 | return self.handler.set_secure_cookie( 191 | name, value, expires_days=cookie_expires / (3600 * 24), domain=cookie_domain, path=cookie_path) 192 | else: 193 | return self.handler.set_cookie(name, value, expires=cookie_expires, domain=cookie_domain, path=cookie_path) 194 | 195 | def _get_cookie(self, name): 196 | if self._config.secure: 197 | return self.handler.get_secure_cookie(name) 198 | else: 199 | return self.handler.get_cookie(name) 200 | 201 | 202 | class HeaderObject(SessionObject): 203 | 204 | def get_session_id(self): 205 | return self.handler.request.headers.get(self._session_name) 206 | 207 | def set_session_id(self, sid): 208 | self.handler.set_header(self._session_name, sid) 209 | 210 | def clear_session_id(self): 211 | self.handler.clear_header(self._session_name) 212 | 213 | 214 | class Store(object): 215 | 216 | """Base class for session stores""" 217 | 218 | def __contains__(self, key): 219 | raise NotImplementedError 220 | 221 | def __getitem__(self, key): 222 | raise NotImplementedError 223 | 224 | def __setitem__(self, key, value): 225 | raise NotImplementedError 226 | 227 | def cleanup(self, timeout): 228 | """removes all the expired sessions""" 229 | raise NotImplementedError 230 | 231 | def encode(self, session_data): 232 | """encodes session dict as a string""" 233 | pickled = pickle.dumps(session_data) 234 | return to_basestring(encodebytes(pickled)) 235 | 236 | def decode(self, session_data): 237 | """decodes the data to get back the session dict """ 238 | pickled = decodebytes(utf8(session_data)) 239 | return pickle.loads(pickled) 240 | 241 | 242 | class DiskStore(Store): 243 | """ 244 | Store for saving a session on disk. 245 | 246 | >>> import tempfile 247 | >>> root = tempfile.mkdtemp() 248 | >>> s = DiskStore(root) 249 | >>> s['a'] = 'foo' 250 | >>> s['a'] 251 | 'foo' 252 | >>> time.sleep(0.01) 253 | >>> s.cleanup(0.01) 254 | >>> s['a'] 255 | Traceback (most recent call last): 256 | ... 257 | KeyError: 'a' 258 | """ 259 | 260 | def __init__(self, root): 261 | # if the storage root doesn't exists, create it. 262 | if not os.path.exists(root): 263 | os.makedirs( 264 | os.path.abspath(root) 265 | ) 266 | self.root = root 267 | 268 | def _get_path(self, key): 269 | key = to_basestring(key) 270 | if os.path.sep in key: 271 | raise ValueError('Bad key: %s' % repr(key)) 272 | return os.path.join(self.root, key) 273 | 274 | def __contains__(self, key): 275 | path = self._get_path(key) 276 | return os.path.exists(path) 277 | 278 | def __getitem__(self, key): 279 | path = self._get_path(key) 280 | if os.path.exists(path): 281 | pickled = open(path).read() 282 | return self.decode(pickled) 283 | else: 284 | return ObjectDict() 285 | 286 | def __setitem__(self, key, value): 287 | path = self._get_path(key) 288 | pickled = self.encode(value) 289 | try: 290 | f = open(path, 'w') 291 | try: 292 | f.write(pickled) 293 | finally: 294 | f.close() 295 | except IOError: 296 | pass 297 | 298 | def __delitem__(self, key): 299 | path = self._get_path(key) 300 | if os.path.exists(path): 301 | os.remove(path) 302 | 303 | def cleanup(self, timeout): 304 | now = time.time() 305 | for f in os.listdir(self.root): 306 | path = self._get_path(f) 307 | atime = os.stat(path).st_atime 308 | if now - atime > timeout: 309 | os.remove(path) 310 | 311 | 312 | class RedisStore(Store): 313 | 314 | def __init__(self, **kwargs): 315 | self.timeout = kwargs.get( 316 | 'timeout') or app_config.session_config.timeout 317 | 318 | def __contains__(self, key): 319 | return self.get_connection(key).exists(key) 320 | 321 | def __setitem__(self, key, value): 322 | conn = self.get_connection(key) 323 | conn.hset(key, 'data', self.encode(value)) 324 | conn.expire(key, self.timeout) 325 | 326 | def __getitem__(self, key): 327 | data = self.get_connection(key).hget(key, 'data') 328 | if data: 329 | return self.decode(data) 330 | else: 331 | return ObjectDict() 332 | 333 | def __delitem__(self, key): 334 | self.get_connection(key).delete(key) 335 | 336 | def cleanup(self, timeout): 337 | pass 338 | 339 | def get_connection(self, key): 340 | import redis 341 | return redis.Redis() 342 | -------------------------------------------------------------------------------- /turbo/template.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from __future__ import absolute_import, print_function 3 | 4 | import re 5 | import functools 6 | 7 | from turbo.conf import app_config 8 | 9 | try: 10 | from jinja2 import Environment, FileSystemLoader 11 | except ImportError: 12 | raise ImportError('jinja2 module ImportError') 13 | 14 | 15 | class Jinja2Environment(Environment): 16 | """find template location 17 | 18 | according to current parent and template relative path to find template path 19 | 20 | args: 21 | template current template that needs to locate 22 | parent which call template with extends or include directive 23 | 24 | return: 25 | real template path 26 | 27 | example: 28 | input: 29 | template ../../base.html 30 | parent app/app/index.html 31 | output: 32 | base.html 33 | 34 | input: 35 | template header.html 36 | parent app/app/index.html 37 | output: 38 | app/app/header.html 39 | 40 | input: 41 | template ../header.html 42 | parent app/app/index.html 43 | output: 44 | app/header.html 45 | 46 | """ 47 | 48 | relative_path = re.compile('(./|../)', re.IGNORECASE) 49 | relative_dir = re.compile('([^/\s]{1,}/)', re.IGNORECASE) # noqa 50 | real_name = re.compile('([^/\s]{1,}$)') # noqa 51 | 52 | def join_path(self, template, parent): 53 | t_group = self.relative_path.findall(template) 54 | p_group = self.relative_dir.findall(parent) 55 | 56 | t_group_length = len(t_group) 57 | template_name = template 58 | # 59 | real_template_path = p_group 60 | if t_group_length: 61 | template_name = self.real_name.match( 62 | template, template.rfind('/') + 1).group() 63 | real_template_path = p_group[0:0 - t_group_length] 64 | 65 | real_template_path.append(template_name) 66 | return ''.join(real_template_path) 67 | 68 | 69 | def turbo_jinja2(func): 70 | _jinja2_env = Jinja2Environment(loader=FileSystemLoader(app_config.web_application_setting[ 71 | 'template_path']), auto_reload=app_config.web_application_setting['debug']) 72 | 73 | @functools.wraps(func) 74 | def wrapper(self, template_name, **kwargs): 75 | template = _jinja2_env.get_template( 76 | ('%s%s') % (self.template_path, template_name)) 77 | return template.render(handler=self, request=self.request, xsrf_form_html=self.xsrf_form_html(), 78 | context=self.get_context(), **kwargs) 79 | 80 | return wrapper 81 | -------------------------------------------------------------------------------- /turbo/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | import inspect 4 | import os 5 | import sys 6 | from datetime import datetime, date 7 | import time 8 | import json 9 | from collections import Iterable 10 | import copy 11 | import base64 # noqa: F401 12 | 13 | from bson.objectid import ObjectId 14 | 15 | from turbo.log import util_log 16 | 17 | PY3 = sys.version_info >= (3,) 18 | 19 | if PY3: 20 | unicode_type = str 21 | basestring_type = str 22 | file_types = 'file' 23 | from base64 import decodebytes, encodebytes 24 | else: 25 | # The names unicode and basestring don't exist in py3 so silence flake8. 26 | unicode_type = unicode # noqa 27 | basestring_type = basestring # noqa 28 | file_types = file # noqa 29 | from base64 import encodestring as encodebytes, decodestring as decodebytes # noqa 30 | 31 | 32 | def to_list_str(value, encode=None): 33 | """recursively convert list content into string 34 | 35 | :arg list value: The list that need to be converted. 36 | :arg function encode: Function used to encode object. 37 | """ 38 | result = [] 39 | for index, v in enumerate(value): 40 | if isinstance(v, dict): 41 | result.append(to_dict_str(v, encode)) 42 | continue 43 | 44 | if isinstance(v, list): 45 | result.append(to_list_str(v, encode)) 46 | continue 47 | 48 | if encode: 49 | result.append(encode(v)) 50 | else: 51 | result.append(default_encode(v)) 52 | 53 | return result 54 | 55 | 56 | def to_dict_str(origin_value, encode=None): 57 | """recursively convert dict content into string 58 | """ 59 | value = copy.deepcopy(origin_value) 60 | for k, v in value.items(): 61 | if isinstance(v, dict): 62 | value[k] = to_dict_str(v, encode) 63 | continue 64 | 65 | if isinstance(v, list): 66 | value[k] = to_list_str(v, encode) 67 | continue 68 | 69 | if encode: 70 | value[k] = encode(v) 71 | else: 72 | value[k] = default_encode(v) 73 | 74 | return value 75 | 76 | 77 | def default_encode(v): 78 | """convert ObjectId, datetime, date into string 79 | """ 80 | if isinstance(v, ObjectId): 81 | return unicode_type(v) 82 | 83 | if isinstance(v, datetime): 84 | return format_time(v) 85 | 86 | if isinstance(v, date): 87 | return format_time(v) 88 | 89 | return v 90 | 91 | 92 | def to_str(v, encode=None): 93 | """convert any list, dict, iterable and primitives object to string 94 | """ 95 | if isinstance(v, basestring_type): 96 | return v 97 | 98 | if isinstance(v, dict): 99 | return to_dict_str(v, encode) 100 | 101 | if isinstance(v, Iterable): 102 | return to_list_str(v, encode) 103 | 104 | if encode: 105 | return encode(v) 106 | else: 107 | return default_encode(v) 108 | 109 | 110 | def format_time(dt): 111 | """datetime format 112 | """ 113 | return time.mktime(dt.timetuple()) 114 | 115 | 116 | def to_objectid(objid): 117 | """字符对象转换成objectid 118 | """ 119 | if objid is None: 120 | return objid 121 | 122 | try: 123 | objid = ObjectId(objid) 124 | except: # noqa 125 | util_log.error('%s is invalid objectid' % objid) 126 | return None 127 | 128 | return objid 129 | 130 | 131 | def json_encode(data, **kwargs): 132 | try: 133 | return json.dumps(data, **kwargs) 134 | except: # noqa 135 | util_log.error('Uncaught exception in json_encode', exc_info=True) 136 | 137 | 138 | def json_decode(data, **kwargs): 139 | try: 140 | return json.loads(data, **kwargs) 141 | except: # noqa 142 | util_log.error('Uncaught exception in json_decode', exc_info=True) 143 | 144 | 145 | def to_int(value, default=None): 146 | try: 147 | return int(value) 148 | except ValueError as e: 149 | util_log.error(e) 150 | 151 | 152 | def to_float(value, default=None): 153 | try: 154 | return float(value) 155 | except ValueError as e: 156 | util_log.error(e) 157 | 158 | 159 | def to_datetime(t, micro=False): 160 | if micro: 161 | return datetime.fromtimestamp(t / 1000) 162 | else: 163 | return datetime.fromtimestamp(t) 164 | 165 | 166 | def to_time(t, micro=False): 167 | if micro: 168 | return time.mktime(t.timetuple()) * 1000 169 | else: 170 | return time.mktime(t.timetuple()) 171 | 172 | 173 | class Escape(object): 174 | 175 | __slots__ = ['to_list_str', 'to_dict_str', 'default_encode', 'format_time', 'to_objectid', 176 | 'to_str', 'to_time', 'to_datetime', 'to_int', 'to_float', 'json_decode', 'json_encode', '__gl'] 177 | 178 | def __init__(self, module): 179 | self.__gl = module 180 | 181 | def __getattr__(self, name): 182 | if name in self.__slots__: 183 | return self.__gl.get(name) 184 | 185 | raise AttributeError('escape has no attribute %s' % name) 186 | 187 | 188 | escape = Escape(globals()) 189 | 190 | 191 | def get_base_dir(currfile, dir_level_num=3): 192 | """ 193 | find certain path according to currfile 194 | """ 195 | root_path = os.path.abspath(currfile) 196 | for i in range(0, dir_level_num): 197 | root_path = os.path.dirname(root_path) 198 | 199 | return root_path 200 | 201 | 202 | def join_sys_path(currfile, dir_level_num=3): 203 | """ 204 | find certain path then load into sys path 205 | """ 206 | if os.path.isdir(currfile): 207 | root_path = currfile 208 | else: 209 | root_path = get_base_dir(currfile, dir_level_num) 210 | 211 | sys.path.append(root_path) 212 | 213 | 214 | def import_object(name, package_space=None): 215 | if name.count('.') == 0: 216 | return __import__(name, package_space, None) 217 | 218 | parts = name.split('.') 219 | obj = __import__('.'.join(parts[:-1]), 220 | package_space, None, [str(parts[-1])], 0) 221 | try: 222 | return getattr(obj, parts[-1]) 223 | except AttributeError: 224 | raise ImportError("No module named %s" % parts[-1]) 225 | 226 | 227 | def camel_to_underscore(name): 228 | """ 229 | convert CamelCase style to under_score_case 230 | """ 231 | as_list = [] 232 | length = len(name) 233 | for index, i in enumerate(name): 234 | if index != 0 and index != length - 1 and i.isupper(): 235 | as_list.append('_%s' % i.lower()) 236 | else: 237 | as_list.append(i.lower()) 238 | 239 | return ''.join(as_list) 240 | 241 | 242 | def remove_folder(path, foldername): 243 | if not foldername: 244 | return 245 | 246 | if not os.path.isdir(path): 247 | return 248 | 249 | dir_content = os.listdir(path) 250 | if not dir_content: 251 | return 252 | 253 | for item in dir_content: 254 | child_path = os.path.join(path, item) 255 | 256 | if not os.path.isdir(child_path): 257 | continue 258 | 259 | if item != foldername: 260 | remove_folder(child_path, foldername) 261 | continue 262 | 263 | # os.rmdir can't be allowed to deldte not empty 264 | for root, dirs, files in os.walk(child_path, topdown=False): 265 | for name in files: 266 | os.remove(os.path.join(root, name)) 267 | 268 | for name in dirs: 269 | os.rmdir(os.path.join(root, name)) 270 | try: 271 | os.rmdir(child_path) 272 | except Exception as e: 273 | raise e 274 | 275 | 276 | def remove_file(path, filename): 277 | if not filename: 278 | return 279 | 280 | if not os.path.isdir(path): 281 | return 282 | 283 | dir_content = os.listdir(path) 284 | if not dir_content: 285 | return 286 | 287 | for item in dir_content: 288 | child_path = os.path.join(path, item) 289 | 290 | if os.path.isdir(child_path): 291 | remove_file(child_path, filename) 292 | continue 293 | 294 | if item != filename: 295 | continue 296 | 297 | try: 298 | os.remove(child_path) 299 | except Exception as e: 300 | raise e 301 | 302 | 303 | def remove_extension(path, extension): 304 | if not extension: 305 | return 306 | 307 | if not os.path.isdir(path): 308 | return 309 | 310 | dir_content = os.listdir(path) 311 | if not dir_content: 312 | return 313 | 314 | for item in dir_content: 315 | child_path = os.path.join(path, item) 316 | 317 | if os.path.isdir(child_path): 318 | remove_extension(child_path, extension) 319 | continue 320 | 321 | name, ext = os.path.splitext(item) 322 | 323 | if ext != extension: 324 | continue 325 | 326 | try: 327 | os.remove(child_path) 328 | except Exception as e: 329 | raise e 330 | 331 | 332 | def build_index(model_list): 333 | from turbo.model import BaseModel 334 | for m in model_list: 335 | for attr_name in dir(m): 336 | attr = getattr(m, attr_name) 337 | if inspect.isclass(attr) and issubclass(attr, BaseModel) and hasattr(attr, 'name'): 338 | if hasattr(attr, 'index'): 339 | for index in attr.index: 340 | attr().create_index(index, background=True) 341 | else: 342 | print("model %s has no 'index' attribute" % attr.__name__) 343 | 344 | 345 | _UTF8_TYPES = (bytes, type(None)) 346 | 347 | 348 | def utf8(value): 349 | # type: (typing.Union[bytes,unicode_type,None])->typing.Union[bytes,None] # noqa 350 | """Converts a string argument to a byte string. 351 | 352 | If the argument is already a byte string or None, it is returned unchanged. 353 | Otherwise it must be a unicode string and is encoded as utf8. 354 | """ 355 | if isinstance(value, _UTF8_TYPES): 356 | return value 357 | if not isinstance(value, unicode_type): 358 | raise TypeError( 359 | "Expected bytes, unicode, or None; got %r" % type(value) 360 | ) 361 | return value.encode("utf-8") 362 | 363 | 364 | _BASESTRING_TYPES = (basestring_type, type(None)) 365 | 366 | 367 | def to_basestring(value): 368 | """Converts a string argument to a subclass of basestring. 369 | 370 | In python2, byte and unicode strings are mostly interchangeable, 371 | so functions that deal with a user-supplied argument in combination 372 | with ascii string constants can use either and should return the type 373 | the user supplied. In python3, the two types are not interchangeable, 374 | so this method is needed to convert byte strings to unicode. 375 | """ 376 | if isinstance(value, _BASESTRING_TYPES): 377 | return value 378 | if not isinstance(value, bytes): 379 | raise TypeError( 380 | "Expected bytes, unicode, or None; got %r" % type(value) 381 | ) 382 | return value.decode("utf-8") 383 | 384 | 385 | def get_func_name(func): 386 | name = getattr(func, 'func_name', None) 387 | if not name: 388 | name = getattr(func, '__name__', None) 389 | return name 390 | -------------------------------------------------------------------------------- /zh-CN_README.md: -------------------------------------------------------------------------------- 1 | turbo 2 | ========= 3 | 4 | [![Build Status](https://travis-ci.org/wecatch/app-turbo.svg?branch=master)](https://travis-ci.org/wecatch/app-turbo) 5 | 6 | 7 | turbo 是一个用以加速建立普通 web 站点和 RESTFul api 的 framework,基于 tornado。 8 | 9 | 10 | ## 特性 11 | 12 | - 方便扩展,易于维护 13 | - 快速开发 web 站点 和 RESTFul api 14 | - 类似 django 或 flask 的 app 组织结构 15 | - 支持轻松定制特性 16 | - 简单的 ORM,易于维护和扩展 17 | - 灵活的 Logger 18 | - Session (提供了对应的钩子函数,可以使用任何 storage, 自带 redis store 实现) 19 | - 支持 MongoDB,MySQL,PostgreSQL 20 | - 支持 MongoDB 异步驱动 [Motor](http://motor.readthedocs.io/en/stable/) 21 | 22 | ## 快速开始 23 | 24 | ``` 25 | pip install turbo 26 | turbo-admin startproject 27 | cd /app-server 28 | touch __test__ 29 | python main.py 30 | ``` 31 | 32 | ## 文档 33 | 34 | 35 | [http://app-turbo.readthedocs.org/](http://app-turbo.readthedocs.org/) 36 | 37 | ## wiki 38 | 39 | - [我为什么创造了 turbo 这个后端的轮子](http://sanyuesha.com/2016/07/23/why-did-i-make-turbo/) 40 | --------------------------------------------------------------------------------