From f6b5160db04194db9b8b454511cc637b04b42fc0 Mon Sep 17 00:00:00 2001 From: NyanKiyoshi <6186720+NyanKiyoshi@users.noreply.github.com> Date: Sun, 16 Sep 2018 21:16:10 +0200 Subject: [PATCH] Add a UWSGI app and a flask app --- .ci/test_servers.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 7 ++++-- Pipfile | 1 + Pipfile.lock | 32 ++++++++++++++++++++++- README.md | 7 ++++++ requirements-dev.txt | 4 +++ setup.cfg | 3 +++ setup.py | 7 ++++-- texrrow/__main__.py | 25 ++++++++++++++++-- texrrow/wsgi.py | 13 ++++++++++ 10 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 .ci/test_servers.py create mode 100644 texrrow/wsgi.py diff --git a/.ci/test_servers.py b/.ci/test_servers.py new file mode 100644 index 0000000..84081b3 --- /dev/null +++ b/.ci/test_servers.py @@ -0,0 +1,60 @@ +"""We want to ensure that users can run Texrrow through UWSGI without issues. +This script is ran by Travis and AppVeyor to ensure the compatibility. + +This is a heavy test and incompatible test with pytest. +Thus we keep it separated from the rest of the tests.""" +import os +import signal +import sys +import time + +import mock +import requests +import requests.exceptions + +from texrrow import __main__ as main + +TEST_PORT = 12345 + + +def _knock_url_and_wait(url, timeout_per_request=1, global_timeout=10): + status = None + end_time = time.time() + global_timeout + while status is None: + try: + response = requests.head(url, timeout=timeout_per_request) + status = response.status_code + except requests.exceptions.ConnectionError: # noqa + if time.time() > end_time: + raise + assert status == 200 + + +def _run_and_test(to_run): + child_pid = os.fork() + if child_pid == 0: + to_run() + sys.exit(0) + try: + test_url = 'http://127.0.0.1:%d' % TEST_PORT + time.sleep(0.1) + pid, status = os.waitpid(child_pid, os.P_NOWAIT) + + # ensure the process did not exit + assert not os.WEXITSTATUS(pid) + + # try to ping uwsgi and get a HTTP 200 + _knock_url_and_wait(test_url) + finally: + os.kill(child_pid, signal.SIGTERM) + os.wait() + + +@mock.patch.object(main, 'PORT', new=TEST_PORT) +def test_start_servers(): + _run_and_test(main.start_uwsgi) + _run_and_test(main.start_flask) + + +if __name__ == '__main__': + test_start_servers() diff --git a/.travis.yml b/.travis.yml index 8aabdca..84d92f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,11 @@ python: - "pypy" - "pypy3" install: - - pip install codecov pytest-cov + - pip install codecov pytest-cov uwsgi - nvm install 8 - npm i - npm run build-assets --production + - python setup.py develop matrix: include: - python: "3.7" @@ -26,10 +27,12 @@ matrix: dist: xenial allow_failures: - python: "nightly" + - python: "pypy" + - python: "pypy3" fast_finish: true script: - - pip install -r requirements.txt - pip install -r requirements-dev.txt - pytest --cov --cov-report= + - python .ci/test_servers.py after_success: - codecov diff --git a/Pipfile b/Pipfile index 7c96586..408fffe 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,7 @@ coverage = "*" blinker = "*" flask-debugtoolbar = "*" mock = "*" +requests = "*" [requires] python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock index 727ebef..782c81f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9bb8b183e1176a4efefa7d3b26d9890f27ec0de9f1aa49b685c48653e055b686" + "sha256": "61e849445da8704ec053a9f279dca3673b93fd841127db303ca4c2ae87e38870" }, "pipfile-spec": 6, "requires": { @@ -160,6 +160,13 @@ ], "version": "==2018.8.24" }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, "click": { "hashes": [ "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", @@ -216,6 +223,13 @@ "index": "pypi", "version": "==0.10.1" }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, "itsdangerous": { "hashes": [ "sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519" @@ -298,6 +312,14 @@ "index": "pypi", "version": "==3.8.0" }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "index": "pypi", + "version": "==2.19.1" + }, "six": { "hashes": [ "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", @@ -306,6 +328,14 @@ "index": "pypi", "version": "==1.11.0" }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version < '4' and python_version >= '2.6' and python_version != '3.3.*' and python_version != '3.2.*'", + "version": "==1.23" + }, "virtualenv": { "hashes": [ "sha256:2ce32cd126117ce2c539f0134eb89de91a8413a29baac49cbab3eb50e2026669", diff --git a/README.md b/README.md index fb33438..881dcf3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,13 @@ A simple python web-server to remotely control a LaTeX presentation from a mobile phone to give dynamic and powerful speeches. +## Usage +``` +texrrow [PORT=5000] +``` +Run `texrrow` in your shell and open your browser in [http://127.0.0.1:5000](http://127.0.0.1:5000)! + + ## Development Usage ### Install Requirements ```shell diff --git a/requirements-dev.txt b/requirements-dev.txt index 9f81180..6d4c1bd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,10 +14,12 @@ atomicwrites==1.2.1 attrs==18.2.0 blinker==1.4 certifi==2018.8.24 +chardet==3.0.4 click==6.7 coverage==5.0a2 flask-debugtoolbar==0.10.1 flask==1.0.2 +idna==2.7 itsdangerous==0.24 jinja2==2.10 markupsafe==1.0 @@ -29,7 +31,9 @@ pipenv==2018.7.1 pluggy==0.7.1 py==1.6.0 pytest==3.8.0 +requests==2.19.1 six==1.11.0 +urllib3==1.23 virtualenv-clone==0.3.0 virtualenv==16.0.0 werkzeug==0.14.1 diff --git a/setup.cfg b/setup.cfg index 8cf1700..bc7d74f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,9 @@ omit = */test_*.py tests/*.py */commands/populatedb.py + */__main__.py + .ci/* + */wsgi* source = texrrow diff --git a/setup.py b/setup.py index ded2287..d7f8ad9 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ requirements = [] if isfile('requirements.txt'): with open('requirements.txt') as fp: - requirements = fp.readlines() + requirements = [line.strip() for line in fp.readlines()] setup( @@ -31,13 +31,16 @@ maintainer='NyanKiyoshi', entry_points={ 'console_scripts': [ - 'texrrow=texrrow.__main__:main']}, + 'texrrow-uwsgi=texrrow.__main__:start_uwsgi', + 'texrrow=texrrow.__main__:start_flask']}, packages=find_packages(exclude=['tests']), include_package_data=True, data_files=[ ('', ['README.md', 'LICENSE', 'requirements.txt'])], keywords=[], install_requires=requirements, + extras_require={ + 'uwsgi': ['uwsgi']}, classifiers=[ 'Development Status :: 1 - Planning', 'License :: OSI Approved :: MIT License', diff --git a/texrrow/__main__.py b/texrrow/__main__.py index b2e1285..b47bc4d 100644 --- a/texrrow/__main__.py +++ b/texrrow/__main__.py @@ -1,3 +1,24 @@ -def main(): +import os +import os.path +import sys + +PORT = 5000 +UWSGI_PATH = os.path.realpath(os.path.join(sys.executable, os.pardir, 'uwsgi')) + + +def _get_port_from_argv(): + if len(sys.argv) > 1: + port = int(sys.argv[1]) + return port + return PORT + + +def start_flask(): from .application import create_app - create_app().run() + create_app().run(host='0.0.0.0', port=_get_port_from_argv(), debug=False) + + +def start_uwsgi(): + port = ':%d' % _get_port_from_argv() + argv = ['uwsgi', '--http', port, '--module', 'texrrow.wsgi:application'] + os.execve(UWSGI_PATH, argv, os.environ) diff --git a/texrrow/wsgi.py b/texrrow/wsgi.py new file mode 100644 index 0000000..b98a4d3 --- /dev/null +++ b/texrrow/wsgi.py @@ -0,0 +1,13 @@ +from texrrow.application import create_app + + +def health_check(_application, health_url): + def health_check_wrapper(environ, start_response): + if environ.get('PATH_INFO') == health_url: + start_response('200 OK', [('Content-Type', 'text/plain')]) + return [] + return _application(environ, start_response) + return health_check_wrapper + + +application = health_check(create_app(), '/health/')