├── .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 |
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 |
2 | - Quantmind
3 | - Site build with Lux on python {{ site_python_version }}
4 | - Docs build with Sphinx
5 | - Changelog
6 | - API
7 | - Extensions
8 | - Images
9 |
10 |
--------------------------------------------------------------------------------
/example/content/context/footer2.md:
--------------------------------------------------------------------------------
1 |
2 |
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 |
5 |
6 |
7 |
8 | ### PNG (300x300)
9 |
10 |
11 |
12 |
13 |
14 |
15 | ### Lux Powered
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
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 |
--------------------------------------------------------------------------------