├── .circleci └── config.yml ├── .coveragerc ├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── appveyor.yml ├── bin └── luxmake.py ├── ci └── install.sh ├── docs ├── application.md ├── artwork │ ├── lux-banner-blue-yellow.svg │ ├── lux-banner-blue.svg │ ├── lux-banner-yellow-blue.svg │ ├── lux-banner-yellow.svg │ └── lux.sketch ├── authentication.md ├── changelog.md ├── extensions.md ├── getting-started.rst ├── history │ ├── 0.1.md │ ├── 0.2.md │ ├── 0.3.md │ ├── 0.4.md │ ├── 0.5.md │ ├── 0.6.md │ ├── 0.7.md │ └── 0.8.md ├── layout.rst ├── migrations.rst ├── providers.md ├── readme.md ├── rest.md └── testing.md ├── example ├── .babelrc ├── README.md ├── __init__.py ├── cfg.py ├── content │ ├── articles │ │ ├── nofollow.md │ │ ├── nolink.md │ │ ├── security.md │ │ └── test.md │ ├── auth │ │ └── login.md │ ├── config.json │ ├── context │ │ ├── follow.html │ │ ├── footer.md │ │ ├── footer1.md │ │ ├── footer2.md │ │ └── footer3.md │ ├── site │ │ ├── 404.md │ │ ├── images.md │ │ └── index.md │ └── templates │ │ ├── doc.html │ │ ├── home.html │ │ ├── main.html │ │ ├── partials │ │ └── landing.html │ │ └── test-multi │ │ ├── bla.html │ │ └── foo.html ├── media │ ├── lux.png │ ├── website.js │ └── website.min.js ├── scss │ ├── _variables.scss │ ├── components │ │ └── _helpers.scss │ └── luxsite.scss ├── webalone │ ├── __init__.py │ └── config.py ├── webapi │ ├── __init__.py │ └── config.py └── website │ ├── __init__.py │ └── config.py ├── lux ├── __init__.py ├── core │ ├── __init__.py │ ├── app.py │ ├── auth.py │ ├── cache.py │ ├── channels.py │ ├── client.py │ ├── cms.py │ ├── commands │ │ ├── __init__.py │ │ ├── clear_cache.py │ │ ├── create_uuid.py │ │ ├── flush_models.py │ │ ├── generate_secret_key.py │ │ ├── project_template │ │ │ ├── js │ │ │ │ ├── close.js │ │ │ │ └── open.js │ │ │ ├── manage.py │ │ │ └── project_name │ │ │ │ ├── __init__.py │ │ │ │ ├── config.py │ │ │ │ └── settings.py │ │ ├── serve.py │ │ ├── show_parameters.py │ │ ├── start_project.py │ │ └── stop.py │ ├── console.py │ ├── exceptions.py │ ├── extension.py │ ├── green.py │ ├── mail.py │ ├── routers.py │ ├── templates.py │ ├── user.py │ └── wrappers.py ├── ext │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── backend.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── create_superuser.py │ │ │ └── create_token.py │ │ ├── models.py │ │ ├── permissions.py │ │ ├── rest │ │ │ ├── __init__.py │ │ │ ├── authorization.py │ │ │ ├── groups.py │ │ │ ├── mailinglist.py │ │ │ ├── passwords.py │ │ │ ├── permissions.py │ │ │ ├── registrations.py │ │ │ ├── tokens.py │ │ │ ├── user.py │ │ │ └── users.py │ │ └── templates │ │ │ └── registration │ │ │ ├── activation_email.txt │ │ │ ├── activation_email_subject.txt │ │ │ ├── activation_message.txt │ │ │ ├── confirmation.html │ │ │ ├── inactive_user.txt │ │ │ ├── password_email.txt │ │ │ ├── password_email_subject.txt │ │ │ └── reset-password.html │ ├── base │ │ └── __init__.py │ ├── content │ │ ├── __init__.py │ │ ├── cms.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── static.py │ │ ├── contents.py │ │ ├── files.py │ │ ├── github.py │ │ ├── models.py │ │ ├── rest.py │ │ ├── urlwrappers.py │ │ └── views.py │ ├── oauth │ │ ├── __init__.py │ │ ├── amazon │ │ │ └── __init__.py │ │ ├── dropbox │ │ │ └── __init__.py │ │ ├── facebook │ │ │ └── __init__.py │ │ ├── github │ │ │ └── __init__.py │ │ ├── google │ │ │ └── __init__.py │ │ ├── linkedin │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── oauth.py │ │ ├── ogp.py │ │ ├── twitter │ │ │ └── __init__.py │ │ └── views.py │ ├── odm │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── sql.py │ │ │ └── template │ │ │ │ └── lux │ │ │ │ ├── alembic.ini.mako │ │ │ │ ├── env.py │ │ │ │ └── script.py.mako │ │ ├── convert.py │ │ ├── fields.py │ │ ├── mapper.py │ │ ├── migrations.py │ │ └── models.py │ ├── readme.md │ ├── rest │ │ ├── __init__.py │ │ ├── apis.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── apispec.py │ │ ├── cors.py │ │ ├── exc.py │ │ ├── openapi.py │ │ ├── pagination.py │ │ ├── rest.py │ │ └── route.py │ ├── sessions │ │ ├── __init__.py │ │ ├── browser.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── flush_sessions.py │ │ ├── error.py │ │ ├── store.py │ │ └── views.py │ ├── sitemap │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── ping_google.py │ ├── smtp │ │ ├── __init__.py │ │ ├── backend.py │ │ ├── log.py │ │ ├── message.py │ │ └── views.py │ ├── sockjs │ │ ├── __init__.py │ │ ├── rpc │ │ │ ├── __init__.py │ │ │ ├── auth.py │ │ │ ├── channels.py │ │ │ └── model.py │ │ ├── socketio.py │ │ ├── transports │ │ │ ├── __init__.py │ │ │ └── websocket.py │ │ ├── utils.py │ │ └── ws.py │ └── sql │ │ └── __init__.py ├── models │ ├── __init__.py │ ├── component.py │ ├── fields.py │ ├── memory.py │ ├── model.py │ ├── query.py │ ├── schema.py │ └── unique.py ├── openapi │ ├── __init__.py │ ├── core.py │ ├── ext │ │ ├── __init__.py │ │ ├── lux.py │ │ └── marshmallow.py │ └── utils.py └── utils │ ├── __init__.py │ ├── async.py │ ├── auth.py │ ├── context.py │ ├── countries.py │ ├── crypt │ ├── __init__.py │ ├── arc4.py │ └── pbkdf2.py │ ├── data.py │ ├── date.py │ ├── files.py │ ├── messages.py │ ├── py.py │ ├── sphinxtogithub.py │ ├── test.py │ ├── text.py │ ├── token.py │ ├── url.py │ └── version.py ├── manage.py ├── package.json ├── requirements ├── dev.txt ├── hard.txt └── soft.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── auth ├── __init__.py ├── authentications.py ├── commands.py ├── errors.py ├── fixtures │ └── permissions.json ├── groups.py ├── mail_list.py ├── password.py ├── permissions.py ├── registration.py ├── test_backend.py ├── test_permissions.py ├── test_postgresql.py ├── test_sqlite.py ├── user.py └── utils.py ├── base ├── __init__.py └── test_media.py ├── config.py ├── content ├── __init__.py ├── test_app.py └── test_static.py ├── core ├── __init__.py ├── media │ └── core │ │ └── placeholder.txt ├── test_app.py ├── test_cache.py ├── test_channels.py ├── test_commands.py ├── test_memory_model.py └── test_wrappers.py ├── crypt ├── __init__.py └── test_pbkdf2.py ├── db.sql ├── dbclean.sql ├── js ├── mock.js └── placeholder.txt ├── mail ├── __init__.py ├── templates │ └── enquiry-response.txt ├── test_contact.py └── test_smtp.py ├── oauth ├── __init__.py ├── github.py └── test_app.py ├── odm ├── __init__.py ├── fixtures │ └── contents.json ├── test_commands.py ├── test_filters.py ├── test_migrations.py ├── test_models.py ├── test_postgresql.py ├── test_sqlite.py └── utils.py ├── rest ├── __init__.py ├── test_openapi.py └── test_pagination.py ├── scss └── placeholder.scss ├── sessions ├── __init__.py ├── test_postgresql.py └── test_sqlite.py ├── sockjs ├── __init__.py ├── fixtures │ └── permissions.json └── test_rest.py ├── templates └── home.html └── web ├── __init__.py ├── fixtures └── test.json ├── test_alone.py ├── test_api.py ├── test_auth.py ├── test_signup.py └── test_text.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | main: 4 | working_directory: ~/main 5 | docker: 6 | - image: python:3.6.3 7 | - image: redis 8 | - image: postgres:9.5 9 | steps: 10 | - checkout 11 | - run: 12 | name: install packages 13 | command: ci/install.sh 14 | - run: 15 | name: test 16 | command: make test 17 | coverage: 18 | working_directory: ~/coverage 19 | docker: 20 | - image: python:3.6.3 21 | - image: postgres:9.5 22 | - image: redis 23 | steps: 24 | - checkout 25 | - run: 26 | name: install packages 27 | command: ci/install.sh 28 | - run: 29 | name: run tests for coverage 30 | command: make coverage 31 | - run: 32 | name: upload coverage stats 33 | command: codecov 34 | legacy: 35 | working_directory: ~/legacy 36 | docker: 37 | - image: python:3.5.4 38 | - image: postgres:9.5 39 | - image: redis 40 | steps: 41 | - checkout 42 | - run: 43 | name: install packages 44 | command: ci/install.sh 45 | - run: 46 | name: test 47 | command: make test 48 | deploy-release: 49 | working_directory: ~/deploy 50 | docker: 51 | - image: python:3.6.3 52 | steps: 53 | - checkout 54 | - run: 55 | name: install packages 56 | command: ci/install.sh 57 | - run: 58 | name: check version 59 | command: python setup.py pypi --final 60 | - run: 61 | name: create source distribution 62 | command: python setup.py sdist 63 | - run: 64 | name: release source distribution 65 | command: twine upload dist/* --username lsbardel --password $PYPI_PASSWORD 66 | - run: 67 | name: tag 68 | command: ci/tag.sh 69 | 70 | workflows: 71 | version: 2 72 | build-deploy: 73 | jobs: 74 | - main: 75 | filters: 76 | branches: 77 | ignore: release 78 | tags: 79 | ignore: /.*/ 80 | - coverage: 81 | filters: 82 | branches: 83 | ignore: release 84 | tags: 85 | ignore: /.*/ 86 | - legacy: 87 | filters: 88 | branches: 89 | ignore: release 90 | tags: 91 | ignore: /.*/ 92 | - deploy-release: 93 | filters: 94 | branches: 95 | only: release 96 | tags: 97 | ignore: /.*/ 98 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | concurrency = greenlet 3 | source = lux 4 | omit = 5 | lux/__init__.py 6 | lux/utils/sphinxtogithub.py 7 | lux/utils/version.py 8 | lux/utils/crypt/arc4.py 9 | 10 | [report] 11 | # Regexes for lines to exclude from consideration 12 | exclude_lines = 13 | (?i)# *pragma[: ]*no *cover 14 | raise NotImplementedError 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .project 3 | .pydevproject 4 | .settings 5 | .idea 6 | 7 | # python 8 | *.pyc 9 | .coverage 10 | .eggs 11 | .venv 12 | _doc_build 13 | __pycache__ 14 | htmlcov 15 | htmlprof 16 | dist 17 | /build 18 | _build 19 | src 20 | deps 21 | venv 22 | release-notes.md 23 | *.egg-info 24 | 25 | # javascript/css 26 | coverage 27 | junit 28 | .grunt 29 | *.map 30 | lux/media/js/templates 31 | example/js/lux 32 | example/website/media/website/*.js 33 | example/website/media/website/*.css 34 | example/website/media/website/*.map 35 | example/*/migrations 36 | 37 | *.log 38 | npm* 39 | node_modules 40 | MANIFEST 41 | .DS_Store 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: required 4 | dist: trusty 5 | 6 | python: 7 | - 3.5.2 8 | # - 3.6-dev 9 | 10 | services: 11 | - redis-server 12 | 13 | install: 14 | - pip install -U -r requirements-dev.txt 15 | 16 | 17 | addons: 18 | postgresql: "9.5" 19 | 20 | 21 | before_script: 22 | - psql -U postgres -f tests/db.sql 23 | 24 | script: 25 | - python setup.py test --coverage -q 26 | - flake8 27 | - if [[ $TRAVIS_PYTHON_VERSION == '3.5.2' ]]; then python setup.py test --coveralls; fi 28 | 29 | notifications: 30 | email: false 31 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | List of Contributors to Lux 2 | 3 | * [Luca Sbardella](https://github.com/lsbardel) 4 | * [Reupen Shah](https://github.com/reupen) 5 | * [Jeremy Herr](https://github.com/jeremyherr) 6 | * [Tom Hardman](https://github.com/tomhardman0) 7 | * [Mariusz Winnik](https://github.com/tazo90) 8 | * [Dariusz Zawadzki](https://github.com/SirZazu) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017 Quantmind 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | * Neither the name of the author nor the names of its contributors 12 | may be used to endorse or promote products derived from this software without 13 | specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 19 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 20 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 22 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 23 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 24 | OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include README.rst 3 | include Makefile 4 | graft requirements 5 | graft tests 6 | graft lux 7 | graft bin 8 | graft example 9 | global-exclude *.pyc 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PYTHON ?= python 3 | PIP ?= pip 4 | 5 | .PHONY: help 6 | 7 | help: 8 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' 9 | 10 | 11 | clean: ## cleanup 12 | rm -fr dist/ *.egg-info *.eggs .eggs build/ 13 | find . -name '__pycache__' | xargs rm -rf 14 | 15 | test: ## run unittests 16 | flake8 17 | $(PYTHON) -W ignore setup.py test -q --sequential 18 | 19 | coverage: ## run unittests with coverage 20 | $(PYTHON) -W ignore setup.py test --coverage -q 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://lux.fluidily.com/assets/logos/lux-banner-blue-yellow.svg 2 | :alt: Lux 3 | :width: 50% 4 | 5 | | 6 | | 7 | 8 | 9 | :Badges: |license| |pyversions| |status| |pypiversion| 10 | :CI: |circleci| |coverage| 11 | :Documentation: https://github.com/quantmind/lux/tree/master/docs/readme.md 12 | :Downloads: https://pypi.python.org/pypi/lux 13 | :Source: https://github.com/quantmind/lux 14 | :Platforms: Linux, OSX, Windows. Python 3.5 and above 15 | :Keywords: asynchronous, wsgi, websocket, redis, json-rpc, REST, web 16 | 17 | .. |pypiversion| image:: https://badge.fury.io/py/lux.svg 18 | :target: https://pypi.python.org/pypi/lux 19 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/lux.svg 20 | :target: https://pypi.python.org/pypi/lux 21 | .. |license| image:: https://img.shields.io/pypi/l/lux.svg 22 | :target: https://pypi.python.org/pypi/lux 23 | .. |status| image:: https://img.shields.io/pypi/status/lux.svg 24 | :target: https://pypi.python.org/pypi/v 25 | .. |downloads| image:: https://img.shields.io/pypi/dd/lux.svg 26 | :target: https://pypi.python.org/pypi/lux 27 | .. |circleci| image:: https://circleci.com/gh/quantmind/lux.svg?style=svg 28 | :target: https://circleci.com/gh/quantmind/lux 29 | .. |coverage| image:: https://codecov.io/gh/quantmind/lux/branch/master/graph/badge.svg 30 | :target: https://codecov.io/gh/quantmind/lux 31 | .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/u0x9r57svde3595d/branch/master?svg=true 32 | :target: https://ci.appveyor.com/project/lsbardel/lux 33 | 34 | An asynchronous web framework for python. Lux is built with pulsar_ and uses 35 | asyncio_ as asynchronous engine. It can be configured to be explicitly asynchronous 36 | or implicitly asynchronous via the greenlet_ library. 37 | 38 | 39 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 40 | .. _pulsar: https://github.com/quantmind/pulsar 41 | .. _greenlet: https://greenlet.readthedocs.org 42 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | 3 | matrix: 4 | # Pre-installed Python versions, which Appveyor may upgrade to 5 | # a later point release. 6 | - PYTHON: "C:\\Python35" 7 | PYTHON_VERSION: "3.5.x" 8 | PYTHON_ARCH: "32" 9 | 10 | - PYTHON: "C:\\Python35-x64" 11 | PYTHON_VERSION: "3.5.x" 12 | PYTHON_ARCH: "64" 13 | 14 | init: 15 | - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" 16 | 17 | install: 18 | - "%WITH_COMPILER% %PYTHON%/python -V" 19 | - "%WITH_COMPILER% %PYTHON%/python -m pip install -r requirements-dev.txt" 20 | - cmd: nuget install redis-64 -excludeversion 21 | - cmd: redis-64\tools\redis-server.exe --service-install 22 | - cmd: redis-64\tools\redis-server.exe --service-start 23 | 24 | build: off 25 | 26 | test_script: 27 | - "%WITH_COMPILER% %PYTHON%/python setup.py test -q" 28 | 29 | notifications: 30 | - provider: Email 31 | on_build_success: false 32 | on_build_failure: false 33 | -------------------------------------------------------------------------------- /bin/luxmake.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import lux 3 | 4 | if __name__ == "__main__": 5 | lux.execute_from_config('lux') 6 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | pip install --upgrade pip wheel 4 | pip install --upgrade setuptools 5 | pip install -r requirements/hard.txt 6 | pip install -r requirements/soft.txt 7 | pip install -r requirements/dev.txt 8 | -------------------------------------------------------------------------------- /docs/application.md: -------------------------------------------------------------------------------- 1 | # Application 2 | 3 | This document describe the public API of a Lux Application ``app``. 4 | The application is an [asynchrouns WSGI][] handler. 5 | 6 | ## # app.app 7 | 8 | The application itself: 9 | ```python 10 | app == app.app 11 | ``` 12 | Useful when using the applicatin instead of a ``request`` object 13 | 14 | ## # app.callable 15 | 16 | Instance of the ``App`` class which created the application. The callable 17 | is a picklable object and thereofre it can be passed to subprocesses when 18 | running in multiprocessing mode. 19 | ```python 20 | app == app.callable.handler() 21 | ``` 22 | 23 | The callable exposes the following properties: 24 | 25 | * ``app.callable.command`` name of the command executed 26 | * ``app.callable.argv`` list or command line parameters, without the command name 27 | * ``app.callable.script`` the python script which created the application 28 | 29 | ## # app.config 30 | 31 | The configuration dictionary. It contains all parameters specified 32 | in extensions included in the application. 33 | 34 | 35 | ## # app.forms 36 | 37 | Dictionary of forms available 38 | 39 | ## # app.green_pool 40 | 41 | Pool of greenlets where middleware are executed 42 | 43 | ## # app.models 44 | 45 | The model container. This is a dictionary-like data structure 46 | containing Lux models registered with the application. 47 | 48 | ## # app.providers 49 | 50 | Dictionary of service [providers](./providers.md) 51 | 52 | ## # app.stdout & app.stderr 53 | 54 | Application standard output and standard error 55 | 56 | [asynchrouns WSGI]: http://quantmind.github.io/pulsar/apps/wsgi/async.html 57 | -------------------------------------------------------------------------------- /docs/artwork/lux.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/docs/artwork/lux.sketch -------------------------------------------------------------------------------- /docs/authentication.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | Authentication is controlled by the ``lux.extensions.rest`` extension which specify 4 | the ``AUTHENTICATION_BACKENDS`` parameter. The parameter represents 5 | a list of python dotted path to authentication backend classes. 6 | 7 | 8 | ## Authenitication Backend 9 | 10 | An authentication backend class should derive from ``lux.core.AuthBase`` 11 | 12 | ### AuthBackend.request(*request*) 13 | 14 | This method is called when a new request is received. An authentication 15 | backend should perform its authentication process or it can set objects in the ``request.cache`` 16 | object or both. This method should always return ``None`` and it can raise 17 | Http errors. 18 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | * [Versions 0.8](/docs/history/0.8.md) 4 | * [Versions 0.7](/docs/history/0.7.md) 5 | * [Versions 0.6](/docs/history/0.6.md) 6 | * [Versions 0.5](/docs/history/0.5.md) 7 | * [Versions 0.4](/docs/history/0.4.md) 8 | * [Versions 0.3](/docs/history/0.3.md) 9 | * [Versions 0.2](/docs/history/0.2.md) 10 | * [Versions 0.1](/docs/history/0.1.md) 11 | -------------------------------------------------------------------------------- /docs/extensions.md: -------------------------------------------------------------------------------- 1 | # Lux Extensions 2 | 3 | * [Writing Extensions](../lux/extensions/readme.md) 4 | 5 | 6 | ## Included Extensions 7 | 8 | * [Auth](../lux/extensions/auth/readme.md) 9 | * [Applications](../lux/extensions/applications/readme.md) 10 | * [Organisations](../lux/extensions/organisations/readme.md) 11 | -------------------------------------------------------------------------------- /docs/getting-started.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Getting Started 3 | ================= 4 | 5 | 6 | Python Requirements 7 | ======================= 8 | 9 | **Hard requirements** 10 | 11 | * pulsar_ asychronous engine 12 | * greenlet_ implicit asynchronous code 13 | * jinja2_ template engine 14 | * pytz_ timezones and countries 15 | * dateutil_ date utilities 16 | * sqlalchemy_ and pulsar-odm_ used by ``lux.extensions.odm`` 17 | * pyjwt_ used by some authentication backends 18 | 19 | **Soft requirements** 20 | 21 | * inflect_ correctly generate plurals, ordinals, indefinite articles; convert numbers to words 22 | * markdown_ 23 | * oauthlib_ for ``lux.extensions.oauth`` 24 | * premailer_ for rendering email HTML 25 | 26 | 27 | Testing 28 | ========== 29 | 30 | For testing, create the test database first:: 31 | 32 | psql -a -f tests/db.sql 33 | 34 | To run tests:: 35 | 36 | python setup.py test 37 | 38 | For options and help type:: 39 | 40 | python setup.py test --help 41 | 42 | flake8_ check (requires flake8 package):: 43 | 44 | flake8 45 | 46 | Debugging javascript on Chrome:: 47 | 48 | npm run-script debug 49 | 50 | 51 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 52 | .. _pulsar: https://github.com/quantmind/pulsar 53 | .. _pytz: http://pytz.sourceforge.net/ 54 | .. _dateutil: https://pypi.python.org/pypi/python-dateutil 55 | .. _sqlalchemy: http://www.sqlalchemy.org/ 56 | .. _pulsar-odm: https://github.com/quantmind/pulsar-odm 57 | .. _pyjwt: https://github.com/jpadilla/pyjwt 58 | .. _pbkdf2: https://pypi.python.org/pypi/pbkdf2 59 | .. _gruntjs: http://gruntjs.com/ 60 | .. _nodejs: http://nodejs.org/ 61 | .. _grunt: http://gruntjs.com/ 62 | .. _markdown: https://pypi.python.org/pypi/Markdown 63 | .. _oauthlib: https://oauthlib.readthedocs.org/en/latest/ 64 | .. _sphinx: http://sphinx-doc.org/ 65 | .. _greenlet: https://greenlet.readthedocs.org 66 | .. _`grunt-html2js`: https://github.com/karlgoldstein/grunt-html2js 67 | .. _lux.js: https://raw.githubusercontent.com/quantmind/lux/master/lux/media/lux/lux.js 68 | .. _`Quantmind`: http://quantmind.com 69 | .. _flake8: https://pypi.python.org/pypi/flake8 70 | .. _jinja2: http://jinja.pocoo.org/docs/dev/ 71 | .. _premailer: https://github.com/peterbe/premailer 72 | .. _inflect: https://github.com/pwdyson/inflect.py 73 | -------------------------------------------------------------------------------- /docs/history/0.1.md: -------------------------------------------------------------------------------- 1 | Ver. 0.1.1 - 2014-Nov-24 2 | ======================================= 3 | * Added the example directory with a static site example 4 | * Test coverage increased, 92 unit tests. 5 | 6 | Ver. 0.1.0 - 2014-Oct-14 7 | ======================================= 8 | * First pre-alpha pypi release 9 | * Working for python 2.7, 3.3 and 3.4 10 | * pulsar and dateutil the only dependencies for core library 11 | -------------------------------------------------------------------------------- /docs/history/0.2.md: -------------------------------------------------------------------------------- 1 | Ver. 0.2.0 - 2015-May-22 2 | ======================================= 3 | * Working on Python 3.4 and above only 4 | * Requires pulsar 1.0.0 or above 5 | * SqlAlchemy_ Object data mapping as extension 6 | * Working with AngularJS 1.3 and 1.4 7 | * Websocket extension based on pulsar and sockjs 8 | * Python: 132 unit tests, 70% coverage. 9 | * JavaScript: 18 unit tests, no coverage report 10 | -------------------------------------------------------------------------------- /docs/history/0.3.md: -------------------------------------------------------------------------------- 1 | # Ver. 0.3.1 - 2015-Nov-26 2 | 3 | 4 | ## API 5 | 6 | * Added ``command`` to the Application callable which returns the command which 7 | created the application 8 | * Switched ``smtp`` extension hook from ``on_start`` to ``on_loaded`` 9 | * Rest metadata endpoint include a permissions object for the user 10 | * RestModel requires both ``updateform`` for updates, it does not use the ``form`` if the update form is not provided 11 | 12 | ## Internals 13 | 14 | * Don't prepend ``lux`` to extension loggers 15 | * Token authentication backend raise ``BadRequest`` (400) when token cannot be decoded 16 | * Store ``exclude_missing`` in form after validation 17 | * ``on_html_document`` event catches errors via the ``safe`` keywords [[9ba5d17](https://github.com/quantmind/lux/commit/af9193d20475588eacbdaf5f629751f6799a76c1)] 18 | 19 | ## Front end 20 | 21 | * Added enquiry to formHandlers and set it as resultHandler in ContactForm 22 | * Update ng-file-upload to version 10.0.2 23 | * Use ng-file-upload to submit forms containing file fields. This behaviour can be disabled by passing ``useNgFileUpload=False`` as a form attribute. 24 | * Form validation fixes [[#155](https://github.com/quantmind/lux/pull/155)] 25 | * Form date input fix [[#194](https://github.com/quantmind/lux/pull/194)] 26 | * Display relationship fields properly in grids [[#194](https://github.com/quantmind/lux/pull/196)] 27 | * Bug fix: correct checkbox rendering in lux forms [[#199](https://github.com/quantmind/lux/pull/199)] 28 | * Added url type handler to lux grids [[#200](https://github.com/quantmind/lux/pull/200)] 29 | 30 | 31 | Ver. 0.3.0 - 2015-Nov-13 32 | =========================== 33 | * Angular grid 34 | * Websocket extension 35 | * Forms set status code to 422 when validation fails 36 | * More extensions and fixes 37 | * Python: 293 unit tests, 75% coverage 38 | * JavaScript: 37 unit tests, 28.3% coverage (639/2258 lines) 39 | -------------------------------------------------------------------------------- /docs/history/0.4.md: -------------------------------------------------------------------------------- 1 | # Ver. 0.4.0 - 2015-Dec-10 2 | 3 | 4 | * Development Status set to ``3 - Alpha`` 5 | 6 | ## API 7 | * Better template directory for ``start_project`` command 8 | * ``style`` command create intermediary directories for the target css file 9 | * Removed the ``lux.extensions.cms`` module. No longer used 10 | * Added Registration views and backend models 11 | * Don't add OG metadata on response errors 12 | * Obfuscate javascript context dictionary using base64 encoding 13 | * RestModel 14 | * get request correctly handles multiple args [[209](https://github.com/quantmind/lux/pull/209)] 15 | * Add support for ``ne`` (not equals) and ``search`` operators [[#212](https://github.com/quantmind/lux/pull/212)] 16 | * ``search`` operator is the default operator for string columns in the javascript ``lux.grid`` component 17 | * Added new ``get_permissions`` method to backend base class and implemented in the ``auth`` backend 18 | * Permissions controlled via JSON documents with actions specified as 19 | ``read``, ``update``, ``create`` and ``delete``. No more numeric values, only string allowed. 20 | It is possible to set the wildcard ``*`` for allowing or denying all permissions 21 | to a given resource 22 | * Admin sitemap method check for read permission if a backend is available and cache on per user basis 23 | * Use ``PASSWORD_SECRET_KEY`` for encrypting passwords 24 | 25 | ## Javascript 26 | * Added scrollbar to sidebar [[214](https://github.com/quantmind/lux/pull/214)] 27 | * Clearfix in sidebar [[215](https://github.com/quantmind/lux/pull/215)] 28 | * Add ability to remember the selected link in sidebar submenus [[211](https://github.com/quantmind/lux/pull/221)] 29 | 30 | ## Bug Fixes 31 | * Make sure ``MEDIA_URL`` does not end with a forward slash when adding the media router 32 | * Several fixes in the ``lux.extensions.rest.client`` 33 | * Allows to display arrays in codemirror editor when in JSON mode [[#171](https://github.com/quantmind/lux/pull/171)] 34 | 35 | **329 unit tests** 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/history/0.5.md: -------------------------------------------------------------------------------- 1 | # Ver. 0.5.1 - 2016-Jan-21 2 | 3 | 4 | ## Forms 5 | 6 | * Select field changes [[#249](https://github.com/quantmind/lux/pull/249)] 7 | * Stop pre-selecting the first option in select fields 8 | * For ui-select selects, this uses the placeholder functionality. For standard selects, this uses a 'Please select...' option. 9 | * Allows the value of non-required select field to be cleared 10 | * Correctly sets the required attribute for ui-select selects 11 | * Makes the display value for ui-select fields use the repr value if available 12 | 13 | 14 | # Ver. 0.5.0 - 2016-Jan-08 15 | 16 | 17 | ## API 18 | * Api client is now a callable and requires the ``request`` object as first parameter. 19 | In this way the user agent and a possible token can be included in the api request 20 | [[233](https://github.com/quantmind/lux/pull/233)] 21 | * RestModel, the ``:search`` operator now explicitly provides a full-text search config/language to PostgreSQL, 22 | allowing such queries to use available GIN/GiST indexes. This defaults to `english`, and can be overridden 23 | globally via the `DEFAULT_TEXT_SEARCH_CONFIG` parameter or per-column by passing 24 | `info={'text_search_config'='language'}` to `sqlalchemy.Column.__init__` 25 | * Browser backend does not assume a session is available [[2a24d0c](https://github.com/quantmind/lux/commit/2a24d0c8b2513dda41cd86952b42f0b3c3184d76)] 26 | 27 | ## Directives 28 | * Breadcrumbs directive no longer appends trailing slashes to links [[#243](https://github.com/quantmind/lux/pull/243)] -------------------------------------------------------------------------------- /docs/history/0.6.md: -------------------------------------------------------------------------------- 1 | # Ver. 0.6.0 - 2016-Feb-09 2 | 3 | This release is backward incompatible with previous ones and brings a host of 4 | new features, bug fixes and improved API. 5 | 6 | ## Python 7 | 8 | * Rest models registration during wsgi middleware creation 9 | * Models are accessible via the ``app.models`` dictionary 10 | * Refactored the ``sockjs`` extension with ``rpc`` and ``pubsub`` protocols 11 | * Several bug fixes in authentication backends 12 | * Much better ``api`` client for lux Rest APIs 13 | * Removed dulwich dependency 14 | 15 | ## Media 16 | 17 | * Javascript source moved into the ``lux/media/js`` directory 18 | * SCSS source located in the ``lux/media/scss`` directory 19 | * Use of ``require.js`` to include modules 20 | * Started using ``scss`` 21 | 22 | ## Docs 23 | 24 | * Documentation in the top level ``docs`` folder 25 | 26 | ## Tests 27 | 28 | * Added websocket tests 29 | * Added web-api interaction tests with a new test class from ``lux.utils.tests`` 30 | * Increased test coverage to 81% 31 | 32 | -------------------------------------------------------------------------------- /docs/history/0.7.md: -------------------------------------------------------------------------------- 1 | ## Ver. 0.7.0 - 2016-May-22 2 | 3 | Backward incompatible release with lots of API refactoring and bug fixes. 4 | Python 3.5 only supported. 5 | 6 | * Use ES6 for javascript components 7 | * Added beforeAll classmethod in test class [8491ade](https://github.com/quantmind/lux/commit/8491aded9a7fde94d58da1ee96985ee167ca8f24) 8 | * Relaxed JSONField validation and added tests for UserRest views [c2c6a2c](https://github.com/quantmind/lux/commit/c2c6a2c22071a8d1c531b01f172834453559978d) 9 | * Lux.js CI in circleci [4d13c6b](https://github.com/quantmind/lux/commit/4d13c6bc4e917e7e217c6f9c9a19b0870c5097c0) 10 | * Added providers dictionary to application. it can be used to configure service providers [d5b28c1](https://github.com/quantmind/lux/commit/d5b28c17192d3830d01cfa9cafbb60faa7541681) 11 | * Added recommonmark to requirement-dev [f570ecc](https://github.com/quantmind/lux/commit/f570eccd0fa1de479a224870e35ede952e706918) 12 | * Use ``instance_state`` function from sqlalchemy to check if a db model is detached in ``tojson`` method [d7b9bcc](https://github.com/quantmind/lux/commit/d7b9bcc05e0f849c19fbc47b3fb409c83f98603b) 13 | * Added ``SESSION_BACKEND`` configuration parameter to distinguish from ``CACHE_SERVER`` [e60cc8e](https://github.com/quantmind/lux/commit/e60cc8e8c2bb4dc0db9670026889286690975e1e) 14 | * Removed angular extension [5fe40e7](https://github.com/quantmind/lux/commit/5fe40e786acff23bd818009b48b52e2536dd2659) 15 | * Allow MEDIA_URL to be absolute [28ade3f](https://github.com/quantmind/lux/commit/28ade3fd930bdde8af4025a00292aa4f67e9ac26) 16 | * Allow to pass an api target dictionary as path to the lux api handler. The api handler can be obtained directly from the request. [53977f4](https://github.com/quantmind/lux/commit/53977f4e5f1a6b6679fec702ec62d742b96bf035) 17 | * Allow to override API routers and DB models [1a257b5](https://github.com/quantmind/lux/commit/1a257b574e7e8cb61923bb2c734ffc8051c82f6c) 18 | * Added clear_cache command [2c9fc3e](https://github.com/quantmind/lux/commit/2c9fc3ec1bb40487a61226c59df60ad54a862dc8) 19 | 20 | -------------------------------------------------------------------------------- /docs/history/0.8.md: -------------------------------------------------------------------------------- 1 | ## Ver. 0.8.2 - 2016-Dec-15 2 | 3 | Bug fixes and test coverage 4 | 5 | 6 | ## Ver. 0.8.1 - 2016-Nov-29 7 | 8 | Several bug fixes and improvements. 9 | Compatible-ish with previous release, still in alpha :expressionless:. 10 | Selected changes: 11 | 12 | * Added ``requirejs`` to content metadata 13 | * Allow to override ``table_create`` in testing 14 | * Added ``create_token`` command 15 | * Added hook for filtering owned model 16 | * Added ownership ``UniqueField`` validator 17 | * Added ``RelationshipField`` for Owned Targets 18 | * Filter ``orgmembership`` rather than using get 19 | 20 | 21 | ## Ver. 0.8.0 - 2016-Nov-07 22 | 23 | Overall refactoring of the codebase, totally incompatible with previous releases :boom: 24 | 25 | * Massive refactoring of authentication backends 26 | * refactored ``auth`` extension for third party access via JWT and Bearer tokens 27 | * Added ``sessions`` extension, previously part of the rest extension 28 | * Added ``applications`` extension for managing multiple lux application 29 | * Added ``organisations`` extension which extends the ``auth`` extension with organisations and applications 30 | * Client API handler works with remote and local APIs 31 | * Api-cs command - initial implementation 32 | * Channels handled by pulsar channels 33 | * Many other improvements 34 | * Last alpha serie, 0.9 will be in beta -------------------------------------------------------------------------------- /docs/layout.rst: -------------------------------------------------------------------------------- 1 | .. _project-layout: 2 | 3 | ================== 4 | Project Layout 5 | ================== 6 | 7 | A lux project always defines a folder with the name defining the web 8 | application. Let's say the web site is called ``quasar``, than the 9 | project standard layout:: 10 | 11 | - quasar-project 12 | - quasar 13 | - media 14 | - src 15 | - quasar 16 | __init__.py 17 | settings.py 18 | manage.py 19 | package.json 20 | Gruntfile.js 21 | README.rst 22 | 23 | 24 | The ``manage.py`` script is the main entry point for the web site and should have 25 | the following structure:: 26 | 27 | import lux 28 | 29 | if __name__ == '__main__': 30 | lux.execute_from_config('quasar.settings') 31 | 32 | 33 | Lux install an utility script which can be used to setup a project and add 34 | extensions to it:: 35 | 36 | luxmake.py startproject quasar 37 | 38 | creates your project layout inside the ``quasar`` directory. 39 | -------------------------------------------------------------------------------- /docs/providers.md: -------------------------------------------------------------------------------- 1 | # Providers 2 | 3 | Lux uses several services such HTTP client, websocket clients and so forth. 4 | Providers are a way to specify the handler which provide a given service: 5 | 6 | To override a provider, create a ``LuxExtension`` and implement the ``on_config`` 7 | method: 8 | ```python 9 | def on_config(self, app): 10 | import requests 11 | app.providers('Http') = lambda _: requests.Session() 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Lux Documentation 2 | 3 | * [Getting Started](./getting-started.rst) 4 | * [Application](./application.md) 5 | * [Extensions](./extensions.md) 6 | * [Providers](./providers.md) 7 | * [Rest API](./rest.md) 8 | * [Migrations](./migrations.rst) 9 | * [Testing](./testing.md) 10 | * [Changelog](./changelog.md) 11 | -------------------------------------------------------------------------------- /docs/rest.md: -------------------------------------------------------------------------------- 1 | # Rest Extension 2 | 3 | Extension for Restful web services. 4 | It requires [apispec][] and [marshmallow][] packages: 5 | ```python 6 | EXTENSIONS = [ 7 | ..., 8 | 'lux.extensions.rest', 9 | ... 10 | ] 11 | ``` 12 | 13 | ## Token Backend 14 | 15 | This extensions implements an abstract authentication backend based on **authorization tokens**, 16 | the ``lux.extensions.rest.TokenBackend``. 17 | 18 | 19 | 20 | [apispec]: https://github.com/marshmallow-code/apispec 21 | [marshmallow]: https://github.com/marshmallow-code/marshmallow 22 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | This document describe the tools available for testing lux applications. 4 | These tools are located in the ``lux.utils.test`` module where you can 5 | find two test classes derived from python ``unittest.TestCase`` 6 | class. 7 | 8 | ## Test Classes 9 | 10 | ### TestCase 11 | 12 | This test class is designed to unit test lux components. It provides a 13 | set of methods useful for web applications. 14 | 15 | #### # self.application(**parameters) 16 | 17 | Create an application with additional config *parameters* 18 | 19 | #### # self.authenticity_token(*doc*) 20 | 21 | Return the ``CSRF`` token contained in the HTML document if it exists. 22 | 23 | ### AppTestCase 24 | 25 | This test class is designed to test a single lux application and 26 | exposes the following class properties and methods: 27 | 28 | #### # cls.app 29 | 30 | The test class application. Created in the ``setUpClass`` method 31 | via the ``cls.create_test_application()`` class method. 32 | 33 | #### # cls.client 34 | 35 | A ``client`` for the application test class, created via 36 | the ``cls.get_client()`` class method. 37 | 38 | #### # cls.config_file 39 | 40 | Dotted path to the config file used to setup the application for 41 | the test class. 42 | 43 | #### # cls.beforeAll() 44 | 45 | Class method invoked at the and of the ``setUpClass`` method. 46 | Database and fixtures are already loaded. 47 | By default **it does nothing**, 48 | override to create class level properties. 49 | It can be asynchronous or not. 50 | 51 | #### # cls.create_admin_jwt() 52 | 53 | Create the application admin JWT token. It can be asynchronous or not. 54 | 55 | #### # cls.get_client() 56 | 57 | Create a test client. It is used in the ``setUpClass`` to create 58 | the ``cls.client``. By default it returns: 59 | ```python 60 | TestClient(cls.app) 61 | ``` 62 | 63 | ## Utilities 64 | 65 | ### TestClient 66 | 67 | The test client used for testing lux applications: 68 | ```python 69 | client = TestClient(app) 70 | ``` 71 | 72 | 73 | ### green 74 | 75 | Decorator to run a test function on the application ``green_pool``. 76 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | Lux powered 2 | 3 | This directory contains three examples for sites powered by lux. The script to 4 | run the examples is ``manage.py`` in the top level directory. 5 | 6 | * A stand-alone website serving HTML content: 7 | ``` 8 | python manage.py webalone 9 | ``` 10 | * A stand-alone API site for a JSON-REST API 11 | ``` 12 | python manage.py webapi 13 | ``` 14 | * A website serving HTML content which uses the API site for authentication and data 15 | ``` 16 | python manage.py website 17 | ``` 18 | 19 | # Setup 20 | 21 | Before running the services, one need to configure the PostgreSQL database. 22 | This is a one-time operation, from the top level directory type: 23 | ``` 24 | psql -U postgres -f tests/db.sql 25 | ``` 26 | 27 | Create a virtual environment and install dependencies: 28 | ``` 29 | pip install virtaulenv myexample 30 | ./myexample/bin/pip install -r requirements-dev.txt 31 | ``` 32 | 33 | ## Webalone example 34 | 35 | This is a stand-alone web application with session authentication and database models. 36 | 37 | Set up migration environment (one time operation) 38 | ``` 39 | # setup migration environment 40 | ./myexample/bin/python manage.py webalone alembic init 41 | # create initial migration 42 | ./myexample/bin/python manage.py webalone alembic auto -m initial 43 | ``` 44 | 45 | Create tables (one time operation) 46 | ``` 47 | ./myexample/bin/python manage.py webalone alembic upgrade heads 48 | ``` 49 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def main(): 4 | from lux.core import execute_from_config 5 | 6 | execute_from_config('example.%s.config', 7 | description='Lux powered example', 8 | services=('webalone', 'webapi', 'website')) 9 | -------------------------------------------------------------------------------- /example/cfg.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tests.config import redis_cache_server 4 | 5 | EXTENSIONS = ( 6 | 'lux.ext.base', 7 | 'lux.ext.rest', 8 | 'lux.ext.content', 9 | 'lux.ext.admin', 10 | 'lux.ext.auth', 11 | 'lux.ext.odm' 12 | ) 13 | 14 | 15 | APP_NAME = COPYRIGHT = HTML_TITLE = 'website.com' 16 | SECRET_KEY = '01W0o2gOG6S1Ldz0ig8qtmdkEick04QlN84fS6OrAqsMMs64Wh' 17 | SESSION_STORE = redis_cache_server 18 | EMAIL_DEFAULT_FROM = 'admin@lux.com' 19 | EMAIL_BACKEND = 'lux.core.mail.LocalMemory' 20 | SESSION_COOKIE_NAME = 'test-website' 21 | SESSION_STORE = 'redis://127.0.0.1:6379/13' 22 | 23 | DATASTORE = 'postgresql+green://lux:luxtest@127.0.0.1:5432/luxtests' 24 | 25 | 26 | SERVE_STATIC_FILES = os.path.join(os.path.dirname(__file__), 'media') 27 | CONTENT_REPO = os.path.dirname(__file__) 28 | CONTENT_LOCATION = 'content' 29 | CONTENT_GROUPS = { 30 | "articles": { 31 | "path": "articles", 32 | "body_template": "home.html", 33 | "meta": { 34 | "priority": 1 35 | } 36 | }, 37 | "site": { 38 | "path": "*", 39 | "body_template": "home.html", 40 | "meta": { 41 | "priority": 1, 42 | "image": "/media/lux/see.jpg" 43 | } 44 | } 45 | } 46 | 47 | DEFAULT_POLICY = [ 48 | { 49 | "resource": "contents:*", 50 | "action": "read" 51 | } 52 | ] 53 | -------------------------------------------------------------------------------- /example/content/articles/nofollow.md: -------------------------------------------------------------------------------- 1 | priority: 0 2 | title: No follow 3 | 4 | This should not be in links nor in the sidemap 5 | -------------------------------------------------------------------------------- /example/content/articles/nolink.md: -------------------------------------------------------------------------------- 1 | title: Not in links 2 | 3 | This should not be in links 4 | -------------------------------------------------------------------------------- /example/content/articles/security.md: -------------------------------------------------------------------------------- 1 | order: 30 2 | 3 | Very secure 4 | -------------------------------------------------------------------------------- /example/content/articles/test.md: -------------------------------------------------------------------------------- 1 | order: 10 2 | priority: 2 3 | title: Just a test 4 | 5 | Another test 6 | -------------------------------------------------------------------------------- /example/content/auth/login.md: -------------------------------------------------------------------------------- 1 | {{ html_main }} 2 | -------------------------------------------------------------------------------- /example/content/config.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/example/content/config.json -------------------------------------------------------------------------------- /example/content/context/follow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/content/context/footer.md: -------------------------------------------------------------------------------- 1 | require_context: html_footer1, html_footer2, html_footer3 2 | 3 | 33 | -------------------------------------------------------------------------------- /example/content/context/footer1.md: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /example/content/context/footer2.md: -------------------------------------------------------------------------------- 1 | 2 | Lux 3 | 4 | -------------------------------------------------------------------------------- /example/content/context/footer3.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/content/site/404.md: -------------------------------------------------------------------------------- 1 | # 404 2 | 3 | **There isn't a page here!** 4 | -------------------------------------------------------------------------------- /example/content/site/images.md: -------------------------------------------------------------------------------- 1 | ### SVG 2 | 3 | 4 | Lux svg 5 | 6 | 7 | 8 | ### PNG (300x300) 9 | 10 | 11 | Lux png 300x300 12 | 13 | 14 | 15 | ### Lux Powered 16 | 17 | 18 | Lux powered 19 | 20 | 21 | 22 | Lux powered light 23 | 24 | -------------------------------------------------------------------------------- /example/content/site/index.md: -------------------------------------------------------------------------------- 1 | template: partials/landing.html 2 | 3 | Lux is easy to install once you have python installed in your system: 4 | ``` 5 | $ pip install lux 6 | ``` 7 | The above command installs ``lux`` into your python ``site_packages`` directory 8 | and the ``luxmake.py`` command you can use to create lux projects: 9 | ``` 10 | $ luxmake.py startproject helloworld 11 | project "helloworld" created 12 | ``` 13 | Lux has been designed to work with [AngularJS][] frontend and to load javascript 14 | modules asynchronously via [RequireJS][]. 15 | -------------------------------------------------------------------------------- /example/content/templates/doc.html: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | {{ html_main }} 21 |
22 |
23 |
24 |
25 |
26 | {{ html_footer }} 27 | -------------------------------------------------------------------------------- /example/content/templates/home.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | {{ html_main }} 5 |
6 | {{ html_footer }} 7 |
8 | -------------------------------------------------------------------------------- /example/content/templates/main.html: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | {{ html_main }} 21 |
22 |
23 |
24 |
25 |
26 | {{ html_footer }} 27 | -------------------------------------------------------------------------------- /example/content/templates/partials/landing.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 |
7 |

Toolkit for crafting super web applications with Python and AngularJS

8 | 14 | 20 |

Version $site_lux_version

21 | 22 | Build Status 23 | 24 | 25 | Coverage Status 26 | 27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 | {{ html_main }} 38 |
39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /example/content/templates/test-multi/bla.html: -------------------------------------------------------------------------------- 1 | {{ html_main }} 2 | -------------------------------------------------------------------------------- /example/content/templates/test-multi/foo.html: -------------------------------------------------------------------------------- 1 | {{ html_main }} 2 | -------------------------------------------------------------------------------- /example/media/lux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/example/media/lux.png -------------------------------------------------------------------------------- /example/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | // Font 2 | $font-size: 16px; 3 | -------------------------------------------------------------------------------- /example/scss/components/_helpers.scss: -------------------------------------------------------------------------------- 1 | @media (min-width: $screen-sm-min) { 2 | // Padding for fixed header 3 | ._header-padding{ 4 | padding-top: 60px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/scss/luxsite.scss: -------------------------------------------------------------------------------- 1 | // Imports 2 | // * Bootswatch variables 3 | // * Bootstrap sass 4 | // * Boostwatch scss 5 | // * Lux legacy python css 6 | @import 'bootswatch/variables'; 7 | @import 'bootstrap-sass/assets/stylesheets/bootstrap'; 8 | @import 'bootswatch/bootswatch'; 9 | // TOTO: Remove this 10 | // Legacy python generated css 11 | @import 'deps/py.lux'; 12 | 13 | // Variables 14 | @import 'deps/lux/variables/all'; 15 | @import 'variables'; 16 | 17 | // Mixins 18 | @import 'deps/lux/mixins/all'; 19 | 20 | // Lux Imports 21 | @import 'deps/lux/components/content'; 22 | @import 'deps/lux/components/datepicker'; 23 | @import 'deps/lux/components/fileupload'; 24 | @import 'deps/lux/components/grid'; 25 | @import 'deps/lux/components/navbar'; 26 | @import 'deps/lux/components/pagination'; 27 | @import 'deps/lux/components/sidebar'; 28 | // 29 | // Site css 30 | @import 'components/helpers'; 31 | -------------------------------------------------------------------------------- /example/webalone/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/example/webalone/__init__.py -------------------------------------------------------------------------------- /example/webalone/config.py: -------------------------------------------------------------------------------- 1 | from example.cfg import * # noqa 2 | 3 | API_URL = '/api' 4 | DEFAULT_CONTENT_TYPE = 'text/html' 5 | # 6 | # sessions 7 | AUTHENTICATION_BACKENDS = [ 8 | 'lux.ext.sessions:SessionBackend', 9 | 'lux.ext.auth:TokenBackend' 10 | ] 11 | SESSION_EXCLUDE_URLS = [ 12 | 'api', 13 | 'api/', 14 | 'media', 15 | 'media/' 16 | ] 17 | # 18 | EXTENSIONS += ('lux.ext.sessions',) # noqa 19 | HTML_SCRIPTS = ['website/website'] 20 | DEFAULT_POLICY = [ 21 | { 22 | "resource": "api:contents:*", 23 | "action": "read" 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /example/webapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/example/webapi/__init__.py -------------------------------------------------------------------------------- /example/webapi/config.py: -------------------------------------------------------------------------------- 1 | from example.cfg import * # noqa 2 | 3 | 4 | API_URL = '' 5 | AUTHENTICATION_BACKENDS = ['lux.ext.auth:TokenBackend'] 6 | DATASTORE = 'postgresql+green://lux:luxtest@127.0.0.1:5432/luxtests' 7 | SERVE_STATIC_FILES = False 8 | -------------------------------------------------------------------------------- /example/website/__init__.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxExtension 2 | from lux.ext.sessions import ActionsRouter 3 | 4 | 5 | class Extension(LuxExtension): 6 | 7 | def middleware(self, app): 8 | return [DummyTestActionsRouter('settings'), 9 | TestActionsRouter('testing')] 10 | 11 | 12 | class DummyTestActionsRouter(ActionsRouter): 13 | templates_path = 'cdhsgcvhdcvd' 14 | 15 | 16 | class TestActionsRouter(ActionsRouter): 17 | default_action = 'bla' 18 | templates_path = 'test-multi' 19 | action_config = {} 20 | -------------------------------------------------------------------------------- /example/website/config.py: -------------------------------------------------------------------------------- 1 | from example.cfg import * # noqa 2 | 3 | EXTENSIONS += ('lux.ext.sessions',) # noqa 4 | DEFAULT_CONTENT_TYPE = 'text/html' 5 | API_URL = 'http://webapi.com' 6 | AUTHENTICATION_BACKENDS = [ 7 | 'lux.ext.sessions:SessionBackend' 8 | ] 9 | 10 | CLEAN_URL = True 11 | REDIRECTS = {'/tos': '/articles/terms-conditions'} 12 | 13 | 14 | HTML_LINKS = ({'href': 'luxsite/lux-114.png', 15 | 'sizes': '57x57', 16 | 'rel': 'apple-touch-icon'}, 17 | {'href': 'luxsite/lux-114.png', 18 | 'sizes': '114x114', 19 | 'rel': 'apple-touch-icon'}, 20 | {'href': 'luxsite/lux-144.png', 21 | 'sizes': '72x72', 22 | 'rel': 'apple-touch-icon'}, 23 | {'href': 'luxsite/lux-144.png', 24 | 'sizes': '144x144', 25 | 'rel': 'apple-touch-icon'}, 26 | 'luxsite/luxsite') 27 | -------------------------------------------------------------------------------- /lux/__init__.py: -------------------------------------------------------------------------------- 1 | """Asynchronous web framework for python""" 2 | import os 3 | 4 | from .utils.version import get_version 5 | 6 | VERSION = (0, 9, 0, 'alpha', 1) 7 | 8 | PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | __version__ = version = get_version(VERSION, __file__) 10 | -------------------------------------------------------------------------------- /lux/core/__init__.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.wsgi import route 2 | 3 | from .commands import ConsoleParser, CommandError, LuxCommand, Setting, option 4 | from .extension import LuxExtension, Parameter 5 | from .console import App, execute_from_config 6 | from .app import Application, extend_config, is_html 7 | from .routers import ( 8 | Router, HtmlRouter, JsonRouter, RedirectRouter, WebFormRouter, 9 | JSON_CONTENT_TYPES, DEFAULT_CONTENT_TYPES 10 | ) 11 | from .templates import register_template_engine, template_engine, Template 12 | from .cms import CMS, Page 13 | from .mail import EmailBackend 14 | from .cache import cached, Cache, register_cache, create_cache 15 | from .exceptions import raise_http_error, ShellError, http_assert 16 | from .auth import AuthBackend, Resource, AuthenticationError 17 | from .channels import LuxChannels 18 | from .user import UserMixin, User, ServiceUser 19 | from .client import AppClient 20 | 21 | 22 | __all__ = [ 23 | 'ConsoleParser', 24 | 'CommandError', 25 | 'LuxCommand', 26 | 'option', 27 | 'Setting', 28 | 'LuxExtension', 29 | 'Parameter', 30 | 'is_html', 31 | # 32 | 'Application', 33 | 'App', 34 | 'AppClient', 35 | 'execute_from_config', 36 | 'extend_config', 37 | 'register_template_engine', 38 | 'template_engine', 39 | 'Template', 40 | # 41 | 'CMS', 42 | 'Page', 43 | # 44 | 'EmailBackend', 45 | 'cached', 46 | 'Cache', 47 | 'register_cache', 48 | 'create_cache', 49 | 'raise_http_error', 50 | 'ShellError', 51 | 'http_assert', 52 | # 53 | 'Resource', 54 | 'AuthBackend', 55 | 'AuthenticationError', 56 | # 57 | 'Html', 58 | 'Router', 59 | 'HtmlRouter', 60 | 'JsonRouter', 61 | 'route', 62 | 'json_message', 63 | 'RedirectRouter', 64 | 'WebFormRouter', 65 | 'LuxContext', 66 | 'JSON_CONTENT_TYPES', 67 | 'DEFAULT_CONTENT_TYPES', 68 | # 69 | 'UserMixin', 70 | 'PasswordMixin', 71 | 'User', 72 | 'ServiceUser', 73 | # 74 | 'LuxChannels' 75 | ] 76 | -------------------------------------------------------------------------------- /lux/core/commands/clear_cache.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxCommand, Setting 2 | 3 | 4 | class Command(LuxCommand): 5 | help = "Clear Cache" 6 | option_list = ( 7 | Setting('prefix', 8 | nargs='?', 9 | desc=('Optional cache prefix. If omitted the default ' 10 | 'application prefix is used (APP_NAME)')), 11 | ) 12 | 13 | def run(self, options, **params): 14 | cache = self.app.cache_server 15 | result = cache.clear(options.prefix) 16 | self.write('Clear %d keys' % result) 17 | return result 18 | -------------------------------------------------------------------------------- /lux/core/commands/create_uuid.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxCommand 2 | from lux.utils.crypt import create_uuid 3 | 4 | 5 | class Command(LuxCommand): 6 | help = "Create a Universal Unique Identifier" 7 | 8 | def run(self, options, **params): 9 | return create_uuid().hex 10 | -------------------------------------------------------------------------------- /lux/core/commands/flush_models.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxCommand, Setting 2 | 3 | 4 | class Command(LuxCommand): 5 | option_list = ( 6 | Setting('dryrun', ('--dryrun',), 7 | action='store_true', 8 | default=False, 9 | desc=("It does not remove any data, instead it displays " 10 | "the number of models which could be removed")), 11 | Setting('models', nargs='+', 12 | desc='model1 model2 ... Use * to include all models') 13 | ) 14 | help = "Flush models in the data servers" 15 | 16 | def run(self, options, interactive=True, yn='yes'): 17 | if options.models[0] == '*': 18 | models = list(self.app.models) 19 | else: 20 | models = [] 21 | for model in options.models: 22 | if model in self.app.models: 23 | models.append(model) 24 | if not models: 25 | return self.write('Nothing done. No models') 26 | # 27 | self.write('\nAre you sure you want to remove these models?\n') 28 | for model in sorted(models): 29 | self.write('%s' % model) 30 | # 31 | if options.dryrun: 32 | self.write('\nNothing done. Dry run') 33 | else: 34 | self.write('') 35 | yn = input('yes/no : ') if interactive else yn 36 | if yn.lower() == 'yes': 37 | request = self.app.wsgi_request() 38 | for model in models: 39 | model = self.app.models[model] 40 | with model.session(request) as session: 41 | N = model.query(request, session).delete() 42 | self.write('{0} - removed {1}'.format(model, N)) 43 | else: 44 | self.write('Nothing done') 45 | -------------------------------------------------------------------------------- /lux/core/commands/generate_secret_key.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxCommand, Setting 2 | from lux.utils.crypt import generate_secret 3 | 4 | 5 | class Command(LuxCommand): 6 | help = "Generate a secret key." 7 | option_list = ( 8 | Setting('length', ('--length',), 9 | default=64, type=int, 10 | desc=('Secret key length')), 11 | Setting('hex', ('--hex',), 12 | default=False, 13 | action='store_true', 14 | desc=('Hexadecimal string')), 15 | ) 16 | 17 | def run(self, options, **params): 18 | return generate_secret(options.length, hexadecimal=options.hex) 19 | -------------------------------------------------------------------------------- /lux/core/commands/project_template/js/close.js: -------------------------------------------------------------------------------- 1 | 2 | lux.bootstrap('$project_name', ['lux.nav']); 3 | }); 4 | -------------------------------------------------------------------------------- /lux/core/commands/project_template/js/open.js: -------------------------------------------------------------------------------- 1 | define(rcfg.min(['lux/lux', 'angular-ui-router', 'angular-strap']), function (lux) { 2 | "use strict"; -------------------------------------------------------------------------------- /lux/core/commands/project_template/manage.py: -------------------------------------------------------------------------------- 1 | 2 | if __name__ == '__main__': 3 | import {{ project_name }} 4 | {{ project_name }}.main() 5 | -------------------------------------------------------------------------------- /lux/core/commands/project_template/project_name/__init__.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxExtension 2 | 3 | 4 | class Extension(LuxExtension): 5 | '''{{ project_name }} extension 6 | ''' 7 | def middleware(self, app): 8 | return [] 9 | 10 | 11 | def main(): 12 | from lux.core import execute_from_config 13 | 14 | execute_from_config('{{ project_name }}.config') 15 | -------------------------------------------------------------------------------- /lux/core/commands/project_template/project_name/config.py: -------------------------------------------------------------------------------- 1 | from .settings import * # noqa 2 | 3 | MINIFIED_MEDIA = False 4 | DATASTORE = 'sqlite:///$project_name.sqlite' 5 | CACHE_SERVER = 'redis://127.0.0.1:6379/3' 6 | 7 | CACHE_DEFAULT_TIMEOUT = 5 8 | -------------------------------------------------------------------------------- /lux/core/commands/project_template/project_name/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | $project_name settings 3 | """ 4 | APP_NAME = '$project_name' 5 | HTML_TITLE = '$project_name' 6 | DESCRIPTION = '$project_name' 7 | 8 | SECRET_KEY = '$secret_key' 9 | SESSION_COOKIE_NAME = APP_NAME.lower() 10 | 11 | AUTHENTICATION_BACKENDS = ['lux.extensions.auth.SessionBackend', 12 | 'lux.extensions.rest.backends.CsrfBackend', 13 | 'lux.extensions.rest.backends.BrowserBackend'] 14 | 15 | 16 | EXTENSIONS = ['lux.extensions.base', 17 | 'lux.extensions.cms', 18 | 'lux.extensions.auth'] 19 | 20 | HTML_META = [{'http-equiv': 'X-UA-Compatible', 21 | 'content': 'IE=edge'}, 22 | {'name': 'viewport', 23 | 'content': 'width=device-width, initial-scale=1'}, 24 | {'name': 'description', 'content': DESCRIPTION}] 25 | 26 | SERVE_STATIC_FILES = True 27 | REQUIREJS = ('$project_name/$project_name',) 28 | FAVICON = '$project_name/favicon.ico' 29 | HTML_LINKS = ['$project_name/$project_name'] 30 | 31 | LOGIN_URL = '/login' 32 | REGISTER_URL = '/signup' 33 | RESET_PASSWORD_URL = '/reset-password' 34 | 35 | # PULSAR config 36 | log_level = ['pulsar.info'] 37 | -------------------------------------------------------------------------------- /lux/core/commands/serve.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import get_actor, arbiter 2 | from pulsar.apps import wsgi 3 | from pulsar.utils.log import clear_logger 4 | 5 | from lux.core import LuxCommand, Setting 6 | 7 | 8 | nominify = Setting('nominify', 9 | ['--nominify'], 10 | action="store_true", 11 | default=False, 12 | desc="Don't use minified media files") 13 | 14 | 15 | class Command(LuxCommand): 16 | help = "Starts a fully-functional Web server using pulsar" 17 | option_list = (nominify,) 18 | wsgiApp = wsgi.WSGIServer 19 | 20 | def __call__(self, argv, start=True, get_app=False): 21 | self.app.callable.command = self.name 22 | app = self.app 23 | server = self.pulsar_app(argv, self.wsgiApp, 24 | server_software=app.config['SERVER_NAME']) 25 | if server.cfg.nominify: 26 | app.params['MINIFIED_MEDIA'] = False 27 | 28 | if start and not server.logger: # pragma nocover 29 | if not get_actor(): 30 | clear_logger() 31 | app._started = server() 32 | app.fire_event('on_start', data=server) 33 | arbiter().start() 34 | 35 | if not start: 36 | return app if get_app else server 37 | -------------------------------------------------------------------------------- /lux/core/commands/show_parameters.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | from lux.core import LuxCommand, Setting 4 | 5 | 6 | class Command(LuxCommand): 7 | help = "Show parameters." 8 | option_list = ( 9 | Setting('extensions', 10 | nargs='*', 11 | desc='Extensions to display parameters from.'), 12 | ) 13 | 14 | def run(self, options, **params): 15 | display = options.extensions 16 | config = self.app.config 17 | extensions = self.app.extensions 18 | for ext in chain([self.app], extensions.values()): 19 | if display and ext.meta.name not in display: 20 | continue 21 | if ext.meta.config: 22 | self.write('\n%s' % ext.meta.name) 23 | self.write('#=====================================') 24 | for key, value in ext.sorted_config(): 25 | self.write('%s: %s' % (key, config[key])) 26 | -------------------------------------------------------------------------------- /lux/core/commands/stop.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | import signal 4 | 5 | from pulsar.utils.tools import Pidfile 6 | 7 | from lux.core import LuxCommand, Setting, CommandError 8 | 9 | 10 | class Command(LuxCommand): 11 | help = "Stop a running server" 12 | 13 | option_list = ( 14 | Setting('timeout', ('--timeout',), 15 | default=10, type=int, 16 | desc=('Timeout for waiting SIGTERM stop')), 17 | ) 18 | pulsar_config_include = ('log_level', 'log_handlers', 'debug', 19 | 'config', 'pid_file') 20 | 21 | def run(self, options, **params): 22 | app = self.app 23 | pid_file = options.pid_file 24 | if pid_file: 25 | if os.path.isfile(pid_file): 26 | pid = Pidfile(pid_file).read() 27 | if not pid: 28 | raise CommandError('No pid in pid file %s' % pid_file) 29 | else: 30 | raise CommandError('Could not located pid file %s' % pid_file) 31 | else: 32 | raise CommandError('Pid file not available') 33 | 34 | try: 35 | self.kill(pid, signal.SIGTERM) 36 | except ProcessLookupError: 37 | raise CommandError('Process %d does not exist' % pid) from None 38 | 39 | start = time.time() 40 | while time.time() - start < options.timeout: 41 | if os.path.isfile(pid_file): 42 | time.sleep(0.2) 43 | else: 44 | app.write('Process %d terminated' % pid) 45 | return 0 46 | 47 | app.write_err('Could not terminate process after %d seconds' % 48 | options.timeout) 49 | self.kill(pid, signal.SIGKILL) 50 | app.write_err('Processed %d killed' % pid) 51 | return 1 52 | 53 | def kill(self, pid, sig): # pragma nocover 54 | os.kill(pid, sig) 55 | -------------------------------------------------------------------------------- /lux/core/console.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from copy import copy 3 | 4 | from pulsar.apps.wsgi import LazyWsgi 5 | 6 | from .commands import CommandError, service_parser 7 | from .app import Application 8 | 9 | 10 | def load_from_config(config_file, description=None, argv=None, 11 | services=None, **params): 12 | if services: 13 | p = service_parser(services, description, False) 14 | opts, argv = p.parse_known_args(argv) 15 | if not opts.service and len(argv) == 1 and argv[0] in ('-h', '--help'): 16 | service_parser(services, description).parse_known_args() 17 | config_file = config_file % (opts.service or services[0]) 18 | if opts.service and description: 19 | description = '%s - %s' % (description, opts.service) 20 | 21 | if argv is None: 22 | argv = sys.argv[:] 23 | params['script'] = argv.pop(0) 24 | 25 | params['description'] = description 26 | return App(config_file, argv, **params) 27 | 28 | 29 | def execute_from_config(config_file, cmdparams=None, **params): 30 | application = load_from_config(config_file, **params).setup() 31 | 32 | # Parse for the command 33 | parser = application.get_parser(add_help=False) 34 | opts, _ = parser.parse_known_args(application.argv) 35 | # 36 | # we have a command 37 | if opts.command: 38 | try: 39 | command = application.get_command(opts.command) 40 | except CommandError as e: 41 | print('\n'.join(('%s.' % e, 'Pass -h for list of commands'))) 42 | return 1 43 | application.argv.remove(command.name) 44 | cmdparams = cmdparams or {} 45 | try: 46 | return command(application.argv, **cmdparams) 47 | except CommandError as e: 48 | print(str(e)) 49 | return 1 50 | else: 51 | # this should fail unless we pass -h 52 | parser = application.get_parser(nargs=1) 53 | parser.parse_args(application.argv) 54 | 55 | 56 | class App(LazyWsgi): 57 | """WSGI callable app 58 | """ 59 | def __init__(self, config_file, argv, script=None, description=None, 60 | config=None, cfg=None, **kw): 61 | params = config or {} 62 | params.update(kw) 63 | self.params = params 64 | self.config_file = config_file 65 | self.script = script 66 | self.argv = argv 67 | self.description = description 68 | self.command = None 69 | self.cfg = cfg 70 | 71 | def setup(self, environ=None): 72 | return Application(self) 73 | 74 | def clone(self, **kw): 75 | params = self.params.copy() 76 | params.update(kw) 77 | app = copy(self) 78 | app.params = params 79 | app.argv = copy(app.argv) 80 | return app 81 | 82 | def close(self): 83 | return self.handler().close() 84 | -------------------------------------------------------------------------------- /lux/core/green.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.greenio.wsgi import GreenStream 2 | from pulsar.apps.greenio import wait 3 | from pulsar.apps.wsgi import wsgi_request, handle_wsgi_error 4 | from pulsar.api import Http404 5 | 6 | 7 | class Handler: 8 | 9 | def __init__(self, app, router): 10 | self.app = app 11 | self.router = router 12 | self.wait = wait if app.green_pool else pass_through 13 | 14 | def __call__(self, environ, start_response): 15 | pool = self.app.green_pool 16 | if pool and not pool.in_green_worker: 17 | return pool.submit(self._call, pool, environ, start_response) 18 | else: 19 | return self._call(pool, environ, start_response) 20 | 21 | def _call(self, pool, environ, start_response): 22 | if pool: 23 | wsgi_input = environ['wsgi.input'] 24 | if wsgi_input and not isinstance(wsgi_input, GreenStream): 25 | environ['wsgi.input'] = GreenStream(wsgi_input) 26 | 27 | request = wsgi_request(environ) 28 | path = request.path 29 | try: 30 | self.app.on_request(data=request) 31 | hnd = self.router.resolve(path, request.method) 32 | if hnd: 33 | request.cache.set('app_handler', hnd.router) 34 | request.cache.set('urlargs', hnd.urlargs) 35 | response = self.wait(hnd.handler(request)) 36 | else: 37 | raise Http404 38 | 39 | except Exception as exc: 40 | response = handle_wsgi_error(environ, exc) 41 | 42 | try: 43 | self.app.fire_event('on_response', data=(request, response)) 44 | except Exception as exc: 45 | response = handle_wsgi_error(environ, exc) 46 | 47 | response.start(environ, start_response) 48 | return response 49 | 50 | 51 | def pass_through(response): 52 | return response 53 | -------------------------------------------------------------------------------- /lux/core/mail.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | Message = namedtuple('Message', 'sender to subject message html_message') 5 | 6 | 7 | class EmailBackend: 8 | 9 | def __init__(self, app): 10 | self.app = app 11 | 12 | def send_mail(self, sender=None, to=None, subject=None, message=None, 13 | html_message=None): 14 | if not sender: 15 | sender = self.app.config['EMAIL_DEFAULT_FROM'] 16 | if not to: 17 | to = self.app.config['EMAIL_DEFAULT_TO'] 18 | if sender and to: 19 | message = self.message(sender, to, subject, message or '', 20 | html_message) 21 | return self.send_mails([message]) 22 | else: 23 | return 0 24 | 25 | def message(self, sender, to, subject, message, html_message): 26 | return Message(sender, to, subject, message, html_message) 27 | 28 | def send_mails(self, messages): 29 | return len(messages) 30 | 31 | 32 | class LocalMemory(EmailBackend): 33 | 34 | def send_mails(self, messages): 35 | if not hasattr(self.app, '_outbox'): 36 | self.app._outbox = [] 37 | self.app._outbox.extend(messages) 38 | return len(messages) 39 | -------------------------------------------------------------------------------- /lux/core/user.py: -------------------------------------------------------------------------------- 1 | from pulsar.utils.structures import AttributeDictionary 2 | 3 | 4 | class UserMixin: 5 | email = None 6 | 7 | def is_superuser(self): 8 | return False 9 | 10 | def is_authenticated(self): 11 | return True 12 | 13 | def is_active(self): 14 | return False 15 | 16 | def is_anonymous(self): 17 | return False 18 | 19 | def get_id(self): 20 | raise NotImplementedError 21 | 22 | def todict(self): 23 | return self.__dict__.copy() 24 | 25 | def email_user(self, app, subject, body, sender=None): 26 | backend = app.email_backend 27 | backend.send_mail(sender, self.email, subject, body) 28 | 29 | 30 | class Anonymous(AttributeDictionary, UserMixin): 31 | 32 | def __repr__(self): 33 | return self.__class__.__name__.lower() 34 | 35 | def is_authenticated(self): 36 | return False 37 | 38 | def is_anonymous(self): 39 | return True 40 | 41 | def get_id(self): 42 | return 0 43 | 44 | 45 | class ServiceUser(Anonymous): 46 | 47 | def is_superuser(self): 48 | return self.is_authenticated() 49 | 50 | def is_authenticated(self): 51 | return bool(self.token) 52 | 53 | def is_anonymous(self): 54 | return True 55 | 56 | def is_active(self): 57 | return False 58 | 59 | 60 | class User(AttributeDictionary, UserMixin): 61 | '''A dictionary-based user 62 | 63 | Used by the :class:`.ApiSessionBackend` 64 | ''' 65 | def is_superuser(self): 66 | return bool(self.superuser) 67 | 68 | def is_active(self): 69 | return True 70 | 71 | def __str__(self): 72 | return self.username or self.email or 'user' 73 | -------------------------------------------------------------------------------- /lux/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/__init__.py -------------------------------------------------------------------------------- /lux/ext/auth/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/auth/commands/__init__.py -------------------------------------------------------------------------------- /lux/ext/auth/commands/create_token.py: -------------------------------------------------------------------------------- 1 | """Management utility to create a token for a user""" 2 | from lux.core import LuxCommand, Setting, CommandError 3 | from lux.utils.crypt import as_hex 4 | 5 | from .create_superuser import get_def_username 6 | 7 | 8 | class Command(LuxCommand): 9 | help = 'Create a superuser.' 10 | option_list = ( 11 | Setting('username', ('--username',), desc='Username'), 12 | ) 13 | 14 | def run(self, options, interactive=False): 15 | username = options.username 16 | if not username: 17 | interactive = True 18 | users = self.app.models['users'] 19 | tokens = self.app.models['tokens'] 20 | with users.begin_session() as session: 21 | if interactive: # pragma nocover 22 | def_username = get_def_username(session) 23 | input_msg = 'Username' 24 | if def_username: 25 | input_msg += ' (Leave blank to use %s)' % def_username 26 | while not username: 27 | username = input(input_msg + ': ') 28 | if def_username and username == '': 29 | username = def_username 30 | 31 | user = session.auth.get_user(session, username=username) 32 | if user is None: 33 | raise CommandError('user %s not available' % username) 34 | token = tokens.create_one(session, dict( 35 | user=user, 36 | description='from create token command' 37 | )) 38 | self.write('Token: %s' % as_hex(token.id)) 39 | return token 40 | -------------------------------------------------------------------------------- /lux/ext/auth/rest/__init__.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from pulsar.api import BadRequest, Http401 4 | 5 | from lux.models import Schema, fields 6 | 7 | 8 | def ensure_service_user(request, errorCls=None): 9 | # user must be anonymous 10 | if not request.cache.user.is_anonymous(): 11 | raise (errorCls or BadRequest) 12 | return 13 | # the service user must be authenticated 14 | if not request.cache.user.is_authenticated(): 15 | raise Http401('Token') 16 | 17 | 18 | def id_or_field(field, id=None, **kwargs): 19 | if id: 20 | try: 21 | UUID(id) 22 | except ValueError: 23 | kwargs[field] = id 24 | else: 25 | kwargs['id'] = id 26 | return kwargs 27 | 28 | 29 | class IdSchema(Schema): 30 | id = fields.UUID(required=True, description='unique id') 31 | -------------------------------------------------------------------------------- /lux/ext/auth/rest/groups.py: -------------------------------------------------------------------------------- 1 | from lux.models import Schema, fields, UniqueField 2 | from lux.ext.rest import RestRouter, route 3 | from lux.ext.odm import Model 4 | 5 | 6 | URI = 'groups' 7 | 8 | 9 | class GroupSchema(Schema): 10 | name = fields.Slug(validate=UniqueField(URI), required=True) 11 | permissions = fields.List( 12 | fields.Nested('PermissionSchema'), 13 | description='List of permissions for this group' 14 | ) 15 | 16 | class Meta: 17 | model = URI 18 | exclude = ('users',) 19 | 20 | 21 | class GroupPathSchema(Schema): 22 | id = fields.String(required=True, 23 | description='group unique ID or name') 24 | 25 | 26 | class GroupModel(Model): 27 | 28 | def __call__(self, data, session): 29 | data.pop('permissions', None) 30 | group = super().__call__(data, session) 31 | return group 32 | 33 | 34 | class GroupCRUD(RestRouter): 35 | """ 36 | --- 37 | summary: Group path 38 | description: provide operations for groups 39 | tags: 40 | - group 41 | """ 42 | model = GroupModel(URI, GroupSchema) 43 | 44 | @route(default_response_schema=[GroupSchema], 45 | responses=(401, 403)) 46 | def get(self, request): 47 | """ 48 | --- 49 | summary: get a list of groups 50 | responses: 51 | 200: 52 | description: a list of groups 53 | """ 54 | return self.model.get_list_response(request) 55 | 56 | @route(default_response=201, 57 | default_response_schema=GroupSchema, 58 | body_schema=GroupSchema, 59 | responses=(401, 403)) 60 | def post(self, request, **kw): 61 | """ 62 | --- 63 | summary: Create a new group 64 | """ 65 | return self.model.create_response(request, **kw) 66 | 67 | @route(GroupPathSchema, 68 | default_response_schema=GroupSchema, 69 | responses=(401, 403, 404)) 70 | def get_one(self, request, **kw): 71 | """ 72 | --- 73 | summary: get a group by its id or name 74 | responses: 75 | 200: 76 | description: the group matching the id or name 77 | """ 78 | return self.model.get_one_response(request, **kw) 79 | 80 | @route(GroupPathSchema, 81 | default_response_schema=GroupSchema, 82 | body_schema=GroupSchema, 83 | responses=(401, 403, 404, 422)) 84 | def patch_one(self, request, **kw): 85 | """ 86 | --- 87 | summary: get a group by its id or name 88 | responses: 89 | 200: 90 | description: the group matching the id or name 91 | """ 92 | return self.model.update_one_response(request, **kw) 93 | -------------------------------------------------------------------------------- /lux/ext/auth/rest/mailinglist.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import Http401, Http404 2 | 3 | from lux.models import Schema, fields, ValidationError 4 | from lux.ext.rest import RestRouter 5 | from lux.ext.odm import Model 6 | 7 | 8 | class MailingListSchema(Schema): 9 | email = fields.Email(label='Your email address') 10 | topic = fields.String(description='Mailing list topic') 11 | 12 | def post_load(self, email): 13 | user = self.request.cache.user 14 | if not email or user.email: 15 | raise ValidationError('required') 16 | return email 17 | 18 | def clean(self): 19 | user = self.request.cache.user 20 | model = self.request.app.models['mailinglist'] 21 | topic = self.cleaned_data['topic'] 22 | with model.session(self.request) as session: 23 | query = model.get_query(session) 24 | if user.is_anonymous(): 25 | if not user.is_authenticated(): 26 | raise Http401('Token') 27 | query.filter( 28 | email=self.cleaned_data['email'], 29 | topic=topic 30 | ) 31 | else: 32 | self.cleaned_data['user'] = user 33 | query.filter( 34 | user=user, 35 | topic=topic 36 | ) 37 | try: 38 | query.one() 39 | except Http404: 40 | pass 41 | else: 42 | raise ValidationError('Already subscribed') 43 | 44 | 45 | class MailingListCRUD(RestRouter): 46 | model = Model( 47 | 'mailinglists', 48 | model_schema=MailingListSchema 49 | ) 50 | -------------------------------------------------------------------------------- /lux/ext/auth/rest/permissions.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from lux.ext.rest import RestRouter, route 4 | from lux.ext.odm import Model 5 | from lux.models import Schema, fields 6 | 7 | from ..permissions import PolicySchema 8 | 9 | 10 | class PermissionSchema(Schema): 11 | model = 'permissions' 12 | name = fields.String(maxLength=60, required=True) 13 | description = fields.String(rows=2, html_type='textarea') 14 | policy = fields.List( 15 | fields.Nested(PolicySchema), 16 | ace=json.dumps({'mode': 'json'}) 17 | ) 18 | 19 | class Meta: 20 | model = 'permissions' 21 | 22 | 23 | class PermissionCRUD(RestRouter): 24 | """ 25 | --- 26 | summary: Permissions 27 | tags: 28 | - permission 29 | """ 30 | model = Model('permissions', PermissionSchema) 31 | 32 | @route(default_response_schema=[PermissionSchema]) 33 | def get(self, request, **kw): 34 | """ 35 | --- 36 | summary: List permissions documents 37 | responses: 38 | 200: 39 | description: List of permissions matching filters 40 | """ 41 | return self.model.get_list_response(request, **kw) 42 | 43 | @route(default_response=201, 44 | body_schema=PermissionSchema, 45 | default_response_schema=[PermissionSchema], 46 | responses=(400, 401, 403, 422)) 47 | def post(self, request, **kw): 48 | """ 49 | --- 50 | summary: Create a new permission document 51 | """ 52 | return self.model.create_response(request, **kw) 53 | 54 | @route('', responses=(400, 401, 403, 404)) 55 | def patch_one(self, request, **kw): 56 | """ 57 | --- 58 | summary: Updated an existing permission document 59 | responses: 60 | 200: 61 | description: Permission document was successfully updated 62 | """ 63 | return self.model.update_response(request, **kw) 64 | 65 | @route('', 66 | default_response=204, 67 | responses=(400, 401, 403, 404)) 68 | def delete(self, request, **kw): 69 | """ 70 | --- 71 | summary: Delete an existing permission document 72 | """ 73 | return self.model.delete_one_response(request, **kw) 74 | -------------------------------------------------------------------------------- /lux/ext/auth/rest/tokens.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from lux.models import Schema, fields 4 | from lux.ext.rest import RestRouter, route 5 | from lux.ext.odm import Model 6 | 7 | 8 | class TokenSchema(Schema): 9 | user = fields.Nested('UserSchema') 10 | 11 | class Meta: 12 | model = 'tokens' 13 | 14 | 15 | class TokenCreateSchema(TokenSchema): 16 | """Create a new Authorization ``Token`` for the authenticated ``User``. 17 | """ 18 | description = fields.String(required=True, minLength=2, maxLength=256) 19 | 20 | 21 | class TokenModel(Model): 22 | 23 | def get_one(self, session, *filters, **kwargs): 24 | query = self.query(session, *filters, **kwargs) 25 | token = query.one() 26 | query.update({'last_access': datetime.utcnow()}, 27 | synchronize_session=False) 28 | return token 29 | 30 | 31 | class TokenCRUD(RestRouter): 32 | """ 33 | --- 34 | summary: Mange user tokens 35 | tags: 36 | - user 37 | - token 38 | """ 39 | model = TokenModel('tokens', TokenSchema) 40 | 41 | @route(default_response_schema=[TokenSchema]) 42 | def get(self, request): 43 | """ 44 | --- 45 | summary: List tokens for a user 46 | responses: 47 | 200: 48 | description: List all user tokens matching query filters 49 | """ 50 | return self.model.get_list_response(request) 51 | 52 | @route(default_response=201, 53 | default_response_schema=TokenSchema, 54 | body_schema=TokenCreateSchema) 55 | def post(self, request): 56 | """ 57 | --- 58 | summary: Create a new token 59 | """ 60 | return self.model.create_response(request) 61 | -------------------------------------------------------------------------------- /lux/ext/auth/rest/users.py: -------------------------------------------------------------------------------- 1 | from lux.ext.rest import RestRouter, route 2 | from lux.models import Schema, fields 3 | 4 | from .user import UserModel, UserSchema, UserUpdateSchema, UserQuerySchema 5 | 6 | 7 | class UserPathSchema(Schema): 8 | id = fields.String(description='user unique ID or username') 9 | 10 | 11 | class UserCRUD(RestRouter): 12 | """ 13 | --- 14 | summary: CRUD operations for users 15 | tags: 16 | - user 17 | """ 18 | model = UserModel("users", UserSchema) 19 | 20 | @route(query_schema=UserQuerySchema, 21 | default_response_schema=[UserSchema], 22 | responses=(401, 403)) 23 | def get(self, request, **kwargs): 24 | """ 25 | --- 26 | summary: List users 27 | responses: 28 | 200: 29 | description: List of users matching filters 30 | """ 31 | return self.model.get_list_response(request, **kwargs) 32 | 33 | @route(body_schema=UserSchema, 34 | default_response=201, 35 | default_response_schema=UserSchema, 36 | responses=(400, 401, 403)) 37 | def post(self, request, **kw): 38 | """ 39 | --- 40 | summary: Create a new user 41 | """ 42 | return self.model.create_response(request, **kw) 43 | 44 | @route(UserPathSchema, 45 | default_response_schema=UserSchema, 46 | responses=(401, 403)) 47 | def get_user(self, request, **kw): 48 | """ 49 | --- 50 | summary: Get a user by its id or username 51 | responses: 52 | 200: 53 | description: The user matching the id or username 54 | """ 55 | return self.model.get_model_response(request, **kw) 56 | 57 | @route(UserPathSchema, 58 | body_schema=UserUpdateSchema, 59 | default_response_schema=UserSchema, 60 | responses=(400, 401, 403)) 61 | def patch_user(self, request, body_schema): 62 | """ 63 | --- 64 | summary: Update a user by its id or username 65 | responses: 66 | 201: 67 | description: The updated user 68 | """ 69 | return self.model.update_one_response(request, body_schema) 70 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/activation_email.txt: -------------------------------------------------------------------------------- 1 | Congratulation, your registration to {{ site_uri }} was successful. 2 | 3 | One last thing to do is to visit {{ register_url }}/{{ auth_key }} to confirm your email address. 4 | The confirmation link is valid for {{ expiration_days }} days. 5 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/activation_email_subject.txt: -------------------------------------------------------------------------------- 1 | Confirm your email on {{ site_uri }} 2 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/activation_message.txt: -------------------------------------------------------------------------------- 1 | We have sent an email to {{ email }}. 2 | Please follow the instructions to activate your account. 3 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/confirmation.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | You have confirmed your email. 7 |
8 |

You can now log in.

9 |
10 |
11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/inactive_user.txt: -------------------------------------------------------------------------------- 1 | Your account is not active yet. You need to confirm your email address via the email link sent from {{ email_from }}. 2 | To receive a new confirmation email click here. 3 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/password_email.txt: -------------------------------------------------------------------------------- 1 | Someone, hopefully you, asked to reset your password at {{ site_uri }}. 2 | 3 | If you would like to continue with the action visit {{ reset_password_url }}/{{ auth_key }} and follow the instructions, otherwise disregard this email. 4 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/password_email_subject.txt: -------------------------------------------------------------------------------- 1 | Password recovery for {{ site_uri }} 2 | -------------------------------------------------------------------------------- /lux/ext/auth/templates/registration/reset-password.html: -------------------------------------------------------------------------------- 1 | {{ html_main }} 2 | -------------------------------------------------------------------------------- /lux/ext/content/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from lux.core import Parameter, LuxExtension 4 | 5 | from .models import ContentModel 6 | from .rest import ContentCRUD 7 | from .cms import CMS, LazyContext 8 | from .github import GithubHook, EventHandler, PullRepo 9 | from .files import content_location 10 | from .views import TemplateRouter 11 | 12 | 13 | __all__ = ['Content', 14 | 'ContentCRUD', 15 | 'CMS', 16 | 'GithubHook', 17 | 'EventHandler', 18 | 'PullRepo', 19 | 'LazyContext'] 20 | 21 | 22 | class Extension(LuxExtension): 23 | _config = [ 24 | Parameter('CONTENT_REPO', None, 25 | 'Directory where content repo is located'), 26 | Parameter('CONTENT_GROUPS', { 27 | "site": { 28 | "path": "*", 29 | "body_template": "home.html" 30 | } 31 | }, 'List of content model configurations'), 32 | Parameter('CONTENT_LOCATION', None, 33 | 'Directory where content is located inside CONTENT_REPO'), 34 | Parameter('HTML_TEMPLATES_URL', 'templates', 35 | 'Base url for serving HTML templates when the default ' 36 | 'content type is text/html. Set to None if not needed.'), 37 | Parameter('GITHUB_HOOK_KEY', None, 38 | 'Secret key for github webhook') 39 | ] 40 | 41 | def on_config(self, app): 42 | self.require(app, 'lux.ext.rest') 43 | 44 | def routes(self, app): 45 | url = app.config['HTML_TEMPLATES_URL'] 46 | if app.config['DEFAULT_CONTENT_TYPE'] == 'text/html' and url: 47 | yield TemplateRouter(url, serve_only=('html', 'txt')) 48 | 49 | def on_loaded(self, app): 50 | if app.config['DEFAULT_CONTENT_TYPE'] == 'text/html': 51 | app.cms = CMS(app) 52 | 53 | def api_sections(self, app): 54 | location = content_location(app) 55 | if not location: 56 | return 57 | 58 | secret = app.config['GITHUB_HOOK_KEY'] 59 | middleware = [] 60 | # 61 | # Add github hook if the secret is specified 62 | if secret: 63 | middleware.append(GithubHook('/refresh-content', 64 | handle_payload=PullRepo(location), 65 | secret=secret)) 66 | middleware.append(ContentCRUD('{0}/', 67 | model=ContentModel(location))) 68 | return middleware 69 | 70 | def get_template_full_path(self, app, name): 71 | repo = content_location(app) 72 | if repo: 73 | return os.path.join(repo, 'templates', name) 74 | else: 75 | return super().get_template_full_path(app, name) 76 | -------------------------------------------------------------------------------- /lux/ext/content/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/content/commands/__init__.py -------------------------------------------------------------------------------- /lux/ext/content/files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from lux.utils.files import skipfile, get_rel_dir 4 | 5 | from lux.core import cached 6 | from .contents import get_reader 7 | 8 | 9 | def content_location(app, *args): 10 | repo = app.config['CONTENT_REPO'] 11 | if repo: 12 | location = app.config['CONTENT_LOCATION'] 13 | if location: 14 | repo = os.path.join(repo, location) 15 | if args: 16 | repo = os.path.join(repo, *args) 17 | return repo 18 | 19 | 20 | @cached 21 | def get_context_files(app): 22 | '''Load static context 23 | ''' 24 | ctx = {} 25 | location = content_location(app, 'context') 26 | if location and os.path.isdir(location): 27 | for dirpath, dirs, filenames in os.walk(location, topdown=False): 28 | if skipfile(os.path.basename(dirpath) or dirpath): 29 | continue 30 | for filename in filenames: 31 | if skipfile(filename): 32 | continue 33 | file_bits = filename.split('.') 34 | bits = [file_bits[0]] 35 | 36 | prefix = get_rel_dir(dirpath, location) 37 | while prefix: 38 | prefix, tail = os.path.split(prefix) 39 | bits.append(tail) 40 | 41 | filename = os.path.join(dirpath, filename) 42 | suffix = get_reader(app, filename).suffix 43 | name = '_'.join(reversed(bits)) 44 | if suffix: 45 | name = '%s_%s' % (suffix, name) 46 | ctx[name] = filename 47 | return ctx 48 | -------------------------------------------------------------------------------- /lux/ext/content/rest.py: -------------------------------------------------------------------------------- 1 | from lux.core import Resource 2 | from lux.ext.rest import RestRouter, route 3 | 4 | 5 | def check_permission_dict(group, action): 6 | return { 7 | 'action': action, 8 | 'args': (group,) 9 | } 10 | 11 | 12 | class ContentCRUD(RestRouter): 13 | """REST API view for content 14 | """ 15 | @route('links') 16 | def get_links(self, request): 17 | data = self.get_list( 18 | request, 19 | load_only=('title', 'description', 'slug', 'url'), 20 | sortby=['title:asc', 'order:desc'], 21 | check_permission=Resource.rest(request, 'read', 22 | self.model.fields(), 23 | pop=1, list=True), 24 | **{'order:gt': 0, 'priority:gt': 0} 25 | ) 26 | return self.json_response(request, data) 27 | 28 | @route('', 29 | method=('get', 'post', 'put', 'delete', 'head', 'options')) 30 | def read_update_delete(self, request): 31 | return super().read_update_delete(request) 32 | -------------------------------------------------------------------------------- /lux/ext/content/urlwrappers.py: -------------------------------------------------------------------------------- 1 | from pulsar.utils.slugify import slugify 2 | 3 | 4 | SEP = ', ' 5 | 6 | 7 | def identity(x, cfg): 8 | return x 9 | 10 | 11 | class Processor: 12 | 13 | def __init__(self, name, processor=None): 14 | self.name = slugify(name, separator='_') 15 | self._processor = processor 16 | 17 | def __call__(self, values=None, cfg=None): 18 | if values: 19 | if self._processor: 20 | return self._processor(values[-1], cfg) 21 | else: 22 | return values[-1] 23 | 24 | 25 | class MultiValue(Processor): 26 | 27 | def __init__(self, name=None, cls=None): 28 | self.name = slugify(name, separator='_') if name else '' 29 | self.cls = cls or identity 30 | 31 | def __call__(self, values=None, cfg=None): 32 | all = [] 33 | if values: 34 | for x in values: 35 | all.extend((self.cls(v.strip(), cfg) for v in x.split(','))) 36 | return all 37 | 38 | 39 | class URLWrapper: 40 | 41 | def __init__(self, name, settings): 42 | self.settings = settings 43 | self.name = name 44 | 45 | @property 46 | def name(self): 47 | return self._name 48 | 49 | @name.setter 50 | def name(self, name): 51 | self._name = name 52 | self.slug = slugify(name) 53 | 54 | def __call__(self, app): 55 | # TODO 56 | return self.name 57 | 58 | def __hash__(self): 59 | return hash(self.slug) 60 | 61 | def _key(self): 62 | return self.slug 63 | 64 | def __str__(self): 65 | return self.name 66 | 67 | def __repr__(self): 68 | return '<%s %s>' % (type(self).__name__, self) 69 | 70 | 71 | class Category(URLWrapper): 72 | pass 73 | 74 | 75 | class Tag(URLWrapper): 76 | pass 77 | 78 | 79 | class Author(URLWrapper): 80 | pass 81 | -------------------------------------------------------------------------------- /lux/ext/content/views.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.wsgi import MediaRouter, file_response 2 | 3 | 4 | class TemplateRouter(MediaRouter): 5 | response_content_types = ('text/plain', 'text/html') 6 | 7 | def filesystem_path(self, request): 8 | path = request.app.template_full_path(request.urlargs['path']) 9 | return path or '' 10 | 11 | def serve_file(self, request, fullpath, status_code=None): 12 | # Prefers text/plain response if possible 13 | content_type = None 14 | if 'text/plain' in request.content_types: 15 | content_type = 'text/plain' 16 | return file_response(request, fullpath, status_code=status_code, 17 | cache_control=self.cache_control, 18 | content_type=content_type) 19 | -------------------------------------------------------------------------------- /lux/ext/oauth/amazon/__init__.py: -------------------------------------------------------------------------------- 1 | from ..oauth import OAuth2 2 | 3 | 4 | class Amazon(OAuth2): 5 | auth_uri = 'https://www.dropbox.com/1/oauth2/authorize' 6 | token_uri = 'https://api.dropbox.com/1/oauth2/token' 7 | -------------------------------------------------------------------------------- /lux/ext/oauth/dropbox/__init__.py: -------------------------------------------------------------------------------- 1 | from ..oauth import OAuth2 2 | 3 | 4 | class Dropbox(OAuth2): 5 | auth_uri = 'https://www.dropbox.com/1/oauth2/authorize' 6 | token_uri = 'https://api.dropbox.com/1/oauth2/token' 7 | -------------------------------------------------------------------------------- /lux/ext/oauth/facebook/__init__.py: -------------------------------------------------------------------------------- 1 | from ..oauth import OAuth2 2 | 3 | 4 | class Facebook(OAuth2): 5 | namespace = 'fb' 6 | auth_uri = 'https://www.facebook.com/dialog/oauth' 7 | token_uri = 'https://graph.facebook.com/oauth/access_token' 8 | default_scope = ['public_profile', 'email'] 9 | fa = 'facebook-square' 10 | 11 | def ogp_add_tags(self, request, ogp): 12 | '''Add meta tags to an HTML5 document 13 | ''' 14 | key = self.config.get('key') 15 | if key: 16 | ogp.prefixes.append('fb: http://ogp.me/ns/fb#') 17 | ogp.doc.head.add_meta(property='fb:app_id', content=key) 18 | -------------------------------------------------------------------------------- /lux/ext/oauth/github/__init__.py: -------------------------------------------------------------------------------- 1 | from ..oauth import OAuth2, OAuth2Api 2 | 3 | 4 | class Api(OAuth2Api): 5 | url = 'https://api.github.com' 6 | 7 | def user(self): 8 | response = self.http.get('%s/user' % self.url) 9 | response.raise_for_status() 10 | return response.json() 11 | 12 | 13 | class Github(OAuth2): 14 | '''Github api 15 | 16 | https://developer.github.com/v3/ 17 | ''' 18 | auth_uri = 'https://github.com/login/oauth/authorize' 19 | token_uri = 'https://github.com/login/oauth/access_token' 20 | fa = 'github-square' 21 | username_field = 'login' 22 | 23 | api = Api 24 | 25 | def username(self, user_data): 26 | return user_data.get('login') 27 | 28 | def firstname(self, user_data): 29 | return user_data.get('name') 30 | 31 | def lastname(self, user_data): 32 | return user_data.get('lastName') 33 | -------------------------------------------------------------------------------- /lux/ext/oauth/linkedin/__init__.py: -------------------------------------------------------------------------------- 1 | from pulsar.utils.slugify import slugify 2 | 3 | from ..oauth import OAuth2, OAuth2Api 4 | 5 | fields = ('firstName,lastName,email-address,formatted-name,' 6 | 'headline,location,industry') 7 | 8 | 9 | class Api(OAuth2Api): 10 | url = 'https://api.linkedin.com/v1/people' 11 | headers = [('content-type', 'application/json'), 12 | ('x-li-format', 'json')] 13 | format = {'format': 'json'} 14 | username_field = 'username' 15 | email_field = 'email' 16 | firstname_field = 'firstName' 17 | lastname_field = 'lastName' 18 | 19 | def user(self): 20 | url = '%s/~:(%s)' % (self.url, fields) 21 | response = self.http.get(url, data=self.format, headers=self.headers) 22 | response.raise_for_status() 23 | return response.json() 24 | 25 | 26 | class Linkedin(OAuth2): 27 | '''LinkedIn api 28 | 29 | https://developer.linkedin.com/apis 30 | ''' 31 | auth_uri = 'https://www.linkedin.com/uas/oauth2/authorization' 32 | token_uri = 'https://www.linkedin.com/uas/oauth2/accessToken' 33 | fa = 'linkedin-square' 34 | api = Api 35 | 36 | def username(self, user_data): 37 | return slugify('%s.%s' % (user_data['firstName'], 38 | user_data['lastName'])) 39 | 40 | def firstname(self, user_data): 41 | return user_data['firstName'] 42 | 43 | def lastname(self, user_data): 44 | return user_data['lastName'] 45 | 46 | def email(self, user_data): 47 | return user_data['emailAddress'] 48 | -------------------------------------------------------------------------------- /lux/ext/oauth/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import relationship, backref 2 | from sqlalchemy import Column, String, ForeignKey, DateTime 3 | 4 | from odm.types import JSONType, UUIDType 5 | from odm.mapper import declared_attr 6 | 7 | import lux.ext.auth.models as auth 8 | 9 | 10 | dbModel = auth.dbModel 11 | 12 | 13 | class User(auth.User): 14 | """Override user with oauth dictionary 15 | """ 16 | oauth = Column(JSONType) 17 | 18 | 19 | class AccessToken(dbModel): 20 | """ 21 | An AccessToken instance represents the actual access token to 22 | access user's resources, as in :rfc:`5`. 23 | Fields: 24 | * :attr:`user_id` The user id 25 | * :attr:`token` Access token 26 | * :attr:`provider` access token provider 27 | * :attr:`expires` Date and time of token expiration, in DateTime format 28 | * :attr:`scope` Allowed scopes 29 | * :attr:`type` access token type 30 | """ 31 | token = Column(String(255), primary_key=True) 32 | provider = Column(String(12), primary_key=True) 33 | expires = Column(DateTime) 34 | scope = Column(JSONType) 35 | type = Column(String(12)) 36 | 37 | @declared_attr 38 | def user_id(cls): 39 | return Column(UUIDType, ForeignKey('user.id', ondelete='CASCADE')) 40 | 41 | @declared_attr 42 | def user(cls): 43 | return relationship( 44 | 'User', 45 | backref=backref("access_tokens", cascade="all, delete-orphan")) 46 | -------------------------------------------------------------------------------- /lux/ext/oauth/twitter/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | ''' 3 | from ..oauth import OAuth1 4 | 5 | 6 | twitter_cards = {} 7 | 8 | 9 | def twitter_card(cls): 10 | type = cls.__name__.lower() 11 | twitter_cards[type] = cls() 12 | return cls 13 | 14 | 15 | class Twitter(OAuth1): 16 | auth_uri = 'https://api.twitter.com/oauth/authorize' 17 | request_token_uri = 'https://api.twitter.com/oauth/request_token' 18 | token_uri = 'https://api.twitter.com/oauth/access_token' 19 | fa = 'twitter-square' 20 | 21 | def on_html_document(self, request, doc): 22 | card = 'summary' 23 | if 'card' in self.config: 24 | card = self.config['card'] 25 | doc.meta.set('twitter:card', card) 26 | 27 | def ogp_add_tags(self, request, ogp): 28 | '''Add meta tags to an HTML5 document 29 | ''' 30 | doc = ogp.doc 31 | card = 'summary' 32 | site = self.config.get('site') 33 | if 'card' in self.config: 34 | card = self.config['card'] 35 | twitter = doc.meta.namespaces.get('twitter') 36 | if twitter: 37 | card = twitter.get('card', card) 38 | if card and site: 39 | Card = twitter_cards.get(card) 40 | if Card: 41 | doc.head.add_meta(name='twitter:card', content=card) 42 | doc.head.add_meta(name='twitter:site', content=site) 43 | Card(doc) 44 | else: 45 | request.logger.warning( 46 | 'Twitter card not defined but card is available') 47 | 48 | 49 | class TwitterCard: 50 | prefix = 'twitter' 51 | default_meta_key = 'name' 52 | 53 | def set(self, doc, key, array=False): 54 | twitter = doc.meta.namespaces.get(self.prefix) 55 | if twitter and key in twitter: 56 | value = twitter[key] 57 | else: 58 | # get the value for the og meta 59 | value = doc.head.get_meta('og:%s' % key, 'property') 60 | if value: 61 | doc.head.add_meta(name='twitter:%s' % key, content=value) 62 | 63 | 64 | @twitter_card 65 | class Summary(TwitterCard): 66 | 67 | def __call__(self, doc): 68 | self.set(doc, 'title') 69 | self.set(doc, 'description') 70 | self.set(doc, 'image') 71 | 72 | 73 | @twitter_card 74 | class Summary_Large_Image(Summary): 75 | pass 76 | 77 | 78 | @twitter_card 79 | class Photo(Summary): 80 | pass 81 | -------------------------------------------------------------------------------- /lux/ext/odm/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/odm/commands/__init__.py -------------------------------------------------------------------------------- /lux/ext/odm/commands/template/lux/alembic.ini.mako: -------------------------------------------------------------------------------- 1 | # Lux configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = ${script_location} 6 | -------------------------------------------------------------------------------- /lux/ext/odm/commands/template/lux/script.py.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | import re 3 | 4 | %>"""${message} 5 | 6 | Revision ID: ${up_revision} 7 | Revises: ${down_revision | comma,n} 8 | Create Date: ${create_date} 9 | 10 | """ 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | from alembic import op 19 | import sqlalchemy as sa 20 | ${imports if imports else ""} 21 | 22 | import odm 23 | import odm.types 24 | 25 | 26 | def upgrade(engine_name): 27 | globals()["upgrade_%s" % engine_name]() 28 | 29 | 30 | def downgrade(engine_name): 31 | globals()["downgrade_%s" % engine_name]() 32 | 33 | <% 34 | db_names = config.get_main_option("databases") 35 | %> 36 | 37 | ## generate an "upgrade_() / downgrade_()" function 38 | ## for each database name in the ini file. 39 | 40 | % for db_name in re.split(r',\s*', db_names): 41 | 42 | def upgrade_${db_name}(): 43 | ${context.get("%s_upgrades" % db_name, "pass")} 44 | 45 | 46 | def downgrade_${db_name}(): 47 | ${context.get("%s_downgrades" % db_name, "pass")} 48 | 49 | % endfor 50 | -------------------------------------------------------------------------------- /lux/ext/readme.md: -------------------------------------------------------------------------------- 1 | # Writing Extensions 2 | 3 | Extension are implemented by subclassing the ``LuxExtension`` class:: 4 | ```python 5 | from lux.core import LuxExtension 6 | 7 | class Extension(LuxExtension): 8 | # Optional version number 9 | _version = '0.1.0' 10 | 11 | def middleware(self, app): 12 | ... 13 | ``` 14 | 15 | The ``middleware`` method is called once only by the application and it 16 | must return an iterable over [WSGI][] middleware or ``None``. 17 | 18 | 19 | ## Events 20 | 21 | An extension can register several callbacks which are invoked at different 22 | points during the application life-span. These callbacks receive as 23 | first positional argument, the application instance running the web site 24 | and are implemented by adding some of the following methods to your 25 | extension class. 26 | 27 | 28 | ### on_config(*app*) 29 | 30 | This is the first event to be fired. It is executed once only after the 31 | ```app.config``` dictionary has been loaded from 32 | the setting file. This is a chance to perform post processing on 33 | parameters. 34 | 35 | 36 | ### on_loaded(*app*) 37 | 38 | Called once only when all extensions have loaded their 39 | middleware into the WSGI handler. 40 | A chance to add additional middleware or perform 41 | any sort of post-processing on the wsgi application ``handler``. 42 | 43 | 44 | ### on_start(*app*, *server*) 45 | 46 | Called once only just before the wsgi ``server`` is about to start serving 47 | the ``app``. 48 | 49 | 50 | ### on_html_document(*app*, *request*, *doc*) 51 | 52 | Called the first time the ``request.html_document`` attribute is accessed. 53 | A chance to add static data or any other Html specific information. 54 | 55 | 56 | ### on_form(*app*, *form*) 57 | 58 | 59 | ### on_close(*app*) 60 | 61 | 62 | [WSGI]: http://www.python.org/dev/peps/pep-3333/ 63 | -------------------------------------------------------------------------------- /lux/ext/rest/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/rest/commands/__init__.py -------------------------------------------------------------------------------- /lux/ext/rest/commands/apispec.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from pulsar.utils.slugify import slugify 4 | 5 | from lux.core import LuxCommand 6 | 7 | 8 | class Command(LuxCommand): 9 | help = "print the api spec document." 10 | 11 | def run(self, options, **params): 12 | api_client = self.app.api() 13 | for api in self.app.apis: 14 | res = api_client.get(api.spec_path) 15 | filename = '%s.json' % slugify(api.spec_path) 16 | with open(filename, 'w') as fp: 17 | json.dump(res.json(), fp, indent=4) 18 | self.logger.info('Saved %s', filename) 19 | -------------------------------------------------------------------------------- /lux/ext/rest/cors.py: -------------------------------------------------------------------------------- 1 | # Cross-Origin Resource Sharing header 2 | CORS = 'Access-Control-Allow-Origin' 3 | 4 | 5 | class cors: 6 | 7 | def __init__(self, methods): 8 | self.methods = methods 9 | 10 | def __call__(self, request): 11 | request_headers = request.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') 12 | headers = request.response.headers 13 | origin = request.get('HTTP_ORIGIN', '*') 14 | 15 | if origin == 'null': 16 | origin = '*' 17 | 18 | headers[CORS] = origin 19 | if request_headers: 20 | headers['Access-Control-Allow-Headers'] = request_headers 21 | headers['Access-Control-Allow-Methods'] = ', '.join(self.methods) 22 | headers['content-length'] = '0' 23 | return request.response 24 | -------------------------------------------------------------------------------- /lux/ext/rest/exc.py: -------------------------------------------------------------------------------- 1 | from lux.models import Schema, fields 2 | 3 | 4 | class ErrorSchema(Schema): 5 | resource = fields.String() 6 | field = fields.String() 7 | code = fields.String() 8 | 9 | 10 | class ErrorMessageSchema(Schema): 11 | message = fields.String(required=True) 12 | errors = fields.List(fields.Nested(ErrorSchema())) 13 | -------------------------------------------------------------------------------- /lux/ext/rest/openapi.py: -------------------------------------------------------------------------------- 1 | from lux.models import Schema, fields 2 | 3 | from .rest import RestRouter 4 | from .route import route 5 | 6 | 7 | default_plugins = ['lux.openapi.ext.lux'] 8 | 9 | 10 | class APISchema(Schema): 11 | BASE_PATH = fields.String(required=True) 12 | TITLE = fields.String(required=True) 13 | DESCRIPTION = fields.String() 14 | VERSION = fields.String(default='0.1.0') 15 | SPEC_PLUGINS = fields.List(fields.String(), default=default_plugins) 16 | SPEC_PATH = fields.String(default='spec', 17 | description='path of api specification document') 18 | MODEL = fields.String(default='*') 19 | CORS = fields.Boolean(default=True) 20 | 21 | 22 | api_schema = APISchema() 23 | 24 | 25 | class Specification(RestRouter): 26 | api = None 27 | 28 | @route() 29 | def get(self, request): 30 | """ 31 | --- 32 | summary: OpenAPI specification document 33 | responses: 34 | 200: 35 | description: The OpenAPI specification document 36 | """ 37 | if not self.api: 38 | pass 39 | spec = self.api.spec_dict() 40 | spec['servers'] = [ 41 | dict( 42 | url='%s://%s' % (request.scheme, request.get_host()), 43 | description="default server" 44 | ) 45 | ] 46 | return request.json_response(spec) 47 | -------------------------------------------------------------------------------- /lux/ext/rest/rest.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from pulsar.api import Http404 3 | 4 | from lux.core import JsonRouter 5 | from lux.models import Model 6 | from lux.openapi import METHODS 7 | 8 | 9 | REST_CONTENT_TYPES = ['application/json'] 10 | CREATE_MODEL_ERROR_MSG = 'Could not create model' 11 | 12 | 13 | class RestRoot(JsonRouter): 14 | '''Api Root 15 | 16 | Provide a get method for displaying a dictionary of api names - api urls 17 | key - value pairs 18 | ''' 19 | response_content_types = REST_CONTENT_TYPES 20 | 21 | def apis(self, request): 22 | routes = {} 23 | for router in self.routes: 24 | url = '%s%s' % (request.absolute_uri('/'), router.route.rule) 25 | if isinstance(router, RestRouter) and router.model: 26 | routes[router.model.uri] = url 27 | else: 28 | routes[router.name] = url 29 | return routes 30 | 31 | def get(self, request): 32 | apis = self.apis(request) 33 | data = OrderedDict(((name, apis[name]) for name in sorted(apis))) 34 | return request.json_response(data) 35 | 36 | 37 | class Rest404(JsonRouter): 38 | response_content_types = REST_CONTENT_TYPES 39 | 40 | def get(self, request): 41 | raise Http404 42 | 43 | 44 | class RestRouter(JsonRouter): 45 | '''A mixin to be used in conjunction with Routers, usually 46 | as the first class in the multi-inheritance declaration 47 | ''' 48 | response_content_types = REST_CONTENT_TYPES 49 | 50 | def __init__(self, *args, **kwargs): 51 | url = None 52 | if args: 53 | url_or_model, args = args[0], args[1:] 54 | if isinstance(url_or_model, Model): 55 | self.model = url_or_model 56 | else: 57 | url = url_or_model 58 | 59 | if not self.model: 60 | self.model = kwargs.pop('model', None) 61 | 62 | if self.model: 63 | if url is None: 64 | url = self.model.uri 65 | else: 66 | url = url.format(self.model.uri) 67 | 68 | rule_methods = {} 69 | for name, info in self.rule_methods.items(): 70 | openapi = info.parameters.get('openapi') 71 | # don't consider new routes the standard methods, 72 | # they are already dealt with 73 | if openapi and name in METHODS: 74 | continue 75 | rule_methods[name] = info 76 | self.rule_methods = rule_methods 77 | super().__init__(url, *args, **kwargs) 78 | -------------------------------------------------------------------------------- /lux/ext/rest/route.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from pulsar.api import ImproperlyConfigured 4 | from pulsar.apps import wsgi 5 | 6 | from lux.models import Schema 7 | from lux.openapi import OperationInfo 8 | from lux.utils.data import compact_dict 9 | 10 | 11 | class route(wsgi.route): 12 | """Extend pulsar wsgi route decorator for openapi information 13 | 14 | It adds the openapi namedtuple to the route parameters dictionary 15 | """ 16 | def __init__(self, rule=None, body_schema=None, path_schema=None, 17 | query_schema=None, header_schema=None, default_response=200, 18 | default_response_schema=None, 19 | responses=None, **kwargs): 20 | if isinstance(rule, type(Schema)): 21 | rule = rule() 22 | if isinstance(rule, Schema): 23 | if path_schema: 24 | raise ImproperlyConfigured( 25 | 'both rule and path_schema are provided as schema' 26 | ) 27 | path_schema = rule 28 | rule = path_schema.rule() 29 | kwargs['openapi'] = OperationInfo( 30 | path=path_schema, 31 | body=body_schema, 32 | query=query_schema, 33 | header=header_schema, 34 | responses=responses, 35 | default_response=default_response, 36 | default_response_schema=default_response_schema 37 | ) 38 | super().__init__(rule, **kwargs) 39 | 40 | def __call__(self, method): 41 | api = self.parameters['openapi'] 42 | 43 | if api.body or api.responses[api.default_response]: 44 | 45 | # the callable must accept the schema as second parameter 46 | @wraps(method) 47 | def _(router, request): 48 | return method(router, request, **compact_dict( 49 | body_schema=api.body, 50 | query_schema=api.query, 51 | schema=api.schema 52 | )) 53 | 54 | return super().__call__(_) 55 | 56 | return super().__call__(method) 57 | -------------------------------------------------------------------------------- /lux/ext/sessions/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/sessions/commands/__init__.py -------------------------------------------------------------------------------- /lux/ext/sessions/commands/flush_sessions.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxCommand, Setting 2 | from lux.extensions.rest import session_backend 3 | 4 | 5 | class Command(LuxCommand): 6 | help = "Clear Sessions" 7 | option_list = ( 8 | Setting('app_name', 9 | nargs='?', 10 | desc=('Optional app name. If omitted the default ' 11 | 'application name is used (APP_NAME)')), 12 | ) 13 | 14 | def run(self, options, **params): 15 | result = session_backend(self.app).clear(options.app_name) 16 | self.write('Clear %d sessions' % result) 17 | return result 18 | -------------------------------------------------------------------------------- /lux/ext/sessions/error.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from pulsar.api import HttpException 4 | 5 | 6 | def schema_http_exception(request, error): 7 | if isinstance(error, HttpException): 8 | try: 9 | data = json.loads(error.args[0]) 10 | except json.JSONDecodeError: 11 | pass 12 | else: 13 | request.response.status_code = error.status 14 | return data 15 | 16 | return dict(error=str(error)) 17 | -------------------------------------------------------------------------------- /lux/ext/sitemap/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/sitemap/commands/__init__.py -------------------------------------------------------------------------------- /lux/ext/sitemap/commands/ping_google.py: -------------------------------------------------------------------------------- 1 | from pulsar.apps.wsgi import Router 2 | 3 | from lux.core import LuxCommand 4 | from .. import Sitemap, ping_google 5 | 6 | 7 | class Command(LuxCommand): 8 | 9 | help = "Alerts Google that the sitemap at has been updated" 10 | 11 | def run(self, options): 12 | for router in self.app.handler.middleware: 13 | if isinstance(router, Router): 14 | self.ping(router) 15 | 16 | def ping(self, router): 17 | if isinstance(router, Sitemap): 18 | url = self.app.site_url(router.route.path) 19 | self.write('Pinging google to update "%s"' % url) 20 | response = ping_google(url) 21 | if response.code == 200: 22 | data = response.read() 23 | self.write(data) 24 | else: 25 | self.write('Error') 26 | else: 27 | for route in router.routes: 28 | self.ping(route) 29 | -------------------------------------------------------------------------------- /lux/ext/smtp/views.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import MethodNotAllowed, as_coroutine 2 | 3 | from lux.core import WebFormRouter 4 | from lux.models import Schema, fields 5 | 6 | 7 | class ContactSchema(Schema): 8 | """ 9 | The base contact form class from which all contact form classes 10 | should inherit. 11 | """ 12 | name = fields.String(maxLength=100, label='Your name') 13 | email = fields.Email(label='Your email address') 14 | body = fields.String(label='Your message', rows=10) 15 | 16 | 17 | class ContactRouter(WebFormRouter): 18 | form = 'contact' 19 | 20 | async def post(self, request): 21 | form_class = self.get_form_class(request) 22 | if not form_class: 23 | raise MethodNotAllowed 24 | 25 | data, _ = await as_coroutine(request.data_and_files()) 26 | form = form_class(request, data=data) 27 | if form.is_valid(): 28 | email = request.app.email_backend 29 | responses = request.app.config['EMAIL_ENQUIRY_RESPONSE'] or () 30 | context = form.cleaned_data 31 | app = request.app 32 | engine = app.template_engine() 33 | 34 | for cfg in responses: 35 | sender = engine(cfg.get('sender', ''), context) 36 | to = engine(cfg.get('to', ''), context) 37 | subject = engine(cfg.get('subject', ''), context) 38 | html_message = None 39 | message = None 40 | if 'message-content' in cfg: 41 | html_message = await self.html_content( 42 | request, cfg['message-content'], context) 43 | else: 44 | message = engine(cfg.get('message', ''), context) 45 | 46 | await email.send_mail(sender=sender, 47 | to=to, 48 | subject=subject, 49 | message=message, 50 | html_message=html_message) 51 | 52 | data = dict(success=True, 53 | message=request.config['EMAIL_MESSAGE_SUCCESS']) 54 | 55 | else: 56 | data = form.tojson() 57 | return self.json_response(request, data) 58 | 59 | def html_content(self, request, content, context): 60 | app = request.app 61 | return app.green_pool.submit(app.cms.html_content, 62 | request, content, context) 63 | -------------------------------------------------------------------------------- /lux/ext/sockjs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Websocket handler for SockJS clients. 3 | """ 4 | from lux.core import Parameter, LuxExtension 5 | from pulsar.utils.importer import module_attribute 6 | 7 | from .socketio import SocketIO 8 | from .ws import LuxWs 9 | from . import rpc 10 | 11 | 12 | __all__ = ['LuxWs'] 13 | 14 | 15 | class Extension(LuxExtension, 16 | rpc.WsChannelsRpc, 17 | rpc.WsAuthRpc, 18 | rpc.WsModelRpc): 19 | 20 | _config = [ 21 | Parameter('WS_URL', '/ws', 'Websocket base url'), 22 | Parameter('WS_HANDLER', 'lux.extensions.sockjs:LuxWs', 23 | 'Dotted path to websocket handler'), 24 | Parameter('WEBSOCKET_HARTBEAT', 25, 'Hartbeat in seconds'), 25 | Parameter('WEBSOCKET_AVAILABLE', True, 26 | 'websocket server handle available') 27 | ] 28 | 29 | def on_config(self, app): 30 | app.add_events(('on_websocket_open', 'on_websocket_close')) 31 | 32 | def middleware(self, app): 33 | """Add websocket middleware 34 | """ 35 | url = app.config['WS_URL'] 36 | if url: 37 | handler = module_attribute(app.config['WS_HANDLER']) 38 | return [SocketIO(url, handler(app))] 39 | -------------------------------------------------------------------------------- /lux/ext/sockjs/rpc/auth.py: -------------------------------------------------------------------------------- 1 | """Websocket RPC for Authentication 2 | """ 3 | from pulsar.api import PermissionDenied, BadRequest, Http401 4 | from pulsar.apps import rpc 5 | 6 | from lux.core import Resource 7 | 8 | 9 | class WsResource(Resource): 10 | 11 | def __call__(self, request, **kw): 12 | try: 13 | return super().__call__(request, **kw) 14 | except PermissionDenied: 15 | raise rpc.InvalidRequest('permission denied') from None 16 | except Http401: 17 | raise rpc.InvalidRequest('authentication required') from None 18 | 19 | 20 | class WsAuthRpc: 21 | 22 | def ws_authenticate(self, wsrequest): 23 | """Websocket RPC method for authenticating a user 24 | """ 25 | if wsrequest.cache.user_info: 26 | raise rpc.InvalidRequest('Already authenticated') 27 | token = wsrequest.required_param("authToken") 28 | model = wsrequest.app.models.get('user') 29 | if not model: 30 | raise rpc.InternalError('user model missing') 31 | wsgi = wsrequest.ws.wsgi_request 32 | backend = wsgi.cache.auth_backend 33 | auth = 'bearer %s' % token 34 | try: 35 | backend.authorize(wsgi, auth) 36 | except BadRequest: 37 | raise rpc.InvalidParams('bad authToken') from None 38 | args = {model.id_field: getattr(wsgi.cache.user, model.id_field)} 39 | user = model.get_instance(wsgi, **args) 40 | user_info = model.tojson(wsgi, user) 41 | wsrequest.cache.user_info = user_info 42 | return user_info 43 | -------------------------------------------------------------------------------- /lux/ext/sockjs/socketio.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import hashlib 3 | from random import randint 4 | 5 | from pulsar.api import HttpException 6 | from pulsar.apps.wsgi import Router, route 7 | from pulsar.utils.httpurl import CacheControl 8 | 9 | from .transports.websocket import WebSocket 10 | from .utils import IFRAME_TEXT 11 | 12 | 13 | class SocketIO(Router): 14 | """A Router for sockjs requests 15 | """ 16 | info_cache = CacheControl(nostore=True) 17 | home_cache = CacheControl(maxage=60*60*24*30) 18 | 19 | def __init__(self, route, handle, **kwargs): 20 | super().__init__(route, **kwargs) 21 | self.handle = handle 22 | self.add_child(WebSocket('/websocket', self.handle, **kwargs)) 23 | self.add_child(WebSocket('//websocket', 24 | self.handle, **kwargs)) 25 | 26 | def get(self, request): 27 | response = request.response 28 | self.home_cache(response.headers) 29 | response.content_type = 'text/plain' 30 | response.content = 'Welcome to SockJS!\n' 31 | return response 32 | 33 | @route(method=('options', 'get')) 34 | def info(self, request): 35 | response = request.response 36 | self.info_cache(response.headers) 37 | self.origin(request) 38 | return request.json_response({ 39 | 'websocket': request.config['WEBSOCKET_AVAILABLE'], 40 | 'origins': ['*:*'], 41 | 'entropy': randint(0, sys.maxsize) 42 | }) 43 | 44 | @route('iframe[0-9-.a-z_]*.html', re=True, 45 | response_content_types=('text/html',)) 46 | def iframe(self, request): 47 | response = request.response 48 | url = request.absolute_uri(self.full_route.path) 49 | response.content = IFRAME_TEXT % url 50 | hsh = hashlib.md5(response.content[0]).hexdigest() 51 | value = request.get('HTTP_IF_NONE_MATCH') 52 | 53 | if value and value.find(hsh) != -1: 54 | raise HttpException(status=304) 55 | 56 | self.home_cache(response.headers) 57 | response['Etag'] = hsh 58 | return response 59 | 60 | def origin(self, request): 61 | """Handles request authentication 62 | """ 63 | response = request.response 64 | origin = request.get('HTTP_ORIGIN', '*') 65 | 66 | # Respond with '*' to 'null' origin 67 | if origin == 'null': 68 | origin = '*' 69 | 70 | response['Access-Control-Allow-Origin'] = origin 71 | 72 | headers = request.get('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') 73 | if headers: 74 | response['Access-Control-Allow-Headers'] = headers 75 | 76 | response['Access-Control-Allow-Credentials'] = 'true' 77 | -------------------------------------------------------------------------------- /lux/ext/sockjs/transports/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Transport: 4 | name = '' 5 | 6 | @property 7 | def cache(self): 8 | return self.handshake.cache 9 | 10 | @property 11 | def config(self): 12 | return self.handshake.config 13 | 14 | @property 15 | def app(self): 16 | return self.handshake.app 17 | 18 | def on_open(self, client): 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /lux/ext/sockjs/transports/websocket.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pulsar.api import Http404 4 | from pulsar.apps import ws 5 | 6 | from . import Transport 7 | 8 | 9 | class WebSocketProtocol(ws.WebSocketProtocol, Transport): 10 | name = 'websocket' 11 | _logger = logging.getLogger('lux.sockjs') 12 | 13 | def on_open(self, client): 14 | self.logger.info('Opened a new websocket connection %s', client) 15 | self._hartbeat(client, 'o') 16 | 17 | def _hartbeat(self, client, b): 18 | connection = self.connection 19 | if not connection or connection.closed: 20 | return 21 | if b == 'h': 22 | self.logger.debug('Hartbeat message %s', client) 23 | 24 | self.write(b) 25 | self._loop.call_later(self.config['WEBSOCKET_HARTBEAT'], 26 | self._hartbeat, client, 'h') 27 | 28 | 29 | class WebSocket(ws.WebSocket): 30 | '''WebSocket wsgi handler with new protocol class 31 | ''' 32 | protocol_class = WebSocketProtocol 33 | 34 | def get(self, request): 35 | if not request.config['WEBSOCKET_AVAILABLE']: 36 | raise Http404 37 | return super().get(request) 38 | -------------------------------------------------------------------------------- /lux/ext/sockjs/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | IFRAME_TEXT = ''' 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 |

Don't panic!

16 |

This is a SockJS hidden iframe. It's used for cross domain magic.

17 | 18 | '''.strip() 19 | -------------------------------------------------------------------------------- /lux/ext/sql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/ext/sql/__init__.py -------------------------------------------------------------------------------- /lux/models/__init__.py: -------------------------------------------------------------------------------- 1 | """Lux models are built on top of 2 | 3 | * Marshmallow Schemas 4 | * SqlAlchemy models 5 | """ 6 | from marshmallow import ValidationError, post_dump, post_load 7 | from marshmallow.validate import OneOf 8 | 9 | import inflect 10 | 11 | from .schema import Schema, resource_name, get_schema_class 12 | from .component import Component 13 | from .model import ModelContainer, Model 14 | from .query import Query, Session 15 | from .unique import UniqueField 16 | 17 | inflect = inflect.engine() 18 | 19 | 20 | __all__ = [ 21 | 'Schema', 22 | 'Component', 23 | 'ValidationError', 24 | 'resource_name', 25 | 'get_schema_class', 26 | # 27 | 'post_dump', 28 | 'post_load', 29 | 'OneOf', 30 | # 31 | 'inflect', 32 | # 33 | 'ModelContainer', 34 | 'Model', 35 | 'Session', 36 | 'Query', 37 | 'UniqueField' 38 | ] 39 | -------------------------------------------------------------------------------- /lux/models/component.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Component: 4 | """Application component 5 | """ 6 | app = None 7 | logger = None 8 | 9 | def init_app(self, app): 10 | self.app = app 11 | if not self.logger: 12 | self.logger = app.logger 13 | return self 14 | 15 | @property 16 | def green_pool(self): 17 | if self.app: 18 | return self.app.green_pool 19 | 20 | @property 21 | def config(self): 22 | if self.app: 23 | return self.app.config 24 | -------------------------------------------------------------------------------- /lux/models/unique.py: -------------------------------------------------------------------------------- 1 | from inspect import currentframe 2 | 3 | from marshmallow import ValidationError 4 | from marshmallow.fields import Field 5 | 6 | from pulsar.api import Http404 7 | 8 | 9 | class Validator: 10 | 11 | def field_session(self): 12 | frame = currentframe().f_back 13 | field = None 14 | while frame: 15 | if not field: 16 | _field = frame.f_locals.get('self') 17 | if isinstance(_field, Field): 18 | field = _field 19 | session = frame.f_locals.get('session') 20 | if session is not None: 21 | break 22 | frame = frame.f_back 23 | return field, session 24 | 25 | def __call__(self, value): 26 | raise NotImplementedError 27 | 28 | 29 | class UniqueField(Validator): 30 | '''Validator for a field which accept unique values 31 | ''' 32 | validation_error = '{0} not available' 33 | 34 | def __init__(self, model, nullable=False, validation_error=None): 35 | self.model = model 36 | self.nullable = nullable 37 | self.validation_error = validation_error or self.validation_error 38 | 39 | def __call__(self, value): 40 | field, session = self.field_session() 41 | model = field.root.app.models.get(self.model) 42 | if not model: 43 | raise ValidationError('No model %s' % self.model) 44 | 45 | kwargs = {field.name: value} 46 | self.test(session, value, model, **kwargs) 47 | 48 | def test(self, session, value, model, **kwargs): 49 | previous_state = None 50 | try: 51 | instance = model.get_one(session, **kwargs) 52 | except Http404: 53 | pass 54 | else: 55 | if instance != previous_state: 56 | raise ValidationError( 57 | self.validation_error.format(value) 58 | ) 59 | -------------------------------------------------------------------------------- /lux/openapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import OpenAPI, OperationInfo, OpenApiSchema, METHODS 2 | 3 | __all__ = [ 4 | 'OpenAPI', 'OperationInfo', 'OpenApiSchema', 'METHODS', 5 | 'VALID_PROPERTIES', 'VALID_PREFIX' 6 | ] 7 | 8 | 9 | VALID_PREFIX = 'x-' 10 | 11 | VALID_PROPERTIES = { 12 | 'format', 13 | 'title', 14 | 'description', 15 | 'default', 16 | 'multipleOf', 17 | 'maximum', 18 | 'exclusiveMaximum', 19 | 'minimum', 20 | 'exclusiveMinimum', 21 | 'maxLength', 22 | 'minLength', 23 | 'pattern', 24 | 'maxItems', 25 | 'minItems', 26 | 'uniqueItems', 27 | 'maxProperties', 28 | 'minProperties', 29 | 'required', 30 | 'enum', 31 | 'type', 32 | 'items', 33 | 'allOf', 34 | 'properties', 35 | 'additionalProperties', 36 | 'readOnly', 37 | 'xml', 38 | 'externalDocs', 39 | 'example', 40 | } 41 | -------------------------------------------------------------------------------- /lux/openapi/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/openapi/ext/__init__.py -------------------------------------------------------------------------------- /lux/openapi/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import yaml 4 | 5 | 6 | def compact(**kwargs): 7 | return {k: v for k, v in kwargs.items() if v} 8 | 9 | 10 | # from django.contrib.admindocs.utils 11 | def trim_docstring(docstring): 12 | """Uniformly trims leading/trailing whitespace from docstrings. 13 | Based on 14 | http://www.python.org/peps/pep-0257.html#handling-docstring-indentation 15 | """ 16 | if not docstring or not docstring.strip(): 17 | return '' 18 | # Convert tabs to spaces and split into lines 19 | lines = docstring.expandtabs().splitlines() 20 | indent = min(len(line) - len(line.lstrip()) 21 | for line in lines if line.lstrip()) 22 | trimmed = [lines[0].lstrip()] + [ 23 | line[indent:].rstrip() for line in lines[1:]] 24 | return "\n".join(trimmed).strip() 25 | 26 | 27 | # from rest_framework.utils.formatting 28 | def dedent(content): 29 | """ 30 | Remove leading indent from a block of text. 31 | Used when generating descriptions from docstrings. 32 | Note that python's `textwrap.dedent` doesn't quite cut it, 33 | as it fails to dedent multiline docstrings that include 34 | unindented text on the initial line. 35 | """ 36 | whitespace_counts = [len(line) - len(line.lstrip(' ')) 37 | for line in content.splitlines()[1:] if line.lstrip()] 38 | 39 | # unindent the content if needed 40 | if whitespace_counts: 41 | whitespace_pattern = '^' + (' ' * min(whitespace_counts)) 42 | content = re.sub( 43 | re.compile(whitespace_pattern, re.MULTILINE), '', content) 44 | 45 | return content.strip() 46 | 47 | 48 | def load_yaml_from_docstring(docstring): 49 | """Loads YAML from docstring.""" 50 | split_lines = trim_docstring(docstring).split('\n') 51 | 52 | # Cut YAML from rest of docstring 53 | for index, line in enumerate(split_lines): 54 | line = line.strip() 55 | if line.startswith('---'): 56 | cut_from = index 57 | break 58 | else: 59 | return None 60 | 61 | yaml_string = "\n".join(split_lines[cut_from:]) 62 | yaml_string = dedent(yaml_string) 63 | return yaml.load(yaml_string) 64 | -------------------------------------------------------------------------------- /lux/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/lux/utils/__init__.py -------------------------------------------------------------------------------- /lux/utils/async.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def maybe_green(app, callable, *args, **kwargs): 4 | """Run a ``callable`` in the green pool if needed 5 | 6 | :param app: lux application 7 | :param callable: callable to execute 8 | :param args: 9 | :param kwargs: 10 | :return: a synchronous or asynchronous result 11 | """ 12 | pool = app.green_pool 13 | if pool: 14 | return pool.submit(callable, *args, **kwargs) 15 | else: 16 | return callable(*args, **kwargs) 17 | 18 | 19 | def green(func): 20 | 21 | def _(*args, **kwargs): 22 | assert len(args) >= 1, ("green decorator should be applied to " 23 | "functions accepting at least one positional " 24 | "parameter") 25 | pool = getattr(args[0], 'green_pool', None) 26 | if pool: 27 | return pool.submit(func, *args, **kwargs) 28 | else: 29 | return func(*args, **kwargs) 30 | 31 | return _ 32 | -------------------------------------------------------------------------------- /lux/utils/auth.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import PermissionDenied, Http401 2 | 3 | 4 | def ensure_authenticated(request): 5 | """ 6 | Ensures the request is by an authenticated user; raises a 401 otherwise 7 | :param request: request object 8 | :return: user object 9 | """ 10 | user = request.cache.user 11 | if not user or not user.is_authenticated(): 12 | raise Http401('Token', 'Requires authentication') 13 | return user 14 | 15 | 16 | def check_permission(request, resource, action): 17 | """ 18 | Checks whether the current user has a permission 19 | :param request: request object 20 | :param resource: resource to check permission for 21 | :param action: action/actions tio perform 22 | :return: True 23 | :raise: PermissionDenied if the user doesn't have the 24 | permission checked 25 | """ 26 | backend = request.cache.auth_backend 27 | if backend.has_permission(request, resource, action): 28 | return True 29 | raise PermissionDenied 30 | 31 | 32 | def normalise_email(email): 33 | """ 34 | Normalise the address by lowercasing the domain part of the email 35 | address. 36 | """ 37 | if email: 38 | email_name, domain_part = email.strip().rsplit('@', 1) 39 | email = '@'.join([email_name, domain_part.lower()]) 40 | return email 41 | -------------------------------------------------------------------------------- /lux/utils/context.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from pulsar.api import context 3 | 4 | 5 | def app_attribute(func, name=None): 6 | name = name or func.__name__ 7 | 8 | @wraps(func) 9 | def _(app=None): 10 | if app is None: 11 | app = current_app() 12 | else: 13 | app = app.app 14 | assert app, "application not available" 15 | if name not in app.cache: 16 | app.cache[name] = func(app) 17 | return app.cache[name] 18 | 19 | return _ 20 | 21 | 22 | def current_app(): 23 | return context.get('__app__') 24 | 25 | 26 | def set_app(app): 27 | context.set('__app__', app) 28 | 29 | 30 | def current_request(): 31 | return context.get('__request__') 32 | 33 | 34 | def set_request(request): 35 | context.set('__request__', request) 36 | context.set('__app__', request.app) 37 | -------------------------------------------------------------------------------- /lux/utils/countries.py: -------------------------------------------------------------------------------- 1 | """ 2 | List of countries 3 | """ 4 | from datetime import datetime 5 | 6 | import pytz 7 | 8 | # from geoip import geolite2 9 | 10 | 11 | _better_names = {'GB': 'United Kingdom'} 12 | 13 | 14 | def better_country_names(): 15 | names = dict(((key, _better_names.get(key, value)) 16 | for key, value in pytz.country_names.items())) 17 | return sorted(names.items(), key=lambda d: d[1]) 18 | 19 | 20 | def countries_lookup(): 21 | return sorted(better_country_names(), key=lambda t: t[1]) 22 | 23 | 24 | def _timezones(): 25 | """Yield timezone name, gap to UTC and UTC 26 | """ 27 | for item in pytz.common_timezones: 28 | dt = datetime.now(pytz.timezone(item)).strftime('%z') 29 | utc = '(UTC%s:%s)' % (dt[:-2], dt[-2:]) 30 | gap = int(dt) 31 | yield item, gap, utc 32 | 33 | 34 | def _common_timezones(): 35 | for item, g, utc in sorted(_timezones(), key=lambda t: t[1]): 36 | yield item, utc 37 | 38 | 39 | common_timezones = [(item, '%s - %s' % (utc, item)) 40 | for item, utc in _common_timezones()] 41 | 42 | country_names = tuple(better_country_names()) 43 | 44 | 45 | def timezone_info(request, data): 46 | return data 47 | -------------------------------------------------------------------------------- /lux/utils/crypt/__init__.py: -------------------------------------------------------------------------------- 1 | from os import urandom 2 | import uuid 3 | import string 4 | from random import randint 5 | from hashlib import sha1 6 | 7 | from pulsar.utils.string import random_string 8 | 9 | digits_letters = string.digits + string.ascii_letters 10 | secret_choices = digits_letters + ''.join( 11 | (p for p in string.punctuation if p != '"') 12 | ) 13 | 14 | 15 | def generate_secret(length=64, allowed_chars=None, hexadecimal=False): 16 | if hexadecimal: 17 | return ''.join((hex(randint(1, 10000)) for _ in range(length))) 18 | return random_string(length, length, allowed_chars or secret_choices) 19 | 20 | 21 | def digest(value, salt_size=8): 22 | salt = urandom(salt_size) 23 | return sha1(salt+value.encode('utf-8')).hexdigest() 24 | 25 | 26 | def create_uuid(id=None): 27 | if isinstance(id, uuid.UUID): 28 | return id 29 | return uuid.uuid4() 30 | 31 | 32 | def create_token(): 33 | return create_uuid().hex 34 | 35 | 36 | def as_hex(value): 37 | if isinstance(value, uuid.UUID): 38 | return value.hex 39 | return value 40 | -------------------------------------------------------------------------------- /lux/utils/crypt/arc4.py: -------------------------------------------------------------------------------- 1 | '''RC4, ARC4, ARCFOUR algorithm for encryption. 2 | 3 | Adapted from 4 | # 5 | # RC4, ARC4, ARCFOUR algorithm 6 | # 7 | # Copyright (c) 2009 joonis new media 8 | # Author: Thimo Kraemer 9 | # 10 | # This program is free software; you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation; either version 2 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program; if not, write to the Free Software 22 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 23 | # MA 02110-1301, USA. 24 | # 25 | ''' 26 | import base64 27 | from os import urandom 28 | 29 | 30 | __all__ = ['rc4crypt', 'encrypt', 'decrypt'] 31 | 32 | 33 | def _rc4crypt(data, box): 34 | '''Return a generator over encrypted bytes''' 35 | x = 0 36 | y = 0 37 | for o in data: 38 | x = (x + 1) % 256 39 | y = (y + box[x]) % 256 40 | box[x], box[y] = box[y], box[x] 41 | yield o ^ box[(box[x] + box[y]) % 256] 42 | 43 | 44 | def rc4crypt(data, key): 45 | '''data and key must be a byte strings''' 46 | x = 0 47 | box = list(range(256)) 48 | for i in range(256): 49 | x = (x + box[i] + key[i % len(key)]) % 256 50 | box[i], box[x] = box[x], box[i] 51 | return bytes(_rc4crypt(data, box)) 52 | 53 | 54 | def encrypt(plaintext, key, salt_size=8): 55 | if not plaintext: 56 | return '' 57 | salt = urandom(salt_size) 58 | v = rc4crypt(plaintext, salt + key) 59 | n = bytes((salt_size,)) 60 | rs = n+salt+v 61 | return base64.b64encode(rs) 62 | 63 | 64 | def decrypt(ciphertext, key): 65 | if ciphertext: 66 | rs = base64.b64decode(ciphertext) 67 | sl = rs[0] + 1 68 | salt = rs[1:sl] 69 | ciphertext = rs[sl:] 70 | return rc4crypt(ciphertext, salt+key) 71 | else: 72 | return '' 73 | 74 | 75 | def verify(encrypted, raw, key, salt_size=8): 76 | return raw == decrypt(encrypted, key) 77 | -------------------------------------------------------------------------------- /lux/utils/data.py: -------------------------------------------------------------------------------- 1 | from itertools import chain, zip_longest 2 | 3 | 4 | def update_dict(source, target): 5 | result = source.copy() 6 | result.update(target) 7 | return result 8 | 9 | 10 | def compact_dict(**kwargs): 11 | return {k: v for k, v in kwargs.items() if v is not None} 12 | 13 | 14 | def grouper(n, iterable, padvalue=None): 15 | '''grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), 16 | ('g','x','x')''' 17 | return zip_longest(*[iter(iterable)]*n, fillvalue=padvalue) 18 | 19 | 20 | def unique_tuple(*iterables): 21 | vals = [] 22 | for v in chain(*[it for it in iterables if it]): 23 | if v not in vals: 24 | vals.append(v) 25 | return tuple(vals) 26 | 27 | 28 | def json_dict(value): 29 | cfg = {} 30 | for key, val in value.items(): 31 | cfg[key] = json_value(val) 32 | return cfg 33 | 34 | 35 | def json_value(value): 36 | if isinstance(value, (int, float, str)): 37 | return value 38 | elif isinstance(value, dict): 39 | return json_dict(value) 40 | elif isinstance(value, (tuple, list, set)): 41 | return json_sequence(value) 42 | else: 43 | return str(value) 44 | 45 | 46 | def json_sequence(value): 47 | return [json_value(v) for v in value] 48 | 49 | 50 | def multi_pop(key, *dicts): 51 | value = None 52 | for d in dicts: 53 | if key in d: 54 | value = d.pop(key) 55 | return value 56 | 57 | 58 | def as_tuple(value=None): 59 | if value is None: 60 | return () 61 | elif isinstance(value, tuple): 62 | return value 63 | elif isinstance(value, (list, tuple, set, frozenset)): 64 | return tuple(value) 65 | else: 66 | return value, 67 | 68 | 69 | def boolean_from_url_query(value): 70 | return value.lower() in ('', 'true', 'yes') 71 | -------------------------------------------------------------------------------- /lux/utils/date.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import date, datetime, timedelta 3 | 4 | from dateutil.parser import parse as dateparser 5 | 6 | import pytz 7 | 8 | 9 | NUMBER = (int, float) 10 | dayfmt = "%04d-%02d-%02d%c" 11 | seconds = '{:02d}:{:02d}:{:02d}' 12 | 13 | 14 | def iso8601(dt, sep='T'): 15 | s = (dayfmt % (dt.year, dt.month, dt.day, sep) + 16 | seconds.format(dt.hour, dt.minute, dt.second)) 17 | 18 | off = dt.utcoffset() 19 | if off is not None: 20 | if off.days < 0: 21 | sign = "-" 22 | off = -off 23 | else: 24 | sign = "+" 25 | hh, mm = divmod(off, timedelta(hours=1)) 26 | mm, ss = divmod(mm, timedelta(minutes=1)) 27 | s += "%s%02d:%02d" % (sign, hh, mm) 28 | if ss: 29 | assert not ss.microseconds 30 | s += ":%02d" % ss.seconds 31 | return s 32 | 33 | 34 | def to_timestamp(dt): 35 | if isinstance(dt, str): 36 | dt = dateparser(dt) 37 | if isinstance(dt, date): 38 | return time.mktime(dt.timetuple()) 39 | elif isinstance(dt, NUMBER): 40 | return dt 41 | elif dt is not None: 42 | raise ValueError(dt) 43 | 44 | 45 | def date_from_now(seconds): 46 | return tzinfo(datetime.utcnow() + timedelta(seconds=seconds)) 47 | 48 | 49 | def tzinfo(dt): 50 | if isinstance(dt, datetime) and not dt.tzinfo: 51 | dt = pytz.utc.localize(dt) 52 | return dt 53 | -------------------------------------------------------------------------------- /lux/utils/messages.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | 3 | 4 | def error_message(*args, errors=None, trace=None): 5 | msgs = args 6 | if errors: 7 | msgs = chain(args, errors) 8 | messages = [] 9 | data = {'error': True} 10 | message = [] 11 | for msg in msgs: 12 | if isinstance(msg, str): 13 | message.append(msg) 14 | elif isinstance(msg, dict): 15 | if len(msg) == 1 and 'message' in msg: 16 | message.append(msg['message']) 17 | elif msg: 18 | messages.append(msg) 19 | data['message'] = ' '.join(message) 20 | if messages: 21 | data['errors'] = messages 22 | if trace: 23 | data['trace'] = trace 24 | return data 25 | -------------------------------------------------------------------------------- /lux/utils/py.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pip 4 | 5 | 6 | def python_packages(request): 7 | return request.json_response(dict(packages())) 8 | 9 | 10 | def packages(): 11 | yield 'python', ' '.join(sys.version.split('\n')) 12 | for p in sorted(_safe_dist(), key=lambda p: p[0]): 13 | yield p 14 | 15 | 16 | def _safe_dist(): 17 | for p in pip.get_installed_distributions(): 18 | try: 19 | yield p.key, p.version 20 | except Exception: # pragma nocover 21 | pass 22 | -------------------------------------------------------------------------------- /lux/utils/text.py: -------------------------------------------------------------------------------- 1 | 2 | try: 3 | import inflect 4 | engine = inflect.engine() 5 | except ImportError: 6 | inflect = None 7 | 8 | 9 | class Engine: 10 | 11 | def plural(self, word): 12 | return '%ss' % word 13 | 14 | 15 | if inflect is None: 16 | 17 | engine = Engine() 18 | -------------------------------------------------------------------------------- /lux/utils/token.py: -------------------------------------------------------------------------------- 1 | """JWT_ 2 | 3 | Once a ``jtw`` is created, authetication is achieved by setting 4 | the ``Authorization`` header to ``Bearer jwt``. 5 | 6 | Requires pyjwt_ package. 7 | 8 | .. _pyjwt: https://pypi.python.org/pypi/PyJWT 9 | .. _JWT: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html 10 | """ 11 | import jwt 12 | import time 13 | from datetime import date 14 | 15 | 16 | encode = jwt.encode 17 | decode = jwt.decode 18 | ExpiredSignature = jwt.ExpiredSignature 19 | DecodeError = jwt.DecodeError 20 | 21 | 22 | def encode_json(payload, secret, **kw): 23 | return encode(payload, secret, **kw).decode('utf-8') 24 | 25 | 26 | def create_token(request, *args, expiry=None, **kwargs): 27 | token = dict(*args, **kwargs) 28 | 29 | if isinstance(expiry, date): 30 | token['exp'] = int(time.mktime(expiry.timetuple())) 31 | 32 | request.app.fire('on_token', request, token) 33 | return encode(token, request.config['SECRET_KEY']).decode('utf-8') 34 | 35 | 36 | def app_token(app, payload=None): 37 | if not payload: 38 | payload = {'app_name': app.config['APP_NAME']} 39 | return encode(payload, app.config['SECRET_KEY']).decode('utf-8') 40 | -------------------------------------------------------------------------------- /lux/utils/url.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlsplit 2 | 3 | 4 | def is_url(url): 5 | if url: 6 | p = urlsplit(url) 7 | return p.scheme and p.netloc 8 | return False 9 | 10 | 11 | def absolute_uri(request, url): 12 | if not is_url(url): 13 | return request.absolute_uri(url) 14 | return url 15 | 16 | 17 | def initial_slash(path): 18 | if path and not path.startswith('/'): 19 | return '/%s' % path 20 | return path 21 | -------------------------------------------------------------------------------- /lux/utils/version.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import subprocess 4 | 5 | symbol = {'alpha': 'a', 'beta': 'b'} 6 | 7 | 8 | def get_version(version, filename=None): 9 | assert len(version) == 5 10 | assert version[3] in ('alpha', 'beta', 'rc', 'final') 11 | main = '.'.join(map(str, version[:3])) 12 | sub = '' 13 | if version[3] == 'alpha' and version[4] == 0: 14 | git_changeset = get_git_changeset(filename) 15 | if git_changeset: 16 | sub = '.dev%s' % git_changeset 17 | if version[3] != 'final' and not sub: 18 | sub = '%s%s' % (symbol.get(version[3], version[3]), version[4]) 19 | return main + sub 20 | 21 | 22 | def sh(command, cwd=None): 23 | return subprocess.Popen(command, 24 | stdout=subprocess.PIPE, 25 | stderr=subprocess.PIPE, 26 | shell=True, 27 | cwd=cwd, 28 | universal_newlines=True).communicate()[0] 29 | 30 | 31 | def get_git_changeset(filename=None): 32 | """Returns a numeric identifier of the latest git changeset. 33 | 34 | The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. 35 | This value isn't guaranteed to be unique, but collisions are very unlikely, 36 | so it's sufficient for generating the development version numbers. 37 | """ 38 | dirname = os.path.dirname(filename or __file__) 39 | git_show = sh('git show --pretty=format:%ct --quiet HEAD', 40 | cwd=dirname) 41 | timestamp = git_show.partition('\n')[0] 42 | try: 43 | timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) 44 | except ValueError: 45 | return None 46 | return timestamp.strftime('%Y%m%d%H%M%S') 47 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | if __name__ == '__main__': 5 | import example 6 | example.main() 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.7.0", 3 | "name": "lux", 4 | "descriptiomn": "Asynchronous web framework for python", 5 | "author": { 6 | "name": "quantmind.com", 7 | "email": "message@quantmind.com" 8 | }, 9 | "dependencies": { 10 | "angular": "1.5", 11 | "lodash-es": "4.15", 12 | "bootstrap-sass": "3.3" 13 | }, 14 | "devDependencies": { 15 | "angular-ui-bootstrap": "2.0", 16 | "angular-ui-grid": "3.2", 17 | "angular-mocks": "1.5", 18 | "babel-eslint": "6.1", 19 | "babel-preset-es2015": "6.13", 20 | "babel-preset-es2015-loose-rollup": "7.0", 21 | "babelify": "7.3", 22 | "browserify": "13.1", 23 | "browserify-istanbul": "2.0", 24 | "d3-collection": "1.0", 25 | "eslint": "3.2", 26 | "eslint-config-angular": "0.5", 27 | "eslint-plugin-angular": "1.3", 28 | "eslint-plugin-disable": "0.3", 29 | "jasmine-core": "2.4", 30 | "karma": "1.2", 31 | "karma-browserify": "5.1", 32 | "karma-coverage": "1.1", 33 | "karma-chrome-launcher": "1.0", 34 | "karma-es5-shim": "0.0.4", 35 | "karma-firefox-launcher": "1.0", 36 | "karma-jasmine": "1.0", 37 | "karma-junit-reporter": "1.1", 38 | "karma-phantomjs-launcher": "1.0", 39 | "ng-annotate": "1.2", 40 | "phantomjs-prebuilt": "2.1", 41 | "requirejs": "2.2", 42 | "rollup": "0.34", 43 | "rollup-plugin-babel": "2.6", 44 | "rollup-plugin-commonjs": "3.3", 45 | "rollup-plugin-json": "2.0", 46 | "rollup-plugin-node-resolve": "2.0", 47 | "rollup-plugin-replace": "1.1", 48 | "ui-select": "0.19", 49 | "uglify-js": "2.7", 50 | "watchify": "3.7" 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "https://github.com/quantmind/lux" 55 | }, 56 | "scripts": { 57 | "test": "npm run-script build-bundle && npm run-script unit", 58 | "build-bundle": "rollup -c && ng-annotate -a build/rollup.js -o build/lux.js", 59 | "build": "npm run-script build-bundle && r.js -o bundle.js optimize=none && uglifyjs example/website/media/website/website.js -c -m -o example/website/media/website/website.min.js", 60 | "debug": "karma start --browsers Chrome", 61 | "lint": "eslint lux/js", 62 | "unit": "karma start lux/js/build/karma.conf.js --single-run" 63 | }, 64 | "keywords": [], 65 | "license": "BSD-3-Clause" 66 | } 67 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | unidecode 2 | sphinx 3 | redis 4 | coverage 5 | codecov 6 | flake8 7 | pyslink 8 | -------------------------------------------------------------------------------- /requirements/hard.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | pytz 3 | greenlet 4 | jinja2 5 | pyjwt 6 | marshmallow 7 | pulsar 8 | -------------------------------------------------------------------------------- /requirements/soft.txt: -------------------------------------------------------------------------------- 1 | lxml 2 | markdown 3 | recommonmark 4 | premailer 5 | pyyaml 6 | beautifulsoup4 7 | inflect 8 | 9 | # pulsar speedup 10 | uvloop 11 | httptools 12 | 13 | 14 | # required by odm extension 15 | psycopg2 16 | alembic 17 | marshmallow-sqlalchemy 18 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = clean sdist bdist_wheel upload 3 | test = pulsar_test 4 | 5 | [flake8] 6 | exclude = .git,.idea,.eggs,build,coverage,dist,docs,venv,junit,node_modules,src,lux/ext/oauth/google/__init__.py,example/webalone/migrations,lux/core/commands/project_template/manage.py 7 | 8 | [metadata] 9 | license-file = LICENSE 10 | 11 | [pulsar_test] 12 | test_modules = tests 13 | test_timeout = 30 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | from setuptools import setup, find_packages 5 | 6 | import lux 7 | 8 | 9 | def read(name): 10 | filename = os.path.join(os.path.dirname(__file__), name) 11 | with open(filename) as fp: 12 | return fp.read() 13 | 14 | 15 | def requirements(name): 16 | install_requires = [] 17 | dependency_links = [] 18 | 19 | for line in read(name).split('\n'): 20 | if line.startswith('-e '): 21 | link = line[3:].strip() 22 | if link == '.': 23 | continue 24 | dependency_links.append(link) 25 | line = link.split('=')[1] 26 | line = line.strip() 27 | if line: 28 | install_requires.append(line) 29 | 30 | return install_requires, dependency_links 31 | 32 | 33 | meta = dict( 34 | version=lux.__version__, 35 | description=lux.__doc__, 36 | name='lux', 37 | author='Luca Sbardella', 38 | author_email="luca@quantmind.com", 39 | maintainer_email="luca@quantmind.com", 40 | url="https://github.com/quantmind/lux", 41 | license="BSD", 42 | long_description=read('README.rst'), 43 | packages=find_packages(exclude=['tests', 'tests.*']), 44 | include_package_data=True, 45 | zip_safe=False, 46 | setup_requires=['pulsar', 'wheel'], 47 | install_requires=requirements('requirements/hard.txt')[0], 48 | scripts=['bin/luxmake.py'], 49 | classifiers=[ 50 | 'Development Status :: 3 - Alpha', 51 | 'Environment :: Web Environment', 52 | 'Intended Audience :: Developers', 53 | 'License :: OSI Approved :: BSD License', 54 | 'Operating System :: OS Independent', 55 | 'Programming Language :: JavaScript', 56 | 'Programming Language :: Python', 57 | 'Programming Language :: Python :: 3', 58 | 'Programming Language :: Python :: 3.5', 59 | 'Programming Language :: Python :: 3.6', 60 | 'Topic :: Utilities' 61 | ] 62 | ) 63 | 64 | 65 | if __name__ == '__main__': 66 | if len(sys.argv) > 1 and sys.argv[1] == 'agile': 67 | from agile.app import AgileManager 68 | AgileManager(description='Release manager for lux', 69 | argv=sys.argv[2:]).start() 70 | else: 71 | setup(**meta) 72 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/tests/__init__.py -------------------------------------------------------------------------------- /tests/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, Integer, String, Boolean, DateTime 4 | 5 | from lux.core import LuxExtension 6 | from lux.models import Schema, fields 7 | from lux.ext.rest import RestRouter 8 | from lux.ext.odm import Model, model_base 9 | 10 | from tests.config import * # noqa 11 | 12 | EXTENSIONS = ['lux.ext.base', 13 | 'lux.ext.rest', 14 | 'lux.ext.odm', 15 | 'lux.ext.auth'] 16 | 17 | API_URL = '' 18 | DEFAULT_CONTENT_TYPE = 'application/json' 19 | COMING_SOON_URL = 'coming-soon' 20 | DATASTORE = 'postgresql+green://lux:luxtest@127.0.0.1:5432/luxtests' 21 | DEFAULT_POLICY = [ 22 | { 23 | "resource": [ 24 | "passwords:*", 25 | "mailinglist:*" 26 | ], 27 | "action": "*" 28 | }, 29 | { 30 | "resource": "objectives:*", 31 | "action": "*" 32 | }, 33 | { 34 | "resource": "objectives:*:deadline", 35 | "action": "*", 36 | "effect": "deny", 37 | "condition": "user.is_anonymous()" 38 | } 39 | ] 40 | 41 | dbModel = model_base('odmtest') 42 | 43 | 44 | class Extension(LuxExtension): 45 | 46 | def api_sections(self, app): 47 | return (ObjectiveCRUD(), SecretCRUD()) 48 | 49 | 50 | class Objective(dbModel): 51 | id = Column(Integer, primary_key=True) 52 | subject = Column(String(250)) 53 | deadline = Column(String(250)) 54 | outcome = Column(String(250)) 55 | done = Column(Boolean, default=False) 56 | created = Column(DateTime, default=datetime.utcnow) 57 | 58 | 59 | class Secret(dbModel): 60 | id = Column(Integer, primary_key=True) 61 | value = Column(String(250)) 62 | created = Column(DateTime, default=datetime.utcnow) 63 | 64 | 65 | class ObjectiveSchema(Schema): 66 | 67 | class Meta: 68 | model = 'objectives' 69 | 70 | 71 | class SecretSchema(Schema): 72 | value = fields.String(required=False) 73 | 74 | 75 | class ObjectiveCRUD(RestRouter): 76 | model = Model( 77 | 'objectives', 78 | model_schema=ObjectiveSchema, 79 | create_schema=ObjectiveSchema 80 | ) 81 | 82 | def get(self, request): 83 | return self.model.get_list_response(request) 84 | 85 | def post(self, request): 86 | return self.model.create_response(request) 87 | 88 | 89 | class SecretCRUD(RestRouter): 90 | model = Model( 91 | 'secrets', 92 | model_schema=SecretSchema 93 | ) 94 | -------------------------------------------------------------------------------- /tests/auth/authentications.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class AuthenticationsMixin: 4 | 5 | # User endpoints 6 | async def test_authenticate(self): 7 | request = await self.client.get('/user') 8 | self.check401(request.response) 9 | -------------------------------------------------------------------------------- /tests/auth/commands.py: -------------------------------------------------------------------------------- 1 | from lux.core import CommandError 2 | 3 | 4 | class AuthCommandsMixin: 5 | 6 | async def test_create_token(self): 7 | command = self.app.get_command('create_token') 8 | self.assertTrue(command.help) 9 | token = await command(['--username', 'pippo']) 10 | self.assertTrue(token) 11 | 12 | async def test_create_token_fail(self): 13 | command = self.app.get_command('create_token') 14 | self.assertTrue(command.help) 15 | await self.wait.assertRaises(CommandError, command, 16 | ['--username', 'dfgdgf']) 17 | -------------------------------------------------------------------------------- /tests/auth/errors.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ErrorsMixin: 4 | """Test for errors in corner case situations 5 | """ 6 | async def test_bad_authentication(self): 7 | request = await self.client.get( 8 | self.api_url('user'), 9 | headers=[('Authorization', 'bjchdbjshbcjd')] 10 | ) 11 | self.json(request.response, 400) 12 | -------------------------------------------------------------------------------- /tests/auth/mail_list.py: -------------------------------------------------------------------------------- 1 | 2 | class MailListMixin: 3 | 4 | async def test_mailing_list_options(self): 5 | request = await self.client.options(self.api_url('mailinglist')) 6 | self.checkOptions(request.response, ['GET', 'POST', 'HEAD']) 7 | 8 | async def test_mailing_list_post_422(self): 9 | request = await self.client.post( 10 | self.api_url('mailinglist'), 11 | json={}, 12 | jwt=self.admin_jwt 13 | ) 14 | self.assertValidationError(request.response, 'email') 15 | 16 | async def test_mailing_list_post(self): 17 | request = await self.client.post( 18 | self.api_url('mailinglist'), 19 | json=dict(email='foo@foo.com', topic='general'), 20 | jwt=self.admin_jwt 21 | ) 22 | data = self.json(request.response, 201) 23 | self.assertTrue(data) 24 | request = await self.client.post( 25 | self.api_url('mailinglist'), 26 | json=dict(email='foo@foo.com'), 27 | jwt=self.admin_jwt 28 | ) 29 | self.assertValidationError(request.response, text='Already subscribed') 30 | -------------------------------------------------------------------------------- /tests/auth/test_backend.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | from lux.models import fields 3 | 4 | from tests.auth.utils import AuthUtils 5 | 6 | 7 | class TestBackend(test.AppTestCase, AuthUtils): 8 | config_file = 'tests.auth' 9 | 10 | @classmethod 11 | def populatedb(cls): 12 | pass 13 | 14 | def test_backend(self): 15 | self.assertTrue(self.app.auth) 16 | 17 | @test.green 18 | def test_get_user_none(self): 19 | auth = self.app.auth 20 | with self.app.models.begin_session() as session: 21 | self.assertEqual( 22 | auth.get_user(session, id=18098098), 23 | None 24 | ) 25 | self.assertEqual( 26 | auth.get_user(session, email='ksdcks.sdvddvf@djdjhdfc.com'), 27 | None 28 | ) 29 | self.assertEqual( 30 | auth.get_user(session, username='dhvfvhsdfgvhfd'), 31 | None 32 | ) 33 | 34 | def test_create_user(self): 35 | return self._new_credentials() 36 | 37 | @test.green 38 | def test_create_superuser(self): 39 | with self.app.models.begin_session() as session: 40 | user = self.app.auth.create_superuser( 41 | session, 42 | username='foo', 43 | email='foo@pippo.com', 44 | password='pluto', 45 | first_name='Foo' 46 | ) 47 | self.assertTrue(user.id) 48 | self.assertEqual(user.first_name, 'Foo') 49 | self.assertTrue(user.superuser) 50 | self.assertTrue(user.active) 51 | 52 | @test.green 53 | def test_permissions(self): 54 | '''Test permission models 55 | ''' 56 | odm = self.app.odm() 57 | 58 | with odm.begin() as session: 59 | user = odm.user(username=test.randomname()) 60 | group = odm.group(name='staff') 61 | session.add(user) 62 | session.add(group) 63 | group.users.append(user) 64 | 65 | self.assertTrue(user.id) 66 | self.assertTrue(group.id) 67 | 68 | groups = user.groups 69 | self.assertTrue(group in groups) 70 | 71 | with odm.begin() as session: 72 | # add goup to the session 73 | session.add(group) 74 | permission = odm.permission(name='admin', 75 | description='Can access the admin', 76 | policy={}) 77 | group.permissions.append(permission) 78 | 79 | def test_rest_user(self): 80 | """Check that the RestField was overwritten properly""" 81 | model = self.app.models['users'] 82 | schema = model.get_schema(model.model_schema) 83 | self.assertIsInstance(schema.fields['email'], fields.Email) 84 | -------------------------------------------------------------------------------- /tests/auth/test_postgresql.py: -------------------------------------------------------------------------------- 1 | from asyncio import gather 2 | 3 | from lux.utils import test 4 | 5 | from tests.auth.user import UserMixin 6 | from tests.auth.password import PasswordMixin 7 | from tests.auth.permissions import PermissionsMixin 8 | from tests.auth.commands import AuthCommandsMixin 9 | from tests.auth.utils import AuthUtils 10 | from tests.auth.registration import RegistrationMixin 11 | from tests.auth.mail_list import MailListMixin 12 | from tests.auth.groups import GroupsMixin 13 | from tests.auth.errors import ErrorsMixin 14 | 15 | 16 | class TestPostgreSql(test.AppTestCase, 17 | AuthUtils, 18 | AuthCommandsMixin, 19 | UserMixin, 20 | PasswordMixin, 21 | PermissionsMixin, 22 | RegistrationMixin, 23 | ErrorsMixin, 24 | GroupsMixin, 25 | MailListMixin): 26 | config_file = 'tests.auth' 27 | 28 | @classmethod 29 | async def beforeAll(cls): 30 | cls.super_token, cls.pippo_token = await gather( 31 | cls.user_token('testuser', jwt=cls.admin_jwt), 32 | cls.user_token('pippo', jwt=cls.admin_jwt) 33 | ) 34 | -------------------------------------------------------------------------------- /tests/auth/test_sqlite.py: -------------------------------------------------------------------------------- 1 | import tests.auth.test_postgresql as test 2 | 3 | 4 | class TestSqlite(test.TestPostgreSql): 5 | config_params = {'DATASTORE': 'sqlite://'} 6 | -------------------------------------------------------------------------------- /tests/auth/user.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class UserMixin: 4 | """Test autheticated user CRUD views 5 | """ 6 | # User endpoints 7 | async def test_get_user_401(self): 8 | request = await self.client.get(self.api_url('user')) 9 | self.check401(request.response) 10 | 11 | async def test_get_user_200(self): 12 | request = await self.client.get(self.api_url('user'), 13 | token=self.pippo_token) 14 | data = self.json(request.response, 200) 15 | self.assertEqual(data['username'], 'pippo') 16 | 17 | async def test_update_user_401(self): 18 | request = await self.client.patch(self.api_url('user'), 19 | json=dict(name='gino')) 20 | self.check401(request.response) 21 | 22 | async def test_update_user_200(self): 23 | credentials = await self._new_credentials() 24 | token = await self.user_token(credentials, 25 | jwt=self.admin_jwt) 26 | request = await self.client.patch(self.api_url('user'), 27 | token=token, 28 | json=dict(first_name='gino')) 29 | data = self.json(request.response, 200) 30 | self.assertEqual(data['username'], credentials['username']) 31 | self.assertEqual(data['first_name'], 'gino') 32 | 33 | async def test_options_user_permissions(self): 34 | request = await self.client.options(self.api_url('user/permissions')) 35 | self.checkOptions(request.response) 36 | 37 | async def test_get_user_permissions_anonymous(self): 38 | request = await self.client.get(self.api_url('user/permissions')) 39 | response = request.response 40 | self.json(response, 200) 41 | 42 | async def test_get_user_permissions_200(self): 43 | request = await self.client.get(self.api_url('user/permissions'), 44 | token=self.pippo_token) 45 | self.json(request.response, 200) 46 | -------------------------------------------------------------------------------- /tests/auth/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import date, timedelta 2 | 3 | from lux.utils import test 4 | 5 | 6 | def deadline(days): 7 | return (date.today() + timedelta(days=days)).isoformat() 8 | 9 | 10 | class AuthUtils: 11 | """No tests, just utilities for testing auth module 12 | """ 13 | async def _create_objective(self, token, subject='My objective', **data): 14 | data['subject'] = subject 15 | data['deadline'] = deadline(10) 16 | request = await self.client.post('/objectives', json=data, token=token) 17 | data = self.json(request.response, 201) 18 | self.assertIsInstance(data, dict) 19 | self.assertTrue('id' in data) 20 | self.assertEqual(data['subject'], subject) 21 | self.assertTrue('created' in data) 22 | self.assertTrue('deadline' in data) 23 | return data 24 | 25 | @test.green 26 | def _new_credentials(self, username=None, superuser=False, active=True): 27 | with self.app.models.begin_session() as session: 28 | username = username or test.randomname() 29 | password = username 30 | email = '%s@test.com' % username 31 | 32 | user = session.auth.create_user( 33 | session, username=username, email=email, 34 | password=password, first_name=username, 35 | active=active, superuser=superuser) 36 | self.assertTrue(user.id) 37 | self.assertEqual(user.first_name, username) 38 | self.assertEqual(user.is_superuser(), superuser) 39 | self.assertEqual(user.is_active(), active) 40 | return {'username': username, 'password': password} 41 | -------------------------------------------------------------------------------- /tests/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/tests/base/__init__.py -------------------------------------------------------------------------------- /tests/base/test_media.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | 3 | 4 | class CommandTests(test.TestCase): 5 | config_params = { 6 | 'EXTENSIONS': ['lux.ext.base'] 7 | } 8 | 9 | def test_absolute_media(self): 10 | app = self.application(MEDIA_URL='http://foo.com', 11 | SERVE_STATIC_FILES=True) 12 | self.assertEqual(app.config['MEDIA_URL'], 'http://foo.com') 13 | self.assertFalse(app.config['SERVE_STATIC_FILES']) 14 | 15 | def test_relative_media(self): 16 | app = self.application(SERVE_STATIC_FILES=True) 17 | self.assertEqual(app.config['MEDIA_URL'], '/static/') 18 | self.assertTrue(app.config['SERVE_STATIC_FILES']) 19 | -------------------------------------------------------------------------------- /tests/config.py: -------------------------------------------------------------------------------- 1 | '''Default config file for testing''' 2 | EXTENSIONS = ['lux.ext.base', 3 | 'tests'] 4 | 5 | MEDIA_URL = '/static/' 6 | 7 | thread_workers = 1 8 | 9 | redis_cache_server = 'redis://127.0.0.1:6379/13' 10 | -------------------------------------------------------------------------------- /tests/content/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from tests.config import * # noqa 5 | 6 | from lux.utils import test 7 | 8 | 9 | API_URL = '/api' 10 | DEFAULT_CONTENT_TYPE = 'text/html' 11 | CONTENT_REPO = os.path.join(os.path.dirname(__file__), 'test_repo') 12 | 13 | GITHUB_HOOK_KEY = 'test12345' 14 | 15 | EXTENSIONS = ['lux.ext.rest', 16 | 'lux.ext.content'] 17 | AUTHENTICATION_BACKENDS = ['lux.core:SimpleBackend'] 18 | CONTENT_GROUPS = { 19 | "blog": { 20 | "path": "blog", 21 | "meta": { 22 | "priority": 1 23 | } 24 | }, 25 | "site": { 26 | "path": "*", 27 | "meta": { 28 | "priority": 1, 29 | "image": "/media/lux/see.jpg" 30 | } 31 | } 32 | } 33 | 34 | 35 | def remove_repo(): 36 | shutil.rmtree(CONTENT_REPO) 37 | 38 | 39 | def create_content(name): 40 | path = os.path.join(CONTENT_REPO, name) 41 | if not os.path.isdir(path): 42 | os.makedirs(path) 43 | with open(os.path.join(path, 'index.md'), 'w') as fp: 44 | fp.write('\n'.join(('title: Index', '', 'Just an index'))) 45 | with open(os.path.join(path, 'foo.md'), 'w') as fp: 46 | fp.write('\n'.join(('title: This is Foo', '', 'Just foo'))) 47 | 48 | 49 | class Test(test.AppTestCase): 50 | config_file = 'tests.content' 51 | 52 | @classmethod 53 | def setUpClass(cls): 54 | create_content('blog') 55 | create_content('site') 56 | return super().setUpClass() 57 | 58 | @classmethod 59 | def tearDownClass(cls): 60 | remove_repo() 61 | return super().tearDownClass() 62 | -------------------------------------------------------------------------------- /tests/content/test_static.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from lux.core import CommandError 4 | 5 | from tests import content 6 | 7 | 8 | class TestStaticSite(content.Test): 9 | 10 | async def test_static(self): 11 | command = self.app.get_command('static') 12 | self.assertTrue(command.help) 13 | 14 | with self.assertRaises(CommandError) as r: 15 | await command(['--static-path', content.CONTENT_REPO]) 16 | self.assertEqual(str(r.exception), 17 | 'specify base url with --base-url flag') 18 | 19 | await command(['--static-path', content.CONTENT_REPO, 20 | '--base-url', 'http://bla.com']) 21 | index = os.path.join(content.CONTENT_REPO, 'index.html') 22 | self.assertTrue(os.path.isfile(index)) 23 | -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- 1 | from lux.core import Parameter 2 | from lux.ext import base 3 | 4 | 5 | CONTENT_GROUPS = { 6 | 'site': { 7 | 'path': '*', 8 | 'body_template': 'home.html' 9 | }, 10 | 'bla': { 11 | 'path': '/bla/', 12 | 'body_template': 'bla.html' 13 | }, 14 | 'foo_id': { 15 | 'path': '/foo/', 16 | 'body_template': 'foo.html' 17 | } 18 | } 19 | 20 | 21 | class Extension(base.Extension): 22 | _config = [ 23 | Parameter('RANDOM_P', False), 24 | Parameter('USE_ETAGS', False, ''), 25 | ] 26 | -------------------------------------------------------------------------------- /tests/core/media/core/placeholder.txt: -------------------------------------------------------------------------------- 1 | This directory is here for testing reasons -------------------------------------------------------------------------------- /tests/core/test_app.py: -------------------------------------------------------------------------------- 1 | from pulsar.api import ImproperlyConfigured 2 | 3 | from lux.utils import test 4 | 5 | 6 | class CommandTests(test.TestCase): 7 | config_file = 'tests.core' 8 | 9 | def test_clone(self): 10 | groups = {'site': {'path': '*'}} 11 | app = self.application() 12 | callable = app.clone_callable(CONTENT_GROUPS=groups) 13 | self.assertNotEqual(app.callable, callable) 14 | app2 = callable.setup() 15 | self.assertEqual(app2.config['CONTENT_GROUPS'], groups) 16 | 17 | def test_require(self): 18 | app = self.application() 19 | self.assertRaises(ImproperlyConfigured, 20 | app.require, 'djbdvb.vkdjfbvkdf') 21 | 22 | def test_cms_attributes(self): 23 | app = self.application() 24 | cms = app.cms 25 | self.assertEqual(cms.app, app) 26 | 27 | def test_cms_page(self): 28 | app = self.application() 29 | page = app.cms.page('') 30 | self.assertTrue(page) 31 | self.assertEqual(page.path, '') 32 | self.assertEqual(page.body_template, 'home.html') 33 | sitemap = app.cms.sitemap() 34 | self.assertIsInstance(sitemap, list) 35 | self.assertEqual(id(sitemap), id(app.cms.sitemap())) 36 | 37 | def test_cms_wildcard(self): 38 | app = self.application() 39 | page = app.cms.page('xxx') 40 | self.assertTrue(page) 41 | self.assertEqual(page.path, '') 42 | self.assertEqual(page.urlargs, {'path': 'xxx'}) 43 | self.assertEqual(page.body_template, 'home.html') 44 | self.assertIsInstance(app.cms.sitemap(), list) 45 | 46 | def test_cms_path_page(self): 47 | app = self.application() 48 | page = app.cms.page('bla/foo') 49 | self.assertTrue(page) 50 | self.assertEqual(page.path, 'bla') 51 | self.assertEqual(page.body_template, 'bla.html') 52 | self.assertIsInstance(app.cms.sitemap(), list) 53 | 54 | def test_extension_override(self): 55 | app = self.application() 56 | self.assertTrue('RANDOM_P' in app.config) 57 | self.assertTrue('USE_ETAGS' in app.config) 58 | self.assertTrue('SERVE_STATIC_FILES' in app.config) 59 | -------------------------------------------------------------------------------- /tests/core/test_channels.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest import skipUnless 3 | 4 | from pulsar.apps.test import check_server 5 | 6 | from lux.utils import test 7 | 8 | from tests.config import redis_cache_server 9 | 10 | 11 | REDIS_OK = check_server('redis') 12 | 13 | 14 | @skipUnless(REDIS_OK, 'Requires a running Redis server') 15 | class ChannelsTests(test.TestCase): 16 | config_file = 'tests.core' 17 | config_params = {'PUBSUB_STORE': redis_cache_server, 18 | 'PUBSUB_PREFIX': 'foooo'} 19 | 20 | def test_handler(self): 21 | app = self.application() 22 | self.assertFalse(app.channels is None) 23 | # self.assertFalse(app.channels) 24 | self.assertEqual(app.channels.namespace, 'foooo_') 25 | 26 | async def test_server_channel(self): 27 | app = self.application() 28 | client = self.app_client(app) 29 | request = await client.get('/') 30 | self.assertEqual(request.response.status_code, 404) 31 | self.assertTrue(app.channels) 32 | self.assertTrue('server' in app.channels) 33 | 34 | async def test_reload(self): 35 | app = self.application() 36 | client = self.app_client(app) 37 | await client.get('/') 38 | # reload the app 39 | clear_local = app.callable.clear_local 40 | future = asyncio.Future() 41 | 42 | def fire(): 43 | clear_local() 44 | future.set_result(None) 45 | 46 | app.callable.clear_local = fire 47 | await app.reload() 48 | await future 49 | 50 | async def test_wildcard(self): 51 | app = self.application() 52 | 53 | future = asyncio.Future() 54 | 55 | def fire(channel, event, data): 56 | future.set_result(event) 57 | 58 | await app.channels.register('test', '*', fire) 59 | await app.channels.publish('test', 'boom') 60 | 61 | result = await future 62 | 63 | self.assertEqual(result, 'boom') 64 | self.assertEqual(len(app.channels), 2) 65 | self.assertTrue(repr(app.channels)) 66 | -------------------------------------------------------------------------------- /tests/core/test_memory_model.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | from lux.models.memory import Model 3 | 4 | 5 | class TestDictModel(test.TestCase): 6 | config_file = 'tests.rest' 7 | 8 | def test_instance(self): 9 | model = Model('test', fields=('id', 'foo', 'name')) 10 | self.assertDictEqual(model.create_instance(), {}) 11 | o = model.instance() 12 | model.set_instance_value(o, 'foo', 'bla') 13 | self.assertEqual(model.get_instance_value(o, 'foo'), 'bla') 14 | model.set_instance_value(o, 'xxx', 'bla') 15 | self.assertEqual(model.get_instance_value(o, 'xxx'), None) 16 | self.assertEqual(len(model.fields()), 5) 17 | 18 | def test_json(self): 19 | model = Model('test', fields=('id', 'foo', 'name')) 20 | app = self.application() 21 | app.models.register(model) 22 | request = app.wsgi_request() 23 | o = model.instance() 24 | model.set_instance_value(o, 'id', 1) 25 | model.set_instance_value(o, 'name', 'pippo') 26 | data = model.tojson(request, o) 27 | self.assertEqual(len(data), 2) 28 | model.set_instance_value(o, 'name', None) 29 | data = model.tojson(request, o) 30 | self.assertEqual(len(data), 1) 31 | -------------------------------------------------------------------------------- /tests/core/test_wrappers.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | 3 | 4 | class TestWrappers(test.TestCase): 5 | config_file = 'tests.core' 6 | 7 | async def test_is_secure(self): 8 | app = self.application() 9 | client = self.app_client(app) 10 | request = await client.get('/') 11 | self.assertFalse(request.is_secure) 12 | 13 | async def test_logger(self): 14 | app = self.application() 15 | client = self.app_client(app) 16 | request = await client.get('/') 17 | self.assertNotEqual(app.logger, request.logger) 18 | -------------------------------------------------------------------------------- /tests/crypt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/tests/crypt/__init__.py -------------------------------------------------------------------------------- /tests/db.sql: -------------------------------------------------------------------------------- 1 | CREATE USER lux WITH PASSWORD 'luxtest'; 2 | ALTER USER lux CREATEDB; 3 | ALTER USER lux LOGIN; 4 | CREATE DATABASE luxtests; 5 | GRANT ALL PRIVILEGES ON DATABASE luxtests to lux; 6 | -------------------------------------------------------------------------------- /tests/dbclean.sql: -------------------------------------------------------------------------------- 1 | copy(select 'drop database ' || datname || ';' from pg_database where datname ilike 'testlux%') to '/tmp/drop_db.sql'; 2 | \i /tmp/drop_db.sql 3 | -------------------------------------------------------------------------------- /tests/js/mock.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/tests/js/mock.js -------------------------------------------------------------------------------- /tests/js/placeholder.txt: -------------------------------------------------------------------------------- 1 | This directory is here for testing reasons -------------------------------------------------------------------------------- /tests/mail/__init__.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxExtension 2 | from lux.ext import smtp 3 | 4 | EXTENSIONS = ['lux.ext.smtp'] 5 | 6 | DEFAULT_CONTENT_TYPE = 'text/html' 7 | EMAIL_BACKEND = 'tests.mail.EmailBackend' 8 | EMAIL_USE_TLS = True 9 | EMAIL_HOST = '127.0.0.1' 10 | EMAIL_PORT = 25 11 | EMAIL_HOST_USER = 'server@luxtest.com' 12 | EMAIL_HOST_PASSWORD = 'dummy' 13 | EMAIL_DEFAULT_FROM = 'admin@luxtest.com' 14 | EMAIL_DEFAULT_TO = 'info@luxtest.com' 15 | SMTP_LOG_LEVEL = 'ERROR' 16 | 17 | EMAIL_ENQUIRY_RESPONSE = [ 18 | { 19 | "subject": "Website enquiry from: {{ name }} <{{ email }}>", 20 | "message": "{{ body }}" 21 | }, 22 | { 23 | "to": "{{ email }}", 24 | "subject": "Thank you for your enquiry", 25 | "message-template": "enquiry-response.txt" 26 | } 27 | ] 28 | 29 | 30 | class EmailBackend(smtp.EmailBackend): 31 | 32 | def __init__(self, app): 33 | self.app = app 34 | self.sent = [] 35 | 36 | async def send_mails(self, messages): 37 | return self._send_mails(messages) 38 | 39 | def _open(self): 40 | return DummyConnection(self) 41 | 42 | 43 | class Extension(LuxExtension): 44 | pass 45 | 46 | 47 | class DummyConnection: 48 | 49 | def __init__(self, backend): 50 | self.backend = backend 51 | 52 | def sendmail(self, *args, **kwargs): 53 | self.backend.sent.append((args, kwargs)) 54 | 55 | def quit(self): 56 | pass 57 | -------------------------------------------------------------------------------- /tests/mail/templates/enquiry-response.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | Thank you for your interest in {{ APP_NAME }}. 4 | We will address your enquiry as soon as possible 5 | 6 | Sincerely, 7 | 8 | the {{ APP_NAME }} Team 9 | 10 | Received enquiry: 11 | ----------------------------------------------- 12 | {{ body }} 13 | ----------------------------------------------- 14 | -------------------------------------------------------------------------------- /tests/mail/test_contact.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | 3 | 4 | class ContactRouterTestCase(test.TestCase): 5 | config_file = 'tests.mail' 6 | 7 | async def test_get_html(self): 8 | app = self.application() 9 | client = test.TestClient(app) 10 | request = await client.get('/contact') 11 | bs = self.bs(request.response, 200) 12 | form = bs.find('lux-form') 13 | self.assertTrue(form) 14 | 15 | async def test_post_one_email_form_valid(self): 16 | app = self.application() 17 | client = test.TestClient(app) 18 | data = dict( 19 | name='Pinco Pallino', 20 | email='pinco@pallino.com', 21 | body='Hi this is a test') 22 | request = await client.post('/contact', json=data) 23 | data = self.json(request.response, 200) 24 | self.assertEqual(data['message'], 25 | "Your message was sent! Thank You for your interest") 26 | self.assertEqual(len(app.email_backend.sent), 2) 27 | 28 | async def test_post_one_email_form_invalid(self): 29 | app = self.application() 30 | client = test.TestClient(app) 31 | data = dict( 32 | name='Pinco Pallino', 33 | email='pinco@pallino.com') 34 | request = await client.post('/contact', json=data) 35 | self.assertValidationError(request.response, 'body') 36 | -------------------------------------------------------------------------------- /tests/mail/test_smtp.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | from lux.ext.smtp import EmailBackend 3 | 4 | 5 | class SmtpTest(test.AppTestCase): 6 | config_file = 'tests.mail' 7 | 8 | @classmethod 9 | def beforeAll(cls): 10 | email = cls.app.email_backend 11 | email.send_mails = email._send_mails 12 | 13 | def test_backend(self): 14 | backend = self.app.email_backend 15 | self.assertIsInstance(backend, EmailBackend) 16 | 17 | def test_send_mail(self): 18 | backend = self.app.email_backend 19 | sent = backend.send_mail(to='pippo@foo.com', 20 | subject='Hello!', 21 | message='This is a test message') 22 | self.assertEqual(sent, 1) 23 | 24 | def test_send_html_mail(self): 25 | backend = self.app.email_backend 26 | sent = backend.send_mail(to='pippo@foo.com', 27 | subject='Hello!', 28 | html_message='

This is a test

') 29 | self.assertEqual(sent, 1) 30 | message, _ = backend.sent.pop() 31 | body = message[2].decode('utf-8') 32 | self.assertEqual(message[1][0], 'pippo@foo.com') 33 | self.assertTrue('

This is a test

' in body) 34 | -------------------------------------------------------------------------------- /tests/oauth/github.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | 3 | from tests.oauth import test 4 | 5 | 6 | class GithubMixin: 7 | 8 | # Github OAUTH 9 | async def __test_authorization_redirect(self): 10 | request = await self.client.get('/oauth/github') 11 | response = request.response 12 | self.assertEqual(response.status_code, 302) 13 | loc = urlparse(response['location']) 14 | self.assertTrue(loc.query) 15 | 16 | async def test_github_redirect_error(self): 17 | request = await self.client.get('/oauth/github/redirect') 18 | response = request.response 19 | self.assertEqual(response.status_code, 302) 20 | self.assertTrue(request.logger.exception.called) 21 | 22 | async def __test_github_redirect_user_created(self): 23 | token1 = await self._oauth_redirect('github') 24 | # 25 | # Lets try another redirect, this time the user is not created because 26 | # it exists already 27 | token2 = await self._oauth_redirect('github') 28 | self.assertNotEqual(token1.token, token2.token) 29 | self.assertEqual(token1.user_id, token2.user_id) 30 | 31 | async def _test_github_oauth_redirect(self, provider): 32 | request = await self.client.get('/oauth/github/redirect?code=dcshcvhd') 33 | response = request.response 34 | self.assertEqual(response.status_code, 302) 35 | cookie = self.cookie(request.response) 36 | self.assertTrue(cookie.startswith('test-oauth=')) 37 | self.assertFalse(request.cache.user.is_authenticated()) 38 | # Accessing the domain with the cookie yield an authenticated user 39 | request = await self.client.get('/', cookie=cookie) 40 | user = request.cache.user 41 | self.assertTrue(user.is_authenticated()) 42 | return await self._test_token(user, 'github') 43 | 44 | @test.green 45 | def _test_github_token(self, user, provider): 46 | odm = self.app.odm() 47 | with odm.begin() as session: 48 | tokens = session.query(odm.accesstoken).filter_by( 49 | user_id=user.id, provider=provider).all() 50 | self.assertEqual(len(tokens), 1) 51 | token = tokens[0] 52 | self.assertTrue(len(token.token), 20) 53 | user = session.query(odm.user).get(user.id) 54 | self.assertTrue(user.oauth['github']) 55 | return token 56 | -------------------------------------------------------------------------------- /tests/oauth/test_app.py: -------------------------------------------------------------------------------- 1 | from tests.oauth import OAuthTest 2 | from tests.oauth.github import GithubMixin 3 | 4 | 5 | class TestGithub(OAuthTest, GithubMixin): 6 | 7 | async def test_oauths(self): 8 | request = await self.client.get('/') 9 | oauths = request.cache.oauths 10 | self.assertTrue(oauths) 11 | -------------------------------------------------------------------------------- /tests/odm/fixtures/contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": [ 3 | { 4 | "subject": "pippo to the rescue", 5 | "desc": "abu" 6 | }, 7 | { 8 | "subject": "pluto to the rescue", 9 | "desc": "genie" 10 | }, 11 | { 12 | "subject": "earth is the centre of the universe" 13 | }, 14 | { 15 | "subject": "A done task", 16 | "done": true 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/odm/test_commands.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | 3 | from tests.odm.utils import SqliteMixin, OdmUtils 4 | 5 | 6 | class TestFlushModelsPsql(OdmUtils, test.AppTestCase): 7 | 8 | async def test_flush_models(self): 9 | cmd = self.fetch_command('flush_models') 10 | self.assertTrue(cmd.help) 11 | self.assertEqual(len(cmd.option_list), 2) 12 | await cmd(['dcsdcds']) 13 | data = cmd.app.stdout.getvalue() 14 | self.assertEqual(data.strip().split('\n')[-1], 15 | 'Nothing done. No models') 16 | # 17 | cmd = self.fetch_command('flush_models') 18 | await cmd(['*', '--dryrun']) 19 | data = cmd.app.stdout.getvalue() 20 | self.assertEqual(data.strip().split('\n')[-1], 21 | 'Nothing done. Dry run') 22 | # 23 | cmd = self.fetch_command('flush_models') 24 | await cmd(['*'], interactive=False, yn='no') 25 | data = cmd.app.stdout.getvalue() 26 | self.assertEqual(data.strip().split('\n')[-1], 27 | 'Nothing done') 28 | # 29 | cmd = self.fetch_command('flush_models') 30 | await cmd(['*'], interactive=False) 31 | data = cmd.app.stdout.getvalue() 32 | lines = data.strip().split('\n') 33 | self.assertTrue(lines) 34 | 35 | 36 | class TestFlushModelsSqlite(SqliteMixin, TestFlushModelsPsql): 37 | pass 38 | -------------------------------------------------------------------------------- /tests/odm/test_models.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlsplit 2 | 3 | from pulsar.api import ImproperlyConfigured 4 | 5 | from lux.utils import test 6 | from lux.models.memory import Model 7 | 8 | from tests.odm.utils import OdmUtils 9 | 10 | 11 | class TestModels(OdmUtils, test.AppTestCase): 12 | 13 | def test_register_null(self): 14 | self.assertFalse(self.app.models.register(None)) 15 | users = self.app.models['users'] 16 | model = Model('user') 17 | self.assertNotEqual(self.app.models.register(model), model) 18 | self.assertEqual(self.app.models.register(model), users) 19 | self.assertEqual(self.app.models.register(lambda: model), users) 20 | self.assertRaises(ImproperlyConfigured, lambda: model.app) 21 | 22 | def test_api_url(self): 23 | users = self.app.models['users'] 24 | self.assertTrue(users.api_route) 25 | request = self.app.wsgi_request() 26 | url = users.api_url(request) 27 | self.assertTrue(urlsplit(url).path, '/users') 28 | -------------------------------------------------------------------------------- /tests/odm/test_sqlite.py: -------------------------------------------------------------------------------- 1 | import tests.odm.test_postgresql as postgresql 2 | 3 | from tests.odm.utils import SqliteMixin 4 | 5 | 6 | class TestSql(SqliteMixin, postgresql.TestPostgreSql): 7 | pass 8 | -------------------------------------------------------------------------------- /tests/odm/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class SqliteMixin: 4 | config_params = {'DATASTORE': 'sqlite://'} 5 | 6 | 7 | class OdmUtils: 8 | config_file = 'tests.odm' 9 | 10 | async def _create_task(self, token, subject='This is a task', person=None, 11 | **data): 12 | data['subject'] = subject 13 | if person: 14 | data['assigned'] = person['id'] 15 | request = await self.client.post(self.api_url('tasks'), 16 | json=data, 17 | token=token) 18 | data = self.json(request.response, 201) 19 | self.assertIsInstance(data, dict) 20 | self.assertTrue('id' in data) 21 | self.assertEqual(data['subject'], subject) 22 | self.assertTrue('created' in data) 23 | self.assertEqual(len(request.cache.new_items), 1) 24 | self.assertEqual(request.cache.new_items[0]['id'], data['id']) 25 | self.assertFalse(request.cache.new_items_before_commit) 26 | return data 27 | 28 | async def _get_task(self, token, id): 29 | request = await self.client.get( 30 | '/tasks/{}'.format(id), 31 | token=token) 32 | response = request.response 33 | self.assertEqual(response.status_code, 200) 34 | data = self.json(response) 35 | self.assertIsInstance(data, dict) 36 | self.assertTrue('id' in data) 37 | return data 38 | 39 | async def _delete_task(self, token, id): 40 | request = await self.client.delete( 41 | '/tasks/{}'.format(id), 42 | token=token) 43 | response = request.response 44 | self.assertEqual(response.status_code, 204) 45 | 46 | async def _create_person(self, token, username, name=None): 47 | name = name or username 48 | request = await self.client.post( 49 | '/people', 50 | json={'username': username, 'name': name}, 51 | token=token) 52 | data = self.json(request.response, 201) 53 | self.assertIsInstance(data, dict) 54 | self.assertTrue('id' in data) 55 | self.assertEqual(data['name'], name) 56 | return data 57 | 58 | async def _update_person(self, token, id, username=None, name=None): 59 | request = await self.client.patch( 60 | self.api_url('people/%s' % id), 61 | json={'username': username, 'name': name}, 62 | token=token 63 | ) 64 | data = self.json(request.response, 200) 65 | self.assertIsInstance(data, dict) 66 | self.assertTrue('id' in data) 67 | if name: 68 | self.assertEqual(data['name'], name) 69 | return data 70 | -------------------------------------------------------------------------------- /tests/rest/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | EXTENSIONS = ( 3 | 'lux.ext.rest', 4 | 'lux.ext.odm', 5 | 'lux.ext.auth', 6 | ) 7 | 8 | DEFAULT_CONTENT_TYPE = "application/json" 9 | -------------------------------------------------------------------------------- /tests/rest/test_openapi.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | 3 | 4 | class TestOpenApi(test.AppTestCase): 5 | config_file = 'tests.rest' 6 | config_params = dict( 7 | API_URL=dict( 8 | BASE_PATH='/v1', 9 | TITLE='test api', 10 | DESCRIPTION='this is just a test api' 11 | ) 12 | ) 13 | 14 | def test_api(self): 15 | app = self.app 16 | self.assertTrue(app.apis) 17 | api = app.apis[0] 18 | self.assertTrue(api.cors) 19 | self.assertTrue(api.spec) 20 | self.assertEqual(api.app, app) 21 | 22 | def test_api_spec_path(self): 23 | api = self.app.apis[0] 24 | self.assertTrue(api.spec) 25 | self.assertEqual(api.spec_path, '/v1/spec') 26 | self.assertTrue(api.spec.doc['info']['title'], 'test api') 27 | 28 | def test_api_doc_info(self): 29 | doc = self.app.apis[0].spec.to_dict() 30 | self.assertIsInstance(doc, dict) 31 | self.assertEqual(doc['openapi'], '3.0.0') 32 | info = doc['info'] 33 | self.assertEqual(info['title'], 'test api') 34 | self.assertTrue(info['version']) 35 | 36 | def test_api_spec_paths(self): 37 | api = self.app.apis[0] 38 | self.assertTrue(api.spec.doc['paths']) 39 | -------------------------------------------------------------------------------- /tests/rest/test_pagination.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | 3 | from pulsar.apps.wsgi.utils import query_dict 4 | 5 | from lux.utils import test 6 | from lux.ext.rest import Pagination 7 | 8 | 9 | class TestUtils(test.TestCase): 10 | config_file = 'tests.rest' 11 | 12 | def test_last_link(self): 13 | app = self.application() 14 | request = app.wsgi_request() 15 | pagination = Pagination() 16 | # 17 | pag = pagination(request, [], 0, 25, 0) 18 | self.assertFalse('first' in pag) 19 | self.assertFalse('prev' in pag) 20 | self.assertFalse('next' in pag) 21 | self.assertFalse('last' in pag) 22 | # 23 | pag = pagination(request, [], 120, 25, 0) 24 | query = query_dict(urlparse(pag['next']).query) 25 | self.assertEqual(query['offset'], '25') 26 | query = query_dict(urlparse(pag['last']).query) 27 | self.assertEqual(query['offset'], '100') 28 | # 29 | pag = pagination(request, [], 120, 25, 75) 30 | query = query_dict(urlparse(pag['last']).query) 31 | self.assertEqual(query['offset'], '100') 32 | self.assertFalse('next' in pag) 33 | # 34 | pag = pagination(request, [], 120, 25, 50) 35 | query = query_dict(urlparse(pag['next']).query) 36 | self.assertEqual(query['offset'], '75') 37 | query = query_dict(urlparse(pag['last']).query) 38 | self.assertEqual(query['offset'], '100') 39 | 40 | def test_custom_offset(self): 41 | app = self.application() 42 | request = app.wsgi_request() 43 | pagination = Pagination() 44 | # 45 | pag = pagination(request, [], 27, 5, 2) 46 | self.assertFalse('prev' in pag) 47 | query = query_dict(urlparse(pag['first']).query) 48 | self.assertEqual(query['offset'], '0') 49 | self.assertEqual(query['limit'], '2') 50 | query = query_dict(urlparse(pag['last']).query) 51 | self.assertEqual(query['offset'], '22') 52 | self.assertEqual(query['limit'], '5') 53 | # 54 | pag = pagination(request, [], 27, 5, 22) 55 | self.assertFalse('next' in pag) 56 | self.assertFalse('last' in pag) 57 | query = query_dict(urlparse(pag['first']).query) 58 | self.assertEqual(query['offset'], '0') 59 | self.assertEqual(query['limit'], '2') 60 | query = query_dict(urlparse(pag['prev']).query) 61 | self.assertEqual(query['offset'], '17') 62 | self.assertEqual(query['limit'], '5') 63 | -------------------------------------------------------------------------------- /tests/scss/placeholder.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/tests/scss/placeholder.scss -------------------------------------------------------------------------------- /tests/sessions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantmind/lux/7318fcd86c77616aada41d8182a04339680a554c/tests/sessions/__init__.py -------------------------------------------------------------------------------- /tests/sessions/test_sqlite.py: -------------------------------------------------------------------------------- 1 | """Test sessions backend with Sqlite""" 2 | import tests.sessions.test_postgresql as postgresql 3 | 4 | 5 | class TestSqlite(postgresql.TestPostgreSql): 6 | config_params = {'DATASTORE': 'sqlite://'} 7 | -------------------------------------------------------------------------------- /tests/sockjs/__init__.py: -------------------------------------------------------------------------------- 1 | from lux.core import LuxExtension 2 | 3 | from tests.config import * # noqa 4 | 5 | EXTENSIONS = ['lux.ext.base', 6 | 'lux.ext.rest', 7 | 'lux.ext.odm', 8 | 'lux.ext.auth', 9 | 'lux.ext.sockjs', 10 | 'tests.odm'] 11 | 12 | WS_URL = '/testws' 13 | API_URL = '' 14 | AUTHENTICATION_BACKENDS = ['lux.ext.auth:TokenBackend'] 15 | DATASTORE = 'postgresql+green://lux:luxtest@127.0.0.1:5432/luxtests' 16 | PUBSUB_STORE = redis_cache_server # noqa 17 | DEFAULT_POLICY = [ 18 | { 19 | "resource": "*", 20 | "action": "*" 21 | } 22 | ] 23 | 24 | 25 | class Extension(LuxExtension): 26 | 27 | def ws_add(self, request): 28 | """Add two numbers 29 | """ 30 | a = request.params.get('a', 0) 31 | b = request.params.get('b', 0) 32 | return a + b 33 | 34 | def ws_echo(self, request): 35 | """Echo parameters 36 | """ 37 | return request.params 38 | -------------------------------------------------------------------------------- /tests/sockjs/fixtures/permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "username": "pippo", 5 | "password": "pippo", 6 | "email": "littlepippo@pippo.com", 7 | "first_name": "Little Pippo", 8 | "active": true 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tests/templates/home.html: -------------------------------------------------------------------------------- 1 | {{ html_main }} 2 | -------------------------------------------------------------------------------- /tests/web/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from lux.utils import test 4 | 5 | 6 | class WebsiteTest(test.WebApiTestCase): 7 | fixtures_path = os.path.join(os.path.dirname(__file__), 'fixtures') 8 | config_file = 'example.webapi.config' 9 | web_config_file = 'example.website.config' 10 | 11 | async def _login(self, credentials=None, csrf=None): 12 | '''Return a token for a new superuser 13 | ''' 14 | # We need csrf and cookie to successfully login 15 | cookie = None 16 | if csrf is None: 17 | request = await self.webclient.get('/login') 18 | bs = self.bs(request.response, 200) 19 | csrf = self.authenticity_token(bs) 20 | cookie = self.cookie(request.response) 21 | self.assertTrue(cookie.startswith('test-website=')) 22 | csrf = csrf or {} 23 | if credentials is None: 24 | credentials = 'testuser' 25 | if not isinstance(credentials, dict): 26 | credentials = dict(username=credentials, 27 | password=credentials) 28 | credentials.update(csrf) 29 | 30 | # Get new token 31 | request = await self.webclient.post( 32 | '/login', 33 | json=credentials, 34 | cookie=cookie 35 | ) 36 | self.assertTrue(self.json(request.response, 200)['success']) 37 | cookie2 = self.cookie(request.response) 38 | self.assertTrue(cookie.startswith('test-website=')) 39 | self.assertNotEqual(cookie, cookie2) 40 | self.assertFalse(request.cache.user.is_authenticated()) 41 | return cookie2 42 | -------------------------------------------------------------------------------- /tests/web/fixtures/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "username": "pippo", 5 | "password": "pippo", 6 | "email": "pippo@test.com", 7 | "first_name": "Pippo", 8 | "active": true 9 | }, 10 | { 11 | "username": "toni", 12 | "password": "toni", 13 | "email": "toni@test.com", 14 | "active": true 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/web/test_alone.py: -------------------------------------------------------------------------------- 1 | from lux.utils import test 2 | 3 | 4 | class ContentTest(test.AppTestCase): 5 | config_file = 'example.webalone.config' 6 | 7 | async def test_media_404(self): 8 | request = await self.client.get('/media/') 9 | self.assertEqual(request.response.status_code, 404) 10 | request = await self.client.get('/media/bla.png') 11 | self.assertEqual(request.response.status_code, 404) 12 | 13 | async def test_media_200(self): 14 | request = await self.client.get('/media/lux.png') 15 | self.assertEqual(request.response.status_code, 200) 16 | -------------------------------------------------------------------------------- /tests/web/test_api.py: -------------------------------------------------------------------------------- 1 | from tests import web 2 | 3 | 4 | class ApiTest(web.WebsiteTest): 5 | 6 | async def test_base(self): 7 | request = await self.client.get('/') 8 | data = self.json(request.response, 200) 9 | self.assertTrue(data) 10 | self.assertTrue('user_url' in data) 11 | 12 | # CONTENT API 13 | async def test_article_list(self): 14 | request = await self.client.get('/contents/articles?priority:gt=0') 15 | data = self.json(request.response, 200)['result'] 16 | self.assertEqual(len(data), 3) 17 | 18 | async def test_article_list_priority_query(self): 19 | request = await self.client.get( 20 | '/contents/articles?priority=3&priority=2') 21 | data = self.json(request.response, 200)['result'] 22 | self.assertEqual(len(data), 1) 23 | self.assertEqual(data[0]['priority'], 2) 24 | self.assertEqual(data[0]['title'], 'Just a test') 25 | 26 | async def test_options_article_links(self): 27 | request = await self.client.options('/contents/articles/_links') 28 | self.assertEqual(request.response.status_code, 200) 29 | 30 | async def test_article_links(self): 31 | request = await self.client.get('/contents/articles/_links') 32 | data = self.json(request.response, 200)['result'] 33 | self.assertEqual(len(data), 2) 34 | 35 | async def test_options_article(self): 36 | request = await self.client.options('/contents/articles/fooo') 37 | self.assertEqual(request.response.status_code, 200) 38 | 39 | async def test_get_article_404(self): 40 | request = await self.client.get('/contents/articles/fooo') 41 | self.json(request.response, 404) 42 | 43 | async def test_head_article_404(self): 44 | request = await self.client.head('/contents/articles/fooo') 45 | self.assertEqual(request.response.status_code, 404) 46 | 47 | async def test_get_article_200(self): 48 | request = await self.client.get('/contents/articles/test') 49 | data = self.json(request.response, 200) 50 | self.assertEqual(data['path'], '/articles/test') 51 | 52 | async def test_head_article_200(self): 53 | request = await self.client.head('/contents/articles/test') 54 | self.assertEqual(request.response.status_code, 200) 55 | -------------------------------------------------------------------------------- /tests/web/test_auth.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from tests import web 4 | 5 | 6 | class AuthTest(web.WebsiteTest): 7 | web_config_params = dict(SESSION_EXPIRY=5) 8 | 9 | def test_apps(self): 10 | self.assertEqual(self.web.meta.name, 'example.website') 11 | self.assertEqual(self.app.meta.name, 'example.webapi') 12 | 13 | async def test_get_login(self): 14 | request = await self.webclient.get('/login') 15 | bs = self.bs(request.response, 200) 16 | self.assertEqual(str(bs.title), 'website.com') 17 | 18 | async def test_login_fail(self): 19 | request = await self.webclient.get('/login') 20 | bs = self.bs(request.response, 200) 21 | credentials = self.authenticity_token(bs) 22 | cookie = self.cookie(request.response) 23 | credentials['username'] = 'xbjhxbhxs' 24 | credentials['password'] = 'sdcsacccscd' 25 | request = await self.webclient.post('/login', 26 | json=credentials, 27 | cookie=cookie) 28 | self.assertValidationError(request.response, 29 | text='Invalid credentials') 30 | 31 | async def test_login(self): 32 | await self._login() 33 | 34 | async def test_authenticated_view(self): 35 | cookie = await self._login() 36 | request = await self.webclient.get('/', cookie=cookie) 37 | self.bs(request.response, 200) 38 | self.assertTrue(request.cache.user.is_authenticated()) 39 | self.assertFalse(request.response.cookies) 40 | 41 | async def test_invalid_authenticated_view(self): 42 | cookie = await self._login() 43 | request = await self.webclient.get('/', cookie=cookie) 44 | self.bs(request.response, 200) 45 | self.assertTrue(request.cache.user.is_authenticated()) 46 | self.assertFalse(request.response.cookies) 47 | # Wait for 5 seconds 48 | await asyncio.sleep(self.web.config['SESSION_EXPIRY']+1) 49 | # The session is now expired 50 | request = await self.webclient.get('/', cookie=cookie) 51 | self.assertFalse(request.cache.user.is_authenticated()) 52 | -------------------------------------------------------------------------------- /tests/web/test_text.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlsplit 2 | 3 | from tests import web 4 | 5 | 6 | class ContentTest(web.WebsiteTest): 7 | 8 | async def test_get(self): 9 | request = await self.webclient.get('/articles/test') 10 | bs = self.bs(request.response, 200) 11 | self.assertEqual(str(bs.title), 'Just a test') 12 | 13 | async def test_get_settings_404(self): 14 | request = await self.webclient.get('/settings') 15 | self.assertEqual(request.response.status_code, 404) 16 | 17 | async def test_get_settings_foo_404(self): 18 | request = await self.webclient.get('/settings/foo') 19 | self.assertEqual(request.response.status_code, 404) 20 | 21 | async def test_get_settings_foo_303(self): 22 | request = await self.webclient.get('/testing') 23 | self.assertEqual(request.response.status_code, 302) 24 | location = request.response['location'] 25 | self.assertTrue(urlsplit(location).path, '/testing/bla') 26 | 27 | async def test_get_sitemap(self): 28 | request = await self.webclient.get('/sitemap.xml') 29 | bs = self.xml(request.response, 200) 30 | sitemap = bs.findAll('sitemap') 31 | self.assertTrue(sitemap) 32 | 33 | async def test_login_json_form(self): 34 | request = await self.webclient.get('/login/jsonform') 35 | data = self.json(request.response, 200) 36 | self.assertIsInstance(data, dict) 37 | self.assertTrue('children' in data) 38 | --------------------------------------------------------------------------------