├── tests
├── __init__.py
├── app_five
│ ├── __init__.py
│ ├── models.py
│ └── conf.py
├── app_four
│ ├── __init__.py
│ ├── models.py
│ └── conf.py
├── app_one
│ ├── __init__.py
│ ├── models.py
│ └── conf.py
├── app_six
│ ├── __init__.py
│ └── models.py
├── app_three
│ ├── __init__.py
│ └── models.py
├── app_two
│ ├── __init__.py
│ └── models.py
├── docker
│ ├── __init__.py
│ └── streams
│ │ ├── __init__.py
│ │ ├── templates
│ │ ├── index.html
│ │ ├── _crax_tests_crax_conf_py.html
│ │ ├── _crax_tests_crax_crax_py.html
│ │ ├── _crax_tests_crax_logger_py.html
│ │ ├── _crax_tests_crax_request_py.html
│ │ ├── _crax_tests_crax_urls_py.html
│ │ ├── _crax_tests_crax_utils_py.html
│ │ ├── _crax_tests_crax_views_py.html
│ │ ├── _crax_tests_crax___init___py.html
│ │ ├── _crax_tests_crax_auth_models_py.html
│ │ ├── _crax_tests_crax_data_types_py.html
│ │ ├── _crax_tests_crax_database_env_py.html
│ │ ├── _crax_tests_crax_exceptions_py.html
│ │ ├── _crax_tests_crax_form_data_py.html
│ │ ├── _crax_tests_crax_response_py.html
│ │ ├── _crax_tests_crax_auth___init___py.html
│ │ ├── _crax_tests_crax_auth_middleware_py.html
│ │ ├── _crax_tests_crax_commands___init___py.html
│ │ ├── _crax_tests_crax_commands_command_py.html
│ │ ├── _crax_tests_crax_commands_history_py.html
│ │ ├── _crax_tests_crax_commands_migrate_py.html
│ │ ├── _crax_tests_crax_database___init___py.html
│ │ ├── _crax_tests_crax_database_command_py.html
│ │ ├── _crax_tests_crax_database_model_py.html
│ │ ├── _crax_tests_crax_middleware_base_py.html
│ │ ├── _crax_tests_crax_middleware_cors_py.html
│ │ ├── _crax_tests_crax_response_types_py.html
│ │ ├── _crax_tests_crax_auth_authentication_py.html
│ │ ├── _crax_tests_crax_commands_db_create_all_py.html
│ │ ├── _crax_tests_crax_commands_db_drop_all_py.html
│ │ ├── _crax_tests_crax_middleware___init___py.html
│ │ ├── _crax_tests_crax_middleware_max_body_py.html
│ │ ├── _crax_tests_crax_middleware_x_frame_py.html
│ │ ├── _crax_tests_crax_commands_create_swagger_py.html
│ │ ├── _crax_tests_crax_commands_makemigrations_py.html
│ │ └── get_stream.html
│ │ ├── urls.py
│ │ └── app.py
├── test_files
│ ├── __init__.py
│ ├── test_crax.sqlite
│ ├── media_files
│ │ ├── monty_logo.png
│ │ └── python_logo.png
│ ├── utils.py
│ ├── command_three.py
│ ├── command_one.py
│ └── command_four.py
├── config_files
│ ├── __init__.py
│ ├── conf_minimal.py
│ ├── conf_custom_middleware.py
│ ├── conf_middleware_error.py
│ ├── conf_url_wrong_type.py
│ ├── conf_handler_500.py
│ ├── conf_minimal_middleware_no_auth.py
│ ├── conf_max_body_error.py
│ ├── conf_wrong_middleware.py
│ ├── conf_auth_no_auth_middleware.py
│ ├── conf_auth_no_secret.py
│ ├── conf_minimal_two_apps.py
│ ├── conf_auth.py
│ ├── conf_cors_no_dict.py
│ ├── conf_logging.py
│ ├── conf_minimal_middleware_no_auth_handlers.py
│ ├── conf_cors_default.py
│ ├── conf_db_missed.py
│ ├── conf_nested_apps.py
│ ├── conf_db_wrong_type.py
│ ├── conf_logging_custom.py
│ ├── conf_cors_custom.py
│ ├── conf_cors_custom_str.py
│ ├── conf_cors_custom_cookie.py
│ ├── conf_wrong_url_inclusion.py
│ ├── conf_auth_no_default_db.py
│ ├── conf_rest.py
│ ├── conf_auth_right_no_db_options.py
│ ├── conf_auth_right.py
│ └── conf_csrf.py
├── test_app_auth
│ ├── __init__.py
│ ├── templates
│ │ ├── 500.html
│ │ ├── index.html
│ │ └── base.html
│ ├── models.py
│ └── urls_auth.py
├── test_app_common
│ ├── __init__.py
│ ├── templates
│ │ ├── 500.html
│ │ ├── index.html
│ │ └── base.html
│ ├── urls_wrong_patterns.py
│ ├── middleware.py
│ ├── models.py
│ ├── urls.py
│ ├── urls_two_apps.py
│ └── routers.py
├── test_app_nested
│ ├── __init__.py
│ ├── leagueA
│ │ ├── __init__.py
│ │ ├── teams
│ │ │ ├── __init__.py
│ │ │ ├── coaches
│ │ │ │ ├── __init__.py
│ │ │ │ ├── templates
│ │ │ │ │ ├── leagueA_coaches_results.html
│ │ │ │ │ └── leagueA_coaches_index.html
│ │ │ │ ├── urls.py
│ │ │ │ └── controllers.py
│ │ │ ├── players
│ │ │ │ ├── __init__.py
│ │ │ │ ├── templates
│ │ │ │ │ ├── leagueA_players_scores.html
│ │ │ │ │ └── leagueA_players_index.html
│ │ │ │ ├── urls.py
│ │ │ │ └── controllers.py
│ │ │ ├── templates
│ │ │ │ ├── leagueA_teams_scores.html
│ │ │ │ └── leagueA_teams_index.html
│ │ │ ├── urls.py
│ │ │ └── views.py
│ │ ├── templates
│ │ │ ├── leagueA_scores.html
│ │ │ └── leagueA_index.html
│ │ ├── urls.py
│ │ └── handlers.py
│ ├── templates
│ │ ├── test_params_exception.html
│ │ ├── masquerade_1.html
│ │ ├── masquerade_2.html
│ │ ├── masquerade_3.html
│ │ ├── test_params.html
│ │ └── create_url.html
│ ├── urls.py
│ └── routers.py
├── test_selenium
│ ├── __init__.py
│ ├── cart
│ │ ├── __init__.py
│ │ ├── urls.py
│ │ └── routers.py
│ ├── static
│ │ ├── python_logo.png
│ │ └── css
│ │ │ └── app.641d82c2.css
│ ├── conftest.py
│ ├── templates
│ │ ├── index.html
│ │ └── home.html
│ ├── urls.py
│ ├── models.py
│ └── conf.py
├── pytest.ini
├── .coveragerc
├── test_crax.sqlite
├── app.py
├── run.py
├── config.yaml
└── README.md
├── crax
├── middleware
│ ├── __init__.py
│ ├── x_frame.py
│ ├── max_body.py
│ ├── base.py
│ └── cors.py
├── commands
│ ├── __init__.py
│ ├── db_drop_all.py
│ ├── db_create_all.py
│ ├── command.py
│ ├── history.py
│ └── migrate.py
├── __init__.py
├── swagger
│ ├── static
│ │ ├── favicon-16x16.png
│ │ └── favicon-32x32.png
│ ├── __init__.py
│ └── types.py
├── auth
│ ├── __init__.py
│ ├── models.py
│ ├── middleware.py
│ └── authentication.py
├── templates
│ ├── default.html
│ ├── error.html
│ ├── base.html
│ └── swagger.html
├── data_types.py
├── conf.py
├── request.py
├── exceptions.py
├── database
│ ├── __init__.py
│ ├── connection.py
│ └── env.py
├── utils.py
├── logger.py
└── urls.py
├── .flake8
├── docker
├── app
│ ├── pg_init.sh
│ ├── mysql_init.sh
│ ├── MySQLDockerfile
│ ├── PostgresDockerfile
│ ├── launch_crax.sh
│ ├── default
│ ├── Dockerfile
│ └── launch_tests.sh
├── database.conf
├── docker_tests.sh
└── docker-compose.yml
├── MANIFEST.in
├── .pre-commit-config.yaml
├── .gitignore
├── README.md
├── .travis.yml
├── install_gecko.sh
├── run_selenium.sh
├── requirements.txt
├── launch_tests.sh
└── setup.py
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/crax/middleware/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/app_five/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/app_four/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/app_one/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/app_six/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/app_three/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/app_two/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_files/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/config_files/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app_auth/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app_common/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app_nested/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_selenium/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_selenium/cart/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/coaches/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/players/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_conf_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_crax_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_logger_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_request_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_urls_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_utils_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_views_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax___init___py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_auth_models_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_data_types_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_database_env_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_exceptions_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_form_data_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_response_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 110
3 | ignore = F401, W503, E722
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_auth___init___py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_auth_middleware_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands___init___py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands_command_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands_history_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands_migrate_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_database___init___py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_database_command_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_database_model_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_middleware_base_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_middleware_cors_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_response_types_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/crax/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from .command import BaseCommand, from_shell
2 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_auth_authentication_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands_db_create_all_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands_db_drop_all_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_middleware___init___py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_middleware_max_body_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_middleware_x_frame_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/crax/__init__.py:
--------------------------------------------------------------------------------
1 | from .crax import Crax
2 | from .utils import get_settings
3 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands_create_swagger_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/_crax_tests_crax_commands_makemigrations_py.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | filterwarnings = ignore:.*loaded twice! ignoring:UserWarning
--------------------------------------------------------------------------------
/docker/app/pg_init.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | psql test_crax < /tmp/pg_init/pg_init.sql -U crax
--------------------------------------------------------------------------------
/tests/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | concurrency = multiprocessing
3 | parallel = True
4 | omit = */migrations/*
--------------------------------------------------------------------------------
/tests/test_crax.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crax-framework/crax/HEAD/tests/test_crax.sqlite
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | recursive-include crax/swagger/static *
3 | recursive-include crax/templates *
4 |
--------------------------------------------------------------------------------
/tests/test_files/test_crax.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crax-framework/crax/HEAD/tests/test_files/test_crax.sqlite
--------------------------------------------------------------------------------
/docker/app/mysql_init.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | mysql test_crax < /tmp/mysql_init/test_mysql.sql -u crax -pCraxPassword
--------------------------------------------------------------------------------
/crax/swagger/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crax-framework/crax/HEAD/crax/swagger/static/favicon-16x16.png
--------------------------------------------------------------------------------
/crax/swagger/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crax-framework/crax/HEAD/crax/swagger/static/favicon-32x32.png
--------------------------------------------------------------------------------
/tests/test_app_auth/templates/500.html:
--------------------------------------------------------------------------------
1 |
2 |
Test custom 500 handler
3 |
4 |
--------------------------------------------------------------------------------
/tests/test_app_common/templates/500.html:
--------------------------------------------------------------------------------
1 |
2 |
Test custom 500 handler
3 |
4 |
--------------------------------------------------------------------------------
/tests/test_files/media_files/monty_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crax-framework/crax/HEAD/tests/test_files/media_files/monty_logo.png
--------------------------------------------------------------------------------
/tests/test_files/media_files/python_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crax-framework/crax/HEAD/tests/test_files/media_files/python_logo.png
--------------------------------------------------------------------------------
/tests/test_selenium/static/python_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/crax-framework/crax/HEAD/tests/test_selenium/static/python_logo.png
--------------------------------------------------------------------------------
/tests/test_app_common/urls_wrong_patterns.py:
--------------------------------------------------------------------------------
1 | from crax.urls import include
2 |
3 | url_list = [
4 | include("test_app_missed.urls"),
5 | ]
6 |
--------------------------------------------------------------------------------
/crax/swagger/__init__.py:
--------------------------------------------------------------------------------
1 | from crax.views import SwaggerCrax
2 | from crax.urls import Route, Url
3 |
4 | urls = [Route(Url("/api_doc", tag="crax"), SwaggerCrax)]
5 |
--------------------------------------------------------------------------------
/tests/test_selenium/cart/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 | from .routers import Cart
3 |
4 | url_list = [
5 | Route(Url("/api/cart"), Cart),
6 | ]
7 |
--------------------------------------------------------------------------------
/crax/auth/__init__.py:
--------------------------------------------------------------------------------
1 | from crax.auth.authentication import login, create_password, create_user
2 |
3 | __all__ = [
4 | "create_password",
5 | "login",
6 | "create_user",
7 | ]
8 |
--------------------------------------------------------------------------------
/tests/test_app_common/middleware.py:
--------------------------------------------------------------------------------
1 | from crax.middleware.base import ResponseMiddleware
2 |
3 |
4 | class ReplaceMiddleware(ResponseMiddleware):
5 | async def process_headers(self):
6 | pass
7 |
--------------------------------------------------------------------------------
/tests/test_app_auth/models.py:
--------------------------------------------------------------------------------
1 | import sqlalchemy as sa
2 | from crax.database.model import BaseTable
3 |
4 |
5 | class Customer(BaseTable):
6 | database = "users"
7 | bio = sa.Column(sa.String(length=100))
8 |
--------------------------------------------------------------------------------
/tests/test_app_nested/templates/test_params_exception.html:
--------------------------------------------------------------------------------
1 | {% extends 'missed_base.html' %}
2 | {% block content %}
3 |
4 |
Missed Base
5 |
6 | {% endblock %}
--------------------------------------------------------------------------------
/crax/templates/default.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content %}
3 |
4 |
WELCOME TO CRAX
5 | Create your first application
6 |
7 | {% endblock %}
--------------------------------------------------------------------------------
/tests/test_app_nested/templates/masquerade_1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Masquerade One
6 |
7 |
8 | Test Masquerade
9 |
10 |
--------------------------------------------------------------------------------
/tests/test_app_nested/templates/masquerade_2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Masquerade Two
6 |
7 |
8 | Test Masquerade
9 |
10 |
--------------------------------------------------------------------------------
/tests/test_app_nested/templates/masquerade_3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Masquerade Three
6 |
7 |
8 | Test Masquerade
9 |
10 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/templates/leagueA_scores.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA
6 |
7 |
8 | LeagueA Scores Page
9 |
10 |
--------------------------------------------------------------------------------
/tests/app.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from crax import Crax
4 | from crax.commands import from_shell
5 |
6 |
7 | app = Crax(settings="test_selenium.conf", debug=True)
8 |
9 | if __name__ == "__main__":
10 | if sys.argv:
11 | from_shell(sys.argv, app.settings)
12 |
--------------------------------------------------------------------------------
/tests/run.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from crax import Crax
4 | from crax.commands import from_shell
5 |
6 |
7 | app = Crax(settings="test_selenium.conf", debug=True)
8 |
9 | if __name__ == "__main__":
10 | if sys.argv:
11 | from_shell(sys.argv, app.settings)
12 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/ambv/black
3 | rev: stable
4 | hooks:
5 | - id: black
6 | language_version: python3.7
7 | - repo: https://github.com/pre-commit/pre-commit-hooks
8 | rev: v2.3.0
9 | hooks:
10 | - id: flake8
--------------------------------------------------------------------------------
/docker/database.conf:
--------------------------------------------------------------------------------
1 | POSTGRES_USER=crax
2 | POSTGRES_PASSWORD=CraxPassword
3 | POSTGRES_HOST=postgres-container
4 | POSTGRES_PORT=5432
5 | POSTGRES_DB=test_crax
6 |
7 | MYSQL_USER=crax
8 | MYSQL_ROOT_PASSWORD=CraxPassword
9 | MYSQL_PASSWORD=CraxPassword
10 | MYSQL_DATABASE=test_crax
11 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/templates/leagueA_teams_scores.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA Teams
6 |
7 |
8 | LeagueA Teams Scores Page
9 | {{ team_name }}
10 |
11 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/coaches/templates/leagueA_coaches_results.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA Players
6 |
7 |
8 | LeagueA Coaches Results Page
9 | {{ team_name }} {{ coach }}
10 |
11 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/players/templates/leagueA_players_scores.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA Players
6 |
7 |
8 | LeagueA Players Scores Page
9 | {{ team_name }} {{ player }}
10 |
11 |
--------------------------------------------------------------------------------
/docker/app/MySQLDockerfile:
--------------------------------------------------------------------------------
1 | FROM mariadb
2 |
3 | RUN mkdir -p /tmp/mysql_init/
4 | COPY app/test_mysql.sql /tmp/mysql_init/
5 | COPY app/mysql_init.sh /docker-entrypoint-initdb.d/
6 |
7 | ENV MYSQL_USER=crax
8 | ENV MYSQL_ROOT_PASSWORD=CraxPassword
9 | ENV MYSQL_PASSWORD=CraxPassword
10 | ENV MYSQL_PORT=3307
11 | ENV MYSQL_DATABASE=test_crax
--------------------------------------------------------------------------------
/docker/app/PostgresDockerfile:
--------------------------------------------------------------------------------
1 | FROM postgres:11
2 |
3 | RUN mkdir -p /tmp/pg_init/
4 | COPY app/pg_init.sql /tmp/pg_init/
5 | COPY app/pg_init.sh /docker-entrypoint-initdb.d/
6 |
7 | ENV POSTGRES_USER=crax
8 | ENV POSTGRES_PASSWORD=CraxPassword
9 | ENV POSTGRES_HOST=postgres-container
10 | ENV POSTGRES_PORT=5433
11 | ENV POSTGRES_DB=test_crax
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/coaches/templates/leagueA_coaches_index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA Coaches
6 |
7 |
8 | LeagueA Coaches Index Page
9 | Params: {{ params_team_name }} {{ coach }}
10 |
11 |
--------------------------------------------------------------------------------
/tests/test_app_auth/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content %}
3 |
4 |
Testing index page
5 |
6 | {% if data %} {{ data }} {% endif %}
7 | {% if files %}
8 | {% for file in files %}}
9 | {{ file }}
10 | {% endfor %}}
11 | {% endif %}
12 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/tests/test_app_common/models.py:
--------------------------------------------------------------------------------
1 | import sqlalchemy as sa
2 | from crax.auth.models import User
3 |
4 | from crax.database.model import BaseTable
5 |
6 |
7 | class Notes(BaseTable):
8 | id = sa.Column(sa.Integer, primary_key=True)
9 | user_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
10 | text = sa.Column(sa.String(length=100))
11 | completed = sa.Column(sa.Boolean)
12 |
--------------------------------------------------------------------------------
/tests/test_app_common/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content %}
3 |
4 |
Testing index page
5 |
6 | {% if data %} {{ data }} {% endif %}
7 | {% if files %}
8 | {% for file in files %}}
9 | {{ file }}
10 | {% endfor %}}
11 | {% endif %}
12 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/tests/docker/streams/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 |
3 | from .routers import Home, StreamView, CoverageView, WsHome
4 |
5 | url_list = [
6 | Route(urls=(Url(r"/")), handler=Home),
7 | Route(urls=(Url(r"/coverage/", masquerade=True)), handler=CoverageView),
8 | Route(urls=(Url(r"/", scheme="websocket")), handler=WsHome),
9 | Route(urls=(Url(r"/get_stream")), handler=StreamView),
10 | ]
11 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url, include
2 |
3 | from .handlers import (
4 | LeagueAIndex,
5 | LeagueAScores,
6 | )
7 |
8 | url_list = [
9 | Route(urls=(Url("/first_league/", name="first_league")), handler=LeagueAIndex),
10 | Route(
11 | urls=(Url("/first_league_scores", name="first_league_scores")),
12 | handler=LeagueAScores,
13 | ),
14 | ]
15 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/templates/leagueA_teams_index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA Teams
6 |
7 |
8 | LeagueA Teams Index Page
9 | {% if query_team_name %}
10 | Query: {{ query_team_name.0 }}
11 | {% endif %}
12 | Params: {{ params_team_name }}
13 |
14 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 |
3 | from .views import (
4 | TeamLeagueAIndex,
5 | TeamLeagueAScores,
6 | )
7 |
8 | url_list = [
9 | Route(
10 | urls=(Url("/first_league//", name="first_league_teams")),
11 | handler=TeamLeagueAIndex,
12 | ),
13 | Route(urls=(Url("/first_league_scores//")), handler=TeamLeagueAScores),
14 | ]
15 |
--------------------------------------------------------------------------------
/tests/test_app_nested/templates/test_params.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block content %}
3 | Test Get Params
4 | {% if query %}
5 | {% for k, v in query.items() %}
6 | {{ k }} = {{ v.0 }}
7 | {% endfor %}
8 | {% endif %}
9 |
10 | {% if params %}
11 | {% for k, v in params.items() %}
12 | {{ k }} = {{ v }}
13 | {% endfor %}
14 | {% endif %}
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/crax/commands/db_drop_all.py:
--------------------------------------------------------------------------------
1 | """
2 | Command to drop all database tables
3 | """
4 | from crax.database.command import MetadataCommands, OPTIONS
5 |
6 |
7 | class DropAll(MetadataCommands):
8 | def drop_all(self) -> None:
9 | self.metadata.drop_all()
10 |
11 |
12 | if __name__ == "__main__": # pragma: no cover
13 | # This code excluded from reports 'cause it was tested directly
14 | drop_all = DropAll(OPTIONS).drop_all
15 | drop_all()
16 |
--------------------------------------------------------------------------------
/tests/app_three/models.py:
--------------------------------------------------------------------------------
1 | from crax.database.model import BaseTable
2 | import sqlalchemy as sa
3 | from tests.app_one.models import CustomerB, Vendor
4 |
5 |
6 | class Orders(BaseTable):
7 | # database = 'custom'
8 | staff = sa.Column(sa.String(length=100), nullable=False)
9 | price = sa.Column(sa.Integer, nullable=False)
10 | customer_id = sa.Column(sa.Integer, sa.ForeignKey(CustomerB.id))
11 | vendor_id = sa.Column(sa.Integer, sa.ForeignKey(Vendor.id))
12 |
--------------------------------------------------------------------------------
/tests/config_files/conf_minimal.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.urls import url_list
5 | except ImportError:
6 | from ..test_app_common.urls import url_list
7 |
8 | ALLOWED_HOSTS = ["*"]
9 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | SECRET_KEY = "qwerty1234567"
11 | MIDDLEWARE = []
12 |
13 | APPLICATIONS = ["test_app_common"]
14 | URL_PATTERNS = url_list
15 | STATIC_DIRS = []
16 |
17 | DATABASES = {}
18 | ERROR_HANDLERS = {}
19 |
--------------------------------------------------------------------------------
/tests/test_app_nested/templates/create_url.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test Url Creation
6 |
7 |
8 | Test Url Creation
9 | Test Url One
10 | Test Url Two
11 |
12 |
--------------------------------------------------------------------------------
/tests/config_files/conf_custom_middleware.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.urls_two_apps import url_list
5 | except ImportError:
6 | from ..test_app_common.urls_two_apps import url_list
7 |
8 |
9 | ALLOWED_HOSTS = ["*"]
10 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
11 | SECRET_KEY = "qwerty1234567"
12 | MIDDLEWARE = [
13 | "test_app.middleware.ReplaceMiddleware",
14 | ]
15 |
16 | APPLICATIONS = ["test_app_common"]
17 | URL_PATTERNS = url_list
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.pyc
3 | .coverage
4 | .directory
5 | __pycache__
6 | venv
7 | crax_egg_info
8 | .pytest_cache
9 | crax/auth/migrations
10 | tests/app_one/migrations
11 | tests/app_two/migrations
12 | tests/app_three/migrations
13 | tests/app_four/migrations
14 | tests/app_five/migrations
15 | tests/app_six/migrations
16 | tests/alembic.ini
17 | tests/alembic_env
18 | /tests/test.log
19 | /docker/app/crax_tests/
20 | /docker/app/test_mysql.sql
21 | /docker/app/pg_init.sql
22 | /crax.egg-info/
23 | /dist/
24 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/players/templates/leagueA_players_index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA Players
6 |
7 |
8 | LeagueA Players Index Page
9 | {% if query_team_name %}
10 | Query: {{ query_team_name.0 }}
11 | {% endif %}
12 | Params: {{ params_team_name }} {{ player }}
13 | {% if optional %} {{ optional }}
{% endif %}
14 |
15 |
--------------------------------------------------------------------------------
/crax/middleware/x_frame.py:
--------------------------------------------------------------------------------
1 | """
2 | Dummy Clickjacking Protection.
3 | """
4 | from crax.middleware.base import RequestMiddleware
5 | from crax.utils import get_settings_variable
6 |
7 | from crax.data_types import Request
8 |
9 |
10 | class XFrameMiddleware(RequestMiddleware):
11 | async def process_headers(self) -> Request:
12 | x_frame_options = get_settings_variable("X_FRAME_OPTIONS", default="SAMEORIGIN")
13 | self.request.response_headers["X-Frame-Options"] = x_frame_options
14 | return self.request
15 |
--------------------------------------------------------------------------------
/tests/config_files/conf_middleware_error.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.urls import url_list
5 | except ImportError:
6 | from ..test_app_common.urls import url_list
7 |
8 | ALLOWED_HOSTS = ["*"]
9 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | SECRET_KEY = "qwerty1234567"
11 | MIDDLEWARE = [
12 | "crax.middleware.x_frame.XF",
13 | ]
14 |
15 | APPLICATIONS = ["test_app_common", "test_app_nested"]
16 | URL_PATTERNS = url_list
17 | STATIC_DIRS = ["static", "test_app/static"]
18 |
--------------------------------------------------------------------------------
/tests/config_files/conf_url_wrong_type.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax.urls import Route
4 |
5 | try:
6 | from test_app_common.routers import Home
7 | except ImportError:
8 | from ..test_app_common.routers import Home
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = []
14 |
15 | APPLICATIONS = ["test_app_common"]
16 | URL_PATTERNS = [Route("/", Home)]
17 | STATIC_DIRS = []
18 |
19 | DATABASES = {}
20 | ERROR_HANDLERS = {}
21 |
--------------------------------------------------------------------------------
/tests/test_selenium/cart/routers.py:
--------------------------------------------------------------------------------
1 | from crax.response_types import JSONResponse
2 | from crax.views import TemplateView
3 |
4 |
5 | class Cart(TemplateView):
6 | template = "index.html"
7 | methods = ["POST"]
8 | enable_csrf = False
9 |
10 | async def post(self):
11 | """
12 | :par pic: file
13 | :content_type: multipart/form-data
14 | """
15 | if self.request.files:
16 | await self.request.files["pic"].save()
17 | response = JSONResponse(self.request, self.request.post)
18 | return response
19 |
--------------------------------------------------------------------------------
/tests/test_selenium/conftest.py:
--------------------------------------------------------------------------------
1 | from pytest import fixture
2 |
3 |
4 | def pytest_addoption(parser):
5 | parser.addoption("--threads", action="store")
6 | parser.addoption("--delay", action="store")
7 | parser.addoption("--executable", action="store")
8 |
9 |
10 | @fixture()
11 | def threads(request):
12 | return request.config.getoption("--threads")
13 |
14 | @fixture()
15 | def delay(request):
16 | return request.config.getoption("--delay")
17 |
18 | @fixture()
19 | def executable(request):
20 | return request.config.getoption("--executable")
21 |
--------------------------------------------------------------------------------
/crax/commands/db_create_all.py:
--------------------------------------------------------------------------------
1 | """
2 | Yet another way to create database from models, defined in project's applications.
3 | Also database can be created using "migrate" command
4 | """
5 |
6 | from crax.database.command import MetadataCommands, OPTIONS
7 |
8 |
9 | class CreateAll(MetadataCommands):
10 | def create_all(self) -> None:
11 | self.metadata.create_all()
12 |
13 |
14 | if __name__ == "__main__": # pragma: no cover
15 | # This code excluded from reports 'cause it was tested directly
16 | create_all = CreateAll(OPTIONS).create_all
17 | create_all()
18 |
--------------------------------------------------------------------------------
/tests/app_six/models.py:
--------------------------------------------------------------------------------
1 | from crax.database.model import BaseTable
2 | import sqlalchemy as sa
3 |
4 |
5 | class BaseDiscount(BaseTable):
6 | database = "custom"
7 | name = sa.Column(sa.String(length=100), nullable=False)
8 | percent = sa.Column(sa.Integer, nullable=False)
9 | # start_date = sa.Column(sa.DateTime(), nullable=True)
10 |
11 | class Meta:
12 | abstract = True
13 |
14 |
15 | class CustomerDiscount(BaseDiscount):
16 | database = "custom"
17 | pass
18 |
19 |
20 | class VendorDiscount(BaseDiscount):
21 | database = "custom"
22 | pass
23 |
--------------------------------------------------------------------------------
/docker/app/launch_crax.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | cd crax_tests && python3 -m venv venv
5 | PYTHON_PATH=$(echo $(pwd)'/venv/bin/python3')
6 | echo -e '\e[32mCRAX VIRTUAL ENVIRONMENT CREATED\032'
7 | ${PYTHON_PATH} -m pip install -r requirements.txt
8 | echo -e '\e[32mREQUIREMENTS INSTALLED\032'
9 | ${PYTHON_PATH} -m pip install uvicorn
10 | echo -e '\e[32mINSTALLED UVICORN\032'
11 | ${PYTHON_PATH} -m pip install .
12 | echo -e '\e[32mINSTALLED CRAX\032'
13 | /etc/init.d/nginx start
14 | bash launch_tests.sh &
15 | cd tests/docker && ${PYTHON_PATH} -m uvicorn streams.app:app --port 5000
16 |
--------------------------------------------------------------------------------
/tests/app_two/models.py:
--------------------------------------------------------------------------------
1 | from crax.database.model import BaseTable
2 | import sqlalchemy as sa
3 |
4 |
5 | class BaseDiscount(BaseTable):
6 | # database = 'custom'
7 | name = sa.Column(sa.String(length=100), nullable=False)
8 | percent = sa.Column(sa.Integer, nullable=False)
9 | # start_date = sa.Column(sa.DateTime(), nullable=True)
10 |
11 | class Meta:
12 | abstract = True
13 |
14 |
15 | class CustomerDiscount(BaseDiscount):
16 | # database = 'custom'
17 | pass
18 |
19 |
20 | class VendorDiscount(BaseDiscount):
21 | # database = 'custom'
22 | pass
23 |
--------------------------------------------------------------------------------
/crax/data_types.py:
--------------------------------------------------------------------------------
1 | """
2 | Type hint variables
3 | """
4 | import typing
5 |
6 | Scope = typing.MutableMapping[str, typing.Any]
7 |
8 | Message = typing.MutableMapping[str, typing.Any]
9 |
10 | Receive = typing.Callable[[], typing.Awaitable[Message]]
11 |
12 | Send = typing.Callable[[Message], typing.Awaitable[None]]
13 |
14 | ASGIApp = typing.TypeVar("ASGIApp")
15 |
16 | Request = typing.TypeVar("Request")
17 |
18 | Model = typing.TypeVar("Model")
19 |
20 | DBQuery = typing.TypeVar("DBQuery")
21 |
22 | Selectable = typing.TypeVar("Selectable")
23 |
24 | ExceptionType = typing.TypeVar("ExceptionType")
25 |
--------------------------------------------------------------------------------
/docker/app/default:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name 127.0.0.1;
4 | client_max_body_size 20M;
5 |
6 | location / {
7 | proxy_pass http://0.0.0.0:5000;
8 | proxy_http_version 1.1;
9 | proxy_set_header Upgrade $http_upgrade;
10 | proxy_set_header Connection "upgrade";
11 |
12 | proxy_redirect off;
13 | proxy_set_header Host $host;
14 | proxy_set_header X-Real-IP $remote_addr;
15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
16 | proxy_set_header X-Forwarded-Host $server_name;
17 | }
18 | }
--------------------------------------------------------------------------------
/crax/templates/error.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% block title %}CRAX {{ status_code }} ERROR{% endblock %}
3 | {% block content %}
4 |
5 |
CRAX {{ status_code }} ERROR
6 | {% if ex_value %}{{ ex_value }}{% endif %}
7 |
8 |
9 |
{% if ex_class %}{{ ex_class }}{% endif %}
10 | {% if traceback %}
11 | {% for tr in traceback %}
{{ tr }}
{% endfor %}
12 | {% endif %}
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/tests/docker/streams/app.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 | from .urls import url_list
5 |
6 |
7 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8 | SECRET_KEY = "qwerty1234567"
9 | MIDDLEWARE = [
10 | "crax.auth.middleware.AuthMiddleware",
11 | "crax.auth.middleware.SessionMiddleware",
12 | ]
13 |
14 | APPLICATIONS = ["streams"]
15 | URL_PATTERNS = url_list
16 | STATIC_DIRS = ["static", "streams/static"]
17 |
18 | DATABASES = {
19 | "default": {"driver": "sqlite", "name": f"/{BASE_URL}/test_crax.sqlite"},
20 | }
21 | app = Crax(settings="streams.app", debug=True)
22 |
--------------------------------------------------------------------------------
/docker/app/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:buster
2 |
3 | RUN apt update
4 | COPY app/crax_tests /crax_tests
5 | COPY app/launch_crax.sh /crax_tests/launch_crax.sh
6 | COPY app/launch_tests.sh /crax_tests/launch_tests.sh
7 | RUN apt install -y --no-install-recommends apt-utils nginx make gcc python3-dev python3-venv
8 |
9 | ENV nginx_vhost /etc/nginx/sites-available/default
10 | ENV nginx_conf /etc/nginx/nginx.conf
11 | COPY app/default ${nginx_vhost}
12 |
13 |
14 | VOLUME ["/etc/nginx/sites-enabled", "/etc/nginx/certs", "/etc/nginx/conf.d", "/var/log/nginx"]
15 |
16 | CMD ["./crax_tests/launch_crax.sh"]
17 | EXPOSE 80 443
18 |
--------------------------------------------------------------------------------
/tests/config_files/conf_handler_500.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.urls import url_list
5 | from test_app_common.routers import Handler500
6 | except ImportError:
7 | from ..test_app_common.urls import url_list
8 | from ..test_app_common.routers import Handler500
9 |
10 |
11 | ALLOWED_HOSTS = ["*"]
12 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13 | SECRET_KEY = "qwerty1234567"
14 | MIDDLEWARE = []
15 |
16 | APPLICATIONS = ["test_app_common"]
17 | URL_PATTERNS = url_list
18 | STATIC_DIRS = []
19 |
20 | DATABASES = {}
21 | ERROR_HANDLERS = {"500_handler": Handler500}
22 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/handlers.py:
--------------------------------------------------------------------------------
1 | from crax.views import TemplateView
2 |
3 |
4 | class LeagueAIndex(TemplateView):
5 | template = "leagueA_index.html"
6 | methods = ["GET"]
7 |
8 | async def get(self):
9 | params = self.request.params
10 | query = self.request.query
11 | self.context = {"query": query, "params": params}
12 |
13 |
14 | class LeagueAScores(TemplateView):
15 | template = "leagueA_scores.html"
16 | methods = ["GET"]
17 |
18 | async def get(self):
19 | params = self.request.params
20 | query = self.request.query
21 | self.context = {"query": query, "params": params}
22 |
--------------------------------------------------------------------------------
/tests/test_selenium/templates/index.html:
--------------------------------------------------------------------------------
1 | Test Crax
--------------------------------------------------------------------------------
/tests/config_files/conf_minimal_middleware_no_auth.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.urls import url_list
5 | except ImportError:
6 | from ..test_app_common.urls import url_list
7 |
8 | ALLOWED_HOSTS = ["*"]
9 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | SECRET_KEY = "qwerty1234567"
11 | MIDDLEWARE = [
12 | "crax.middleware.x_frame.XFrameMiddleware",
13 | "crax.middleware.max_body.MaxBodySizeMiddleware",
14 | "crax.middleware.cors.CorsHeadersMiddleware",
15 | ]
16 |
17 | APPLICATIONS = ["test_app_common"]
18 | URL_PATTERNS = url_list
19 | STATIC_DIRS = []
20 |
21 | DATABASES = {}
22 | ERROR_HANDLERS = {}
23 | X_FRAME_OPTIONS = "DENY"
24 |
--------------------------------------------------------------------------------
/tests/config_files/conf_max_body_error.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.urls import url_list
5 | except ImportError:
6 | from ..test_app_common.urls import url_list
7 |
8 | ALLOWED_HOSTS = ["*"]
9 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | SECRET_KEY = "qwerty1234567"
11 | MIDDLEWARE = [
12 | "crax.middleware.x_frame.XFrameMiddleware",
13 | "crax.middleware.max_body.MaxBodySizeMiddleware",
14 | "crax.middleware.cors.CorsHeadersMiddleware",
15 | ]
16 |
17 | APPLICATIONS = ["test_app_common"]
18 | URL_PATTERNS = url_list
19 | STATIC_DIRS = []
20 |
21 | DATABASES = {}
22 | ERROR_HANDLERS = {}
23 | X_FRAME_OPTIONS = "DENY"
24 | MAX_BODY_SIZE = 8
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CRAX
2 | [](https://github.com/ephmann/crax)
3 | [](https://travis-ci.com/crax-framework/crax)
4 | [](https://codecov.io/gh/crax-framework/crax)
5 |
6 | ### Python Asynchronous Web Development Switz Knife
7 |
8 |
9 |
10 | Crax is a collection of tools put together to make web development fast and easy.
11 | It can also be called a framework because it is ready to create any web application or service out of the box.
12 |
13 | See the documentation at https://crax.wiki/
--------------------------------------------------------------------------------
/tests/app_five/models.py:
--------------------------------------------------------------------------------
1 | import sqlalchemy as sa
2 | from crax.auth.models import User
3 | from tests.app_two.models import CustomerDiscount, VendorDiscount
4 |
5 |
6 | class BaseUser(User):
7 | database = "custom"
8 | bio = sa.Column(sa.String(length=100), nullable=False)
9 |
10 | class Meta:
11 | abstract = True
12 |
13 |
14 | class CustomerA(BaseUser):
15 | database = "custom"
16 | ave_bill = sa.Column(sa.Integer)
17 |
18 |
19 | class CustomerB(BaseUser):
20 | database = "custom"
21 | discount = sa.Column(sa.Integer)
22 | customer_discount_id = sa.Column(sa.Integer, sa.ForeignKey(CustomerDiscount.id))
23 |
24 |
25 | class Vendor(BaseUser):
26 | database = "custom"
27 | vendor_discount_id = sa.Column(sa.Integer, sa.ForeignKey(VendorDiscount.id))
28 |
--------------------------------------------------------------------------------
/tests/app_one/models.py:
--------------------------------------------------------------------------------
1 | import sqlalchemy as sa
2 | from crax.auth.models import User
3 | from tests.app_two.models import CustomerDiscount, VendorDiscount
4 |
5 |
6 | class BaseUser(User):
7 | bio = sa.Column(sa.String(length=100), nullable=False)
8 | # age = sa.Column(sa.Integer(), nullable=True)
9 |
10 | class Meta:
11 | abstract = True
12 |
13 |
14 | class CustomerA(BaseUser):
15 | ave_bill = sa.Column(sa.Integer)
16 |
17 |
18 | class CustomerB(BaseUser):
19 | discount = sa.Column(sa.Integer)
20 | customer_discount_id = sa.Column(sa.Integer, sa.ForeignKey(CustomerDiscount.id))
21 |
22 | class Meta:
23 | order_by = "username"
24 |
25 |
26 | class Vendor(BaseUser):
27 | vendor_discount_id = sa.Column(sa.Integer, sa.ForeignKey(VendorDiscount.id))
28 |
--------------------------------------------------------------------------------
/tests/test_selenium/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test Home
6 |
7 |
8 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/players/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 |
3 | from .controllers import (
4 | LeagueAPlayersIndex,
5 | LeagueAPlayersScores,
6 | )
7 |
8 | url_list = [
9 | Route(
10 | urls=(
11 | Url(
12 | r"/first_league/(?P\w{0,30})/"
13 | r"(?P\w{0,30})/(?:(?P\d+))?",
14 | name="first_league_players",
15 | type="re_path",
16 | )
17 | ),
18 | handler=LeagueAPlayersIndex,
19 | ),
20 | Route(
21 | urls=(
22 | Url(
23 | "/first_league_scores///",
24 | name="first_league_players_scores",
25 | )
26 | ),
27 | handler=LeagueAPlayersScores,
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/crax/swagger/types.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 |
3 |
4 | @dataclass
5 | class SwaggerInfo:
6 | description: str = None
7 | version: str = None
8 | title: str = None
9 | termsOfService: str = None
10 | contact: dict = None
11 | license: dict = None
12 | host: str = None
13 | basePath: str = None
14 | servers: list = field(default_factory=list)
15 |
16 |
17 | @dataclass
18 | class SwaggerTag:
19 | name: str = None
20 | description: str = None
21 | externalDocs: dict = None
22 |
23 |
24 | @dataclass
25 | class SwaggerMethod:
26 | tags: list = field(default_factory=list)
27 | summary: str = ""
28 | description: str = ""
29 | operationId: str = None
30 | parameters: list = field(default_factory=list)
31 | responses: dict = None
32 | requestBody: dict = None
33 |
--------------------------------------------------------------------------------
/tests/app_four/models.py:
--------------------------------------------------------------------------------
1 | import sqlalchemy as sa
2 | from crax.auth.models import User
3 | from tests.app_six.models import CustomerDiscount, VendorDiscount
4 |
5 |
6 | class BaseUser(User):
7 | database = "custom"
8 | bio = sa.Column(sa.String(length=100), nullable=False)
9 | # age = sa.Column(sa.Integer(), nullable=True)
10 |
11 | class Meta:
12 | abstract = True
13 |
14 |
15 | class CustomerA(BaseUser):
16 | database = "custom"
17 | ave_bill = sa.Column(sa.Integer)
18 |
19 |
20 | class CustomerB(BaseUser):
21 | database = "custom"
22 | discount = sa.Column(sa.Integer)
23 | customer_discount_id = sa.Column(sa.Integer, sa.ForeignKey(CustomerDiscount.id))
24 |
25 |
26 | class Vendor(BaseUser):
27 | database = "custom"
28 | vendor_discount_id = sa.Column(sa.Integer, sa.ForeignKey(VendorDiscount.id))
29 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/views.py:
--------------------------------------------------------------------------------
1 | from crax.views import TemplateView
2 |
3 |
4 | class TeamLeagueAIndex(TemplateView):
5 | template = "leagueA_teams_index.html"
6 | methods = ["GET"]
7 |
8 | async def get(self):
9 | params = self.request.params
10 | query = self.request.query
11 | query_team_name = None
12 | if query and "team_name" in query:
13 | query_team_name = query["team_name"]
14 | self.context = {
15 | "query_team_name": query_team_name,
16 | "params_team_name": params["team_name"],
17 | }
18 |
19 |
20 | class TeamLeagueAScores(TemplateView):
21 | template = "leagueA_teams_scores.html"
22 | methods = ["GET"]
23 |
24 | async def get(self):
25 | params = self.request.params
26 | self.context = {"team_name": params["team_name"]}
--------------------------------------------------------------------------------
/tests/config_files/conf_wrong_middleware.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from tests.test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 |
11 | ALLOWED_HOSTS = ["*"]
12 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13 | SECRET_KEY = "qwerty1234567"
14 | MIDDLEWARE = [
15 | "test_app.middleware.WrongMiddleware",
16 | ]
17 |
18 | APPLICATIONS = ["test_app_common", "test_app_nested"]
19 | URL_PATTERNS = url_list
20 | STATIC_DIRS = ["static", "test_app_common/static"]
21 |
22 | DATABASES = {}
23 | ERROR_HANDLERS = {
24 | "500_handler": Handler500,
25 | "404_handler": Handler404,
26 | "405_handler": Handler405,
27 | }
28 |
--------------------------------------------------------------------------------
/docker/docker_tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | DIRS=$(ls ..)
3 |
4 | find .. -name '__pycache__' | xargs rm -rf
5 | mkdir -p app/crax_tests/
6 |
7 | for d in ${DIRS}
8 | do
9 | if [[ $d == *"docker"* ]] || [[ $d == *"venv"* ]]
10 | then
11 | echo "Skipping $d"
12 | else
13 | echo "Collecting $d"
14 | cp -r ../"$d" app/crax_tests/
15 | fi
16 | done
17 |
18 | cp ../requirements.txt app/crax_tests/requirements.txt
19 | cp ../requirements.txt app/crax_tests/tests/requirements.txt
20 | cp ../tests/test_files/test_postgresql.sql app/pg_init.sql
21 | perl -pi -e 's/OWNER TO postgres/OWNER TO crax/;' app/pg_init.sql
22 | cp ../tests/test_files/test_mysql.sql app/test_mysql.sql
23 |
24 | docker-compose down --remove-orphans
25 | docker network create crax_net
26 | docker-compose up --build -d
27 | docker-compose logs -f
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/coaches/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 |
3 | from .controllers import (
4 | LeagueACoachesIndex,
5 | LeagueACoachesResults,
6 | )
7 |
8 | url_list = [
9 | Route(
10 | urls=(
11 | Url(
12 | r"/first_league_coaches/(?P\w{0,30})/"
13 | r"(?P\w{0,30})/",
14 | name="first_league_coaches",
15 | type="re_path",
16 | namespace="leagueA.coaches",
17 | )
18 | ),
19 | handler=LeagueACoachesIndex,
20 | ),
21 | Route(
22 | urls=(
23 | Url(
24 | "/first_league_scores///",
25 | name="first_league_coach_results",
26 | namespace="leagueA.coaches",
27 | )
28 | ),
29 | handler=LeagueACoachesResults,
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial # required for Python >= 3.7
2 | language: python
3 | python:
4 | - "3.7"
5 |
6 | env:
7 | global:
8 | - PGPORT=5433
9 |
10 | install:
11 | - pip install -r requirements.txt
12 | addons:
13 | postgresql: "11"
14 |
15 | services:
16 | - postgresql
17 | - mysql
18 |
19 | before_install:
20 | - sudo apt-get update
21 | - sudo apt-get --yes remove postgresql\*
22 | - sudo apt-get install -y postgresql-11 postgresql-client-11
23 | - sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf
24 | - sudo service postgresql restart 11
25 | - pip install codecov
26 | - export PIP_USE_MIRRORS=true
27 |
28 | before_script:
29 | - psql -c "CREATE USER travis;" -U postgres
30 | - psql -c "CREATE DATABASE test_crax;" -U postgres
31 | - mysql -e "CREATE DATABASE test_crax;"
32 |
33 |
34 | script:
35 | bash launch_tests.sh
36 |
37 | after_success:
38 | - cd tests
39 | - codecov
40 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/coaches/controllers.py:
--------------------------------------------------------------------------------
1 | from crax.views import TemplateView
2 |
3 |
4 | class LeagueACoachesIndex(TemplateView):
5 | template = "leagueA_coaches_index.html"
6 | methods = ["GET"]
7 |
8 | async def get(self):
9 | params = self.request.params
10 | query = self.request.query
11 | query_team_name = None
12 | if query and "team_name" in query:
13 | query_team_name = query["team_name"]
14 | self.context = {
15 | "query_team_name": query_team_name,
16 | "params_team_name": params["team_name"],
17 | "coach": params["coach"],
18 | }
19 |
20 |
21 | class LeagueACoachesResults(TemplateView):
22 | template = "leagueA_coaches_results.html"
23 | methods = ["GET"]
24 |
25 | async def get(self):
26 | params = self.request.params
27 | self.context = {"team_name": params["team_name"], "coach": params["coach"]}
28 |
--------------------------------------------------------------------------------
/tests/test_app_common/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 |
3 | from .routers import (
4 | Home,
5 | GuestView,
6 | EmptyView,
7 | BytesView,
8 | PostView,
9 | PostViewTemplateRender,
10 | guest_view_coroutine,
11 | PostViewTemplateView,
12 | guest_coroutine_view,
13 | ZeroDivision,
14 | WsEcho,
15 | )
16 |
17 | url_list = [
18 | Route(Url("/", name="home"), Home),
19 | Route(Url("guest_view"), GuestView),
20 | Route(Url("no_body"), EmptyView),
21 | Route(Url("bytes_body"), BytesView),
22 | Route(Url("guest_coroutine_view"), guest_coroutine_view),
23 | Route(Url("guest_view_coroutine"), guest_view_coroutine),
24 | Route(Url("post_view"), PostView),
25 | Route(Url("post_view_render"), PostViewTemplateView),
26 | Route(Url("post_view_render_custom"), PostViewTemplateRender),
27 | Route(Url("zero_division"), ZeroDivision),
28 | Route(Url("/", scheme="websocket"), WsEcho),
29 | ]
30 |
--------------------------------------------------------------------------------
/tests/config_files/conf_auth_no_auth_middleware.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_auth.routers import Handler500, Handler403
5 | from test_app_auth.urls_auth import url_list
6 | except ImportError:
7 | from ..test_app_auth.routers import Handler500, Handler403
8 | from ..test_app_auth.urls_auth import url_list
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "SuperSecretKey1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app/static"]
22 |
23 | DATABASES = {}
24 |
25 | ERROR_HANDLERS = {
26 | "500_handler": Handler500,
27 | "403_handler": Handler403,
28 | }
29 | X_FRAME_OPTIONS = "DENY"
30 |
--------------------------------------------------------------------------------
/tests/test_app_common/urls_two_apps.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url, include
2 |
3 | from .routers import (
4 | Home,
5 | GuestView,
6 | PostView,
7 | PostViewTemplateRender,
8 | guest_view_coroutine,
9 | PostViewTemplateView,
10 | guest_coroutine_view,
11 | )
12 |
13 | url_list = [
14 | Route(Url("/"), Home),
15 | Route(Url("guest_view"), GuestView),
16 | Route(Url("guest_coroutine_view"), guest_coroutine_view),
17 | Route(Url("guest_view_coroutine"), guest_view_coroutine),
18 | Route(Url("post_view"), PostView),
19 | Route(Url("post_view_render"), PostViewTemplateView),
20 | Route(Url("post_view_render_custom"), PostViewTemplateRender),
21 | include("tests.test_app_nested.urls"),
22 | include("tests.test_app_nested.leagueA.urls"),
23 | include("tests.test_app_nested.leagueA.teams.urls"),
24 | include("tests.test_app_nested.leagueA.teams.players.urls"),
25 | include("tests.test_app_nested.leagueA.teams.coaches.urls"),
26 | ]
27 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/teams/players/controllers.py:
--------------------------------------------------------------------------------
1 | from crax.views import TemplateView
2 |
3 |
4 | class LeagueAPlayersIndex(TemplateView):
5 | template = "leagueA_players_index.html"
6 | methods = ["GET"]
7 |
8 | async def get(self):
9 | params = self.request.params
10 | query = self.request.query
11 | query_team_name = None
12 | if query and "team_name" in query:
13 | query_team_name = query["team_name"]
14 | self.context = {
15 | "query_team_name": query_team_name,
16 | "params_team_name": params["team_name"],
17 | "player": params["player"],
18 | "optional": params["optional"],
19 | }
20 |
21 |
22 | class LeagueAPlayersScores(TemplateView):
23 | template = "leagueA_players_scores.html"
24 | methods = ["GET"]
25 |
26 | async def get(self):
27 | params = self.request.params
28 | self.context = {"team_name": params["team_name"], "player": params["player"]}
29 |
--------------------------------------------------------------------------------
/crax/middleware/max_body.py:
--------------------------------------------------------------------------------
1 | """
2 | Max body size protection middleware that checks if request body is not
3 | larger then defined in project settings. Surly we can manage this with
4 | web server options, but why not?
5 | """
6 | import typing
7 |
8 | from crax.data_types import ExceptionType
9 | from crax.middleware.base import RequestMiddleware
10 | from crax.utils import get_settings_variable
11 |
12 | from crax.data_types import Request
13 |
14 |
15 | class MaxBodySizeMiddleware(RequestMiddleware):
16 | async def process_headers(self) -> typing.Union[Request, ExceptionType]:
17 | max_body = get_settings_variable("MAX_BODY_SIZE", default=1024 * 1024)
18 | content_length = self.request.headers.get("content-length")
19 | if content_length and int(content_length) > int(max_body):
20 | self.request.status_code = 400
21 | return RuntimeError(
22 | f"Too large body. Allowed body size up to {max_body} bytes"
23 | )
24 | return self.request
25 |
--------------------------------------------------------------------------------
/tests/config_files/conf_auth_no_secret.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_auth.routers import Handler500, Handler403
5 | from test_app_common.urls import url_list
6 | except ImportError:
7 | from ..test_app_auth.routers import Handler500, Handler403
8 | from ..test_app_common.urls import url_list
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 |
13 |
14 | MIDDLEWARE = [
15 | "crax.auth.middleware.AuthMiddleware",
16 | "crax.middleware.x_frame.XFrameMiddleware",
17 | "crax.middleware.max_body.MaxBodySizeMiddleware",
18 | "crax.auth.middleware.SessionMiddleware",
19 | "crax.middleware.cors.CorsHeadersMiddleware",
20 | ]
21 |
22 | APPLICATIONS = ["tests", "test_app", "test_app_auth"]
23 | URL_PATTERNS = url_list
24 | STATIC_DIRS = ["static", "test_app/static"]
25 |
26 | DATABASES = {}
27 |
28 | ERROR_HANDLERS = {
29 | "500_handler": Handler500,
30 | "403_handler": Handler403,
31 | }
32 | X_FRAME_OPTIONS = "DENY"
33 |
--------------------------------------------------------------------------------
/tests/config_files/conf_minimal_two_apps.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from ..test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 | X_FRAME_OPTIONS = "DENY"
30 |
--------------------------------------------------------------------------------
/tests/config_files/conf_auth.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_auth.routers import Handler500, Handler403
5 | from test_app_auth.urls_auth import url_list
6 | except ImportError:
7 | from ..test_app_auth.routers import Handler500, Handler403
8 | from ..test_app_auth.urls_auth import url_list
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.auth.middleware.AuthMiddleware",
15 | "crax.middleware.x_frame.XFrameMiddleware",
16 | "crax.middleware.max_body.MaxBodySizeMiddleware",
17 | "crax.auth.middleware.SessionMiddleware",
18 | "crax.middleware.cors.CorsHeadersMiddleware",
19 | ]
20 |
21 | APPLICATIONS = ["test_app_common", "test_app_auth"]
22 | URL_PATTERNS = url_list
23 | STATIC_DIRS = ["static", "test_app/static"]
24 |
25 | ERROR_HANDLERS = {
26 | "500_handler": Handler500,
27 | "403_handler": Handler403,
28 | "401_handler": Handler403,
29 | }
30 | X_FRAME_OPTIONS = "DENY"
31 |
--------------------------------------------------------------------------------
/tests/config_files/conf_cors_no_dict.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from ..test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 |
30 | X_FRAME_OPTIONS = "DENY"
31 | CORS_OPTIONS = ["*"]
32 |
--------------------------------------------------------------------------------
/tests/config_files/conf_logging.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from tests.test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app_common/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 | DISABLE_LOGS = False
30 | X_FRAME_OPTIONS = "DENY"
31 |
--------------------------------------------------------------------------------
/tests/config_files/conf_minimal_middleware_no_auth_handlers.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from ..test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app_common/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 | X_FRAME_OPTIONS = "DENY"
30 |
--------------------------------------------------------------------------------
/tests/config_files/conf_cors_default.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from ..test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 |
11 | ALLOWED_HOSTS = ["*"]
12 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13 | SECRET_KEY = "qwerty1234567"
14 | MIDDLEWARE = [
15 | "crax.middleware.x_frame.XFrameMiddleware",
16 | "crax.middleware.max_body.MaxBodySizeMiddleware",
17 | "crax.middleware.cors.CorsHeadersMiddleware",
18 | ]
19 |
20 | APPLICATIONS = ["test_app_common", "test_app_nested"]
21 | URL_PATTERNS = url_list
22 | STATIC_DIRS = ["static", "test_app/static"]
23 |
24 | DATABASES = {}
25 | ERROR_HANDLERS = {
26 | "500_handler": Handler500,
27 | "404_handler": Handler404,
28 | "405_handler": Handler405,
29 | }
30 |
31 | X_FRAME_OPTIONS = "DENY"
32 | CORS_OPTIONS = {}
33 |
--------------------------------------------------------------------------------
/tests/config_files/conf_db_missed.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | try:
6 | from test_app_auth.routers import Handler500
7 | from test_app_auth.urls_auth import url_list
8 | except ImportError:
9 | from ..test_app_auth.routers import Handler500
10 | from ..test_app_auth.urls_auth import url_list
11 |
12 | ALLOWED_HOSTS = ["*"]
13 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14 | SECRET_KEY = "qwerty1234567"
15 | MIDDLEWARE = [
16 | "crax.auth.middleware.AuthMiddleware",
17 | "crax.middleware.x_frame.XFrameMiddleware",
18 | "crax.middleware.max_body.MaxBodySizeMiddleware",
19 | "crax.auth.middleware.SessionMiddleware",
20 | "crax.middleware.cors.CorsHeadersMiddleware",
21 | ]
22 |
23 | APPLICATIONS = ["test_app_common", "test_app_auth"]
24 | URL_PATTERNS = url_list
25 | STATIC_DIRS = ["static", "test_app_common/static"]
26 |
27 | ERROR_HANDLERS = {
28 | "500_handler": Handler500,
29 | }
30 |
31 | X_FRAME_OPTIONS = "DENY"
32 | app = Crax(settings='tests.config_files.conf_db_missed')
33 |
--------------------------------------------------------------------------------
/tests/config_files/conf_nested_apps.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from tests.test_app_common.routers import Handler404, Handler500, Handler405
5 | from tests.test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from tests.test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested", "leagueA", "teams", "players"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app_common/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 | X_FRAME_OPTIONS = "DENY"
30 |
--------------------------------------------------------------------------------
/tests/config_files/conf_db_wrong_type.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | try:
6 | from test_app_auth.routers import Handler500
7 | from test_app_auth.urls_auth import url_list
8 | except ImportError:
9 | from ..test_app_auth.routers import Handler500
10 | from ..test_app_auth.urls_auth import url_list
11 |
12 | ALLOWED_HOSTS = ["*"]
13 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14 | SECRET_KEY = "qwerty1234567"
15 | MIDDLEWARE = [
16 | "crax.auth.middleware.AuthMiddleware",
17 | "crax.middleware.x_frame.XFrameMiddleware",
18 | "crax.middleware.max_body.MaxBodySizeMiddleware",
19 | "crax.auth.middleware.SessionMiddleware",
20 | "crax.middleware.cors.CorsHeadersMiddleware",
21 | ]
22 |
23 | APPLICATIONS = ["test_app_common", "test_app_auth"]
24 | URL_PATTERNS = url_list
25 | STATIC_DIRS = ["static", "test_app_common/static"]
26 |
27 | DATABASES = []
28 |
29 | ERROR_HANDLERS = {
30 | "500_handler": Handler500,
31 | }
32 |
33 | X_FRAME_OPTIONS = "DENY"
34 | app = Crax(settings='tests.config_files.conf_db_wrong_type')
35 |
--------------------------------------------------------------------------------
/tests/test_app_auth/urls_auth.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 |
3 | from .routers import (
4 | Home,
5 | ProtectedView,
6 | StaffRequired,
7 | SuperuserRequired,
8 | AuthView,
9 | LogoutView,
10 | WrongSessionView,
11 | AnonymousSessionView,
12 | CreateView,
13 | InsertView,
14 | WrongInsertView,
15 | WrongMethodInsertView,
16 | WrongTableMethodView,
17 | )
18 |
19 | url_list = [
20 | Route(Url("/"), Home),
21 | Route(Url("/protected"), ProtectedView),
22 | Route(Url("/staff_only"), StaffRequired),
23 | Route(Url("/superuser_only"), SuperuserRequired),
24 | Route(Url("/login"), AuthView),
25 | Route(Url("/logout"), LogoutView),
26 | Route(Url("/wrong_session"), WrongSessionView),
27 | Route(Url("/anonymous_session"), AnonymousSessionView),
28 | Route(Url("/create"), CreateView),
29 | Route(Url("/insert"), InsertView),
30 | Route(Url("/wrong_insert"), WrongInsertView),
31 | Route(Url("/wrong_method_insert"), WrongMethodInsertView),
32 | Route(Url("/wrong_table_method"), WrongTableMethodView),
33 | ]
34 |
--------------------------------------------------------------------------------
/tests/test_app_nested/leagueA/templates/leagueA_index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LeagueA
6 |
7 |
8 | LeagueA Index Page
9 | Home Page
10 | Nested First Level
11 | Nested Second Level
12 | Nested Second Level
13 |
14 | Nested Third Level
15 | Nested Third Level
16 | Namespace Third Level
17 |
18 |
--------------------------------------------------------------------------------
/install_gecko.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | for i in "$@"
4 | do
5 | case ${i} in
6 | -a=*|--arch=*)
7 | ARCH="${i#*=}"
8 | shift;;
9 | -p=*|--pass=*)
10 | SUDO_PASSWORD="${i#*=}"
11 | shift;;
12 | -v=*|--version=*)
13 | VERSION="${i#*=}"
14 | shift;;
15 | -d=*|--dir=*)
16 | DIR="${i#*=}"
17 | shift;;
18 | esac
19 | done
20 |
21 | if [[ "${ARCH}" == "" ]]
22 | then
23 | echo 'Please specify your architecture'
24 | exit 1
25 | fi
26 | if [[ "${VERSION}" == "" ]]
27 | then
28 | VERSION='0.27.0'
29 | fi
30 | if [[ "${DIR}" == "" ]]
31 | then
32 | DIR='/usr/bin'
33 | fi
34 | wget https://github.com/mozilla/geckodriver/releases/download/v${VERSION}/geckodriver-v${VERSION}-linux${ARCH}.tar.gz
35 | tar -xvzf *linux${ARCH}.tar.gz
36 | chmod +x ./geckodriver
37 | if [[ "${SUDO_PASSWORD}" == "" ]]
38 | then
39 | sudo cp geckodriver ${DIR}
40 | else
41 | sudo -S <<< ${SUDO_PASSWORD} cp geckodriver ${DIR}
42 | fi
43 | rm -f ./gecko*
44 |
--------------------------------------------------------------------------------
/tests/config_files/conf_logging_custom.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from tests.test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app_common/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 | DISABLE_LOGS = False
30 | LOG_FILE = "app.log"
31 | LOGGER_NAME = "test_logger"
32 | LOG_LEVEL = "DEBUG"
33 | LOG_CONSOLE = True
34 | X_FRAME_OPTIONS = "DENY"
35 |
--------------------------------------------------------------------------------
/tests/config_files/conf_cors_custom.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from tests.test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 |
30 | X_FRAME_OPTIONS = "DENY"
31 | CORS_OPTIONS = {
32 | "origins": ["http://127.0.0.1:8000", "http://127.0.0.1:3000"],
33 | "methods": ["POST", "PATCH"],
34 | "headers": ["content-type"],
35 | }
36 |
--------------------------------------------------------------------------------
/run_selenium.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | pip install .[sqlite]
3 | pip install uvicorn selenium
4 | cd tests
5 | rm -f python_logo.png
6 | cp test_crax.sqlite test_selenium/test.sqlite
7 |
8 | for i in "$@"
9 | do
10 | case ${i} in
11 | -e=*|--executable=*)
12 | EXECUTABLE="${i#*=}"
13 | shift;;
14 | -t=*|--threads=*)
15 | THREADS="${i#*=}"
16 | shift;;
17 | -d=*|--delay=*)
18 | DELAY="${i#*=}"
19 | shift;;
20 | esac
21 | done
22 |
23 | if [[ "${EXECUTABLE}" != "" ]]
24 | then
25 | export CRAX_GECKO_EXECUTABLE=${EXECUTABLE}
26 | fi
27 | if [[ "${THREADS}" != "" ]]
28 | then
29 | export CRAX_TEST_THREADS=${THREADS}
30 | fi
31 | if [[ "${DELAY}" != "" ]]
32 | then
33 | export CRAX_TEST_DELAY=${DELAY}
34 | fi
35 | python -m uvicorn run:app &
36 | cd test_selenium && python thread_tests.py
37 | pip uninstall --yes sqlalchemy databases alembic crax selenium
38 | rm -f test.sqlite
39 | rm -f geckodriver.log
40 | rm -f ../python_logo.png
41 | rm -f ../crax.log
42 | ps aux | grep -v grep | grep uvicorn | awk '{print $2}' | xargs kill -9
43 |
--------------------------------------------------------------------------------
/crax/conf.py:
--------------------------------------------------------------------------------
1 | """
2 | Configuration example file. Also used if no real configuration is given.
3 | """
4 | import os
5 |
6 | # ######################## BASE ######################################
7 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8 | STATIC_DIRS = ["static"]
9 | URL_PATTERNS = []
10 | APPLICATIONS = ["crax"]
11 | MIDDLEWARE = []
12 | ALLOWED_HOSTS = ["*"]
13 | SECRET_KEY = ""
14 | DATABASES = {}
15 | ERROR_HANDLERS = {}
16 |
17 |
18 | # ######################## AUTH ######################################
19 | SESSION_COOKIE_NAME = "session_id"
20 | SESSION_EXPIRES = 14 * 24 * 60 * 60
21 |
22 |
23 | # ######################## MIDDLEWARE ######################################
24 | CORS_OPTIONS = {"origins": ["http://localhost:8080"], "cors_cookie": "zzzzzz"}
25 |
26 | X_FRAME_OPTIONS = "DENY"
27 |
28 |
29 | # ######################## LOGGING ######################################
30 | DISABLE_LOGS = False
31 | LOGGING_BACKEND = "crax.logger.CraxLogger"
32 | LOGGER_NAME = "crax"
33 | LOG_FILE = ""
34 | LOG_LEVEL = "INFO"
35 | LOG_ROTATE_TIME = "midnight"
36 | LOG_CONSOLE = False
37 | LOG_STEAMS = []
38 | ENABLE_SENTRY = True
39 | SENTRY_LOG_LEVEL = "INFO"
40 | SENTRY_EVENT_LEVEL = "ERROR"
41 | LOG_SENTRY_DSN = ""
42 |
--------------------------------------------------------------------------------
/tests/config_files/conf_cors_custom_str.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from ..test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 |
11 | ALLOWED_HOSTS = ["*"]
12 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13 | SECRET_KEY = "qwerty1234567"
14 | MIDDLEWARE = [
15 | "crax.middleware.x_frame.XFrameMiddleware",
16 | "crax.middleware.max_body.MaxBodySizeMiddleware",
17 | "crax.middleware.cors.CorsHeadersMiddleware",
18 | ]
19 |
20 | APPLICATIONS = ["test_app_common", "test_app_nested"]
21 | URL_PATTERNS = url_list
22 | STATIC_DIRS = ["static", "test_app/static"]
23 |
24 | DATABASES = {}
25 | ERROR_HANDLERS = {
26 | "500_handler": Handler500,
27 | "404_handler": Handler404,
28 | "405_handler": Handler405,
29 | }
30 |
31 | X_FRAME_OPTIONS = "DENY"
32 | CORS_OPTIONS = {
33 | "origins": "http://127.0.0.1:8000",
34 | "methods": "POST, PATCH",
35 | "headers": "*",
36 | "cors_cookie": "Allow-By-Cookie",
37 | "expose_headers": "Exposed_One, Exposed_Two",
38 | }
39 |
--------------------------------------------------------------------------------
/tests/test_files/utils.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from crax import Crax
3 | from uvicorn import Server, Config as UvConfig
4 |
5 | from contextlib import asynccontextmanager
6 |
7 |
8 | class SimpleResponseTest:
9 | def __init__(self, test_func, *args, settings=None, debug=False, on_startup=None):
10 | self.func = test_func
11 | self.on_startup = on_startup
12 | if settings is None:
13 | self.app = Crax()
14 | else:
15 | self.app = Crax(settings=settings, debug=debug, on_startup=on_startup)
16 | self.args = args
17 |
18 | @staticmethod
19 | @asynccontextmanager
20 | async def create_server(config):
21 | server = Server(config)
22 | task = asyncio.ensure_future(server.serve())
23 | try:
24 | yield task
25 | finally:
26 | await server.shutdown()
27 | task.cancel()
28 |
29 | async def _run(self):
30 | config = UvConfig(self.app)
31 | async with self.create_server(config):
32 | loop = asyncio.get_event_loop()
33 | result = await loop.run_in_executor(None, self.func, *self.args)
34 | return result
35 |
36 | def __await__(self):
37 | return self._run().__await__()
38 |
--------------------------------------------------------------------------------
/tests/test_app_auth/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}Crax Tests{% endblock %}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
26 |
27 |
28 | {% block content %}{% endblock %}
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/tests/config_files/conf_cors_custom_cookie.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from test_app_common.urls_two_apps import url_list
6 | except ImportError:
7 | from ..test_app_common.urls_two_apps import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 |
11 | ALLOWED_HOSTS = ["*"]
12 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13 | SECRET_KEY = "qwerty1234567"
14 | MIDDLEWARE = [
15 | "crax.middleware.x_frame.XFrameMiddleware",
16 | "crax.middleware.max_body.MaxBodySizeMiddleware",
17 | "crax.middleware.cors.CorsHeadersMiddleware",
18 | ]
19 |
20 | APPLICATIONS = ["test_app_common", "test_app_nested"]
21 | URL_PATTERNS = url_list
22 | STATIC_DIRS = ["static", "test_app/static"]
23 |
24 | DATABASES = {}
25 | ERROR_HANDLERS = {
26 | "500_handler": Handler500,
27 | "404_handler": Handler404,
28 | "405_handler": Handler405,
29 | }
30 |
31 | X_FRAME_OPTIONS = "DENY"
32 | CORS_OPTIONS = {
33 | "origins": ["http://127.0.0.1:8000", "http://127.0.0.1:3000"],
34 | "methods": ["POST", "PATCH"],
35 | "headers": ["content-type"],
36 | "cors_cookie": "Allow-By-Cookie",
37 | "expose_headers": ["Exposed_One", "Exposed_Two"],
38 | }
39 |
--------------------------------------------------------------------------------
/tests/config_files/conf_wrong_url_inclusion.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | try:
4 | from test_app_common.routers import Handler404, Handler500, Handler405
5 | from ..urls_wrong_patterns import url_list
6 | except ImportError:
7 | from ..test_app_common.urls_wrong_patterns import url_list
8 | from ..test_app_common.routers import Handler404, Handler500, Handler405
9 |
10 | ALLOWED_HOSTS = ["*"]
11 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
12 | SECRET_KEY = "qwerty1234567"
13 | MIDDLEWARE = [
14 | "crax.middleware.x_frame.XFrameMiddleware",
15 | "crax.middleware.max_body.MaxBodySizeMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_app_common", "test_app_nested"]
20 | URL_PATTERNS = url_list
21 | STATIC_DIRS = ["static", "test_app_common/static"]
22 |
23 | DATABASES = {}
24 | ERROR_HANDLERS = {
25 | "500_handler": Handler500,
26 | "404_handler": Handler404,
27 | "405_handler": Handler405,
28 | }
29 |
30 | X_FRAME_OPTIONS = "DENY"
31 | CORS_OPTIONS = {
32 | "origins": ["http://127.0.0.1:8000", "http://127.0.0.1:3000"],
33 | "methods": ["POST", "PATCH"],
34 | "headers": ["content-type"],
35 | "cors_cookie": "Allow-By-Cookie",
36 | "expose_headers": ["Exposed_One", "Exposed_Two"],
37 | }
38 |
--------------------------------------------------------------------------------
/tests/test_app_common/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title %}Crax Tests{% endblock %}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
26 |
27 |
28 | {% block content %}{% endblock %}
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/tests/config_files/conf_auth_no_default_db.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | try:
6 | from test_app_auth.routers import Handler500
7 | from test_app_auth.urls_auth import url_list
8 | except ImportError:
9 | from ..test_app_auth.routers import Handler500
10 | from ..test_app_auth.urls_auth import url_list
11 |
12 | ALLOWED_HOSTS = ["*"]
13 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14 | SECRET_KEY = "qwerty1234567"
15 | MIDDLEWARE = [
16 | "crax.auth.middleware.AuthMiddleware",
17 | "crax.middleware.x_frame.XFrameMiddleware",
18 | "crax.middleware.max_body.MaxBodySizeMiddleware",
19 | "crax.auth.middleware.SessionMiddleware",
20 | "crax.middleware.cors.CorsHeadersMiddleware",
21 | ]
22 |
23 | APPLICATIONS = ["test_app_common", "test_app_auth"]
24 | URL_PATTERNS = url_list
25 | STATIC_DIRS = ["static", "test_app/static"]
26 |
27 | DATABASES = {
28 | "users": {
29 | "driver": "postgresql",
30 | "host": "127.0.0.1",
31 | "user": "crax",
32 | "password": "CraxPassword",
33 | "name": "testing_users",
34 | }
35 | }
36 |
37 |
38 | ERROR_HANDLERS = {
39 | "500_handler": Handler500,
40 | }
41 |
42 | X_FRAME_OPTIONS = "DENY"
43 | app = Crax(settings='tests.config_files.conf_auth_no_default_db')
44 |
--------------------------------------------------------------------------------
/crax/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title %}CRAX {{ title }}{% endblock %}
7 |
10 |
11 |
12 |
15 |
16 | {% block content %}{% endblock %}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/tests/test_selenium/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url
2 | from .routers import (
3 | Home,
4 | Customers,
5 | CustomersView,
6 | CustomerDiscountView,
7 | CustomerDiscounts,
8 | OrderView,
9 | ProtectedView,
10 | APIView,
11 | Order,
12 | )
13 | from .cart.urls import url_list as cart_urls
14 |
15 | url_list = [
16 | Route(Url("/home"), Home),
17 | Route(Url("/protected"), ProtectedView),
18 | Route(urls=(Url("/api/customers", tag="customer")), handler=Customers),
19 | Route(Url("/api/customer/"), handler=CustomersView),
20 | Route(urls=(Url("/api/discounts", tag="discount")), handler=CustomerDiscounts),
21 | Route(
22 | Url(
23 | r"/api/discount/(?P\d+)/(?:(?P\w+))?", type="re_path"
24 | ),
25 | handler=CustomerDiscountView,
26 | ),
27 | Route(Url("/api/orders", tag="order", methods=["GET", "POST"]), handler=Order),
28 | Route(Url("/api/order/"), handler=OrderView),
29 | Route(
30 | urls=(
31 | Url("/"),
32 | Url("/v1/customers"),
33 | Url("/v1/discounts"),
34 | Url("/v1/cart"),
35 | Url("/v1/customer/"),
36 | Url("/v1/discount///"),
37 | ),
38 | handler=APIView,
39 | ),
40 | ] + cart_urls
41 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | selenium
2 | aiofiles==0.5.0
3 | aiomysql==0.0.20
4 | aio-pika==6.6.1
5 | aiosqlite==0.13.0
6 | alembic==1.4.2
7 | apipkg==1.5
8 | appdirs==1.4.4
9 | asyncpg==0.20.1
10 | attrs==19.3.0
11 | black==19.10b0
12 | certifi==2020.6.20
13 | cfgv==3.1.0
14 | chardet==3.0.4
15 | click==7.1.2
16 | coverage==5.1
17 | databases==0.3.2
18 | distlib==0.3.0
19 | execnet==1.7.1
20 | filelock==3.0.12
21 | flake8==3.8.3
22 | h11==0.9.0
23 | httptools==0.1.1
24 | identify==1.4.15
25 | idna==2.9
26 | importlib-metadata==1.6.0
27 | in-place==0.4.0
28 | itsdangerous==1.1.0
29 | Jinja2==2.11.2
30 | lxml==4.5.1
31 | Mako==1.1.2
32 | MarkupSafe==1.1.1
33 | mccabe==0.6.1
34 | more-itertools==8.4.0
35 | nodeenv==1.3.5
36 | packaging==20.4
37 | pathspec==0.8.0
38 | pluggy==0.13.1
39 | pre-commit==2.3.0
40 | psycopg2-binary==2.8.5
41 | py==1.9.0
42 | pycodestyle==2.6.0
43 | pyflakes==2.2.0
44 | PyMySQL==0.9.2
45 | pyparsing==2.4.7
46 | pytest==5.4.3
47 | pytest-asyncio==0.14.0
48 | pytest-cov==2.10.0
49 | pytest-forked==1.1.3
50 | pytest-xdist==1.32.0
51 | python-dateutil==2.8.1
52 | python-editor==1.0.4
53 | python-multipart==0.0.5
54 | PyYAML==5.3.1
55 | regex==2020.5.7
56 | requests==2.24.0
57 | six==1.14.0
58 | SQLAlchemy==1.3.16
59 | SQLAlchemy-Utils==0.36.6
60 | toml==0.10.0
61 | typed-ast==1.4.1
62 | urllib3==1.25.9
63 | uvicorn==0.11.5
64 | uvloop==0.14.0
65 | virtualenv==20.0.20
66 | wcwidth==0.2.5
67 | websockets==8.1
68 | zipp==3.1.0
69 |
--------------------------------------------------------------------------------
/launch_tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | pip install .
3 | cd tests
4 | LOG_FILE='test.log'
5 |
6 | function pyTestCrax() {
7 | pytest --cov=../crax --cov-config=.coveragerc test_files/command_one.py
8 | pytest --cov=../crax --cov-config=.coveragerc test_files/command_two.py
9 | python -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_three.py
10 | python -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_four.py
11 | python -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/auth_tests.py
12 | }
13 |
14 | function runTests() {
15 | rm -f ${LOG_FILE}
16 | touch ${LOG_FILE}
17 | pyTestCrax | tee ${LOG_FILE}
18 | ret=$(cat ${LOG_FILE} | grep 'FAILED')
19 | if [ "$ret" = "" ];
20 | then
21 | rm -f ${LOG_FILE}
22 | echo 'OK'
23 | else echo ${ret} && exit 1;
24 | fi
25 | }
26 | pip install sqlalchemy databases alembic asyncpg psycopg2-binary aiomysql pymysql==0.9.2
27 | echo 'SQLite tests started'
28 | export CRAX_TEST_MODE='sqlite'
29 | runTests
30 |
31 | export CRAX_TEST_MODE='mysql'
32 | echo 'MySQL tests started'
33 | runTests
34 |
35 | export CRAX_TEST_MODE='postgresql'
36 | echo 'PostgreSQL tests started'
37 | runTests
38 |
39 | pip uninstall --yes sqlalchemy databases alembic
40 | python -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/common_tests.py
41 |
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.5'
2 |
3 | services:
4 | rabbitmq-container:
5 | image: rabbitmq:3.8.3-management
6 | hostname: rabbitmq-container
7 | container_name: rmq_crax
8 | ports:
9 | - 5673:5673
10 | - 15673:15673
11 | networks:
12 | - crax_net
13 |
14 | postgres-container:
15 | image: ephmann/crax_postgres
16 | hostname: postgres-container
17 | container_name: pg_crax
18 | build:
19 | context: .
20 | dockerfile: app/PostgresDockerfile
21 | ports:
22 | - 5433:5433
23 | networks:
24 | - crax_net
25 |
26 | mysql-container:
27 | image: ephmann/crax_mysql
28 | hostname: mysql-container
29 | container_name: mysql_crax
30 | build:
31 | context: .
32 | dockerfile: app/MySQLDockerfile
33 | command: --default-authentication-plugin=mysql_native_password
34 | ports:
35 | - 3307:3307
36 | networks:
37 | - crax_net
38 |
39 | crax-container:
40 | image: ephmann/crax_tests
41 | container_name: crax
42 | build:
43 | context: .
44 | dockerfile: app/Dockerfile
45 | env_file: database.conf
46 | ports:
47 | - 5000:80
48 | depends_on:
49 | - rabbitmq-container
50 | - postgres-container
51 | - mysql-container
52 | volumes:
53 | - /var/run/docker.sock:/var/run/docker.sock
54 | networks:
55 | - crax_net
56 |
57 | networks:
58 | crax_net:
59 | external:
60 | name: crax_net
--------------------------------------------------------------------------------
/tests/test_selenium/models.py:
--------------------------------------------------------------------------------
1 | import sqlalchemy as sa
2 | from crax.auth.models import User
3 | from crax.database.model import BaseTable
4 |
5 |
6 | class BaseDiscount(BaseTable):
7 | name = sa.Column(sa.String(length=100), nullable=False)
8 | percent = sa.Column(sa.Integer, nullable=False)
9 |
10 | class Meta:
11 | abstract = True
12 |
13 |
14 | class CustomerDiscount(BaseDiscount):
15 | pass
16 |
17 |
18 | class VendorDiscount(BaseDiscount):
19 | pass
20 |
21 |
22 | class UserInfo(BaseTable):
23 | age = sa.Column(sa.Integer(), nullable=True)
24 |
25 |
26 | class BaseUser(User):
27 | bio = sa.Column(sa.String(length=100), nullable=False)
28 |
29 | class Meta:
30 | abstract = True
31 |
32 |
33 | class CustomerA(BaseUser, UserInfo):
34 | ave_bill = sa.Column(sa.Integer)
35 |
36 |
37 | class CustomerB(BaseUser, UserInfo):
38 | discount = sa.Column(sa.Integer)
39 | customer_discount_id = sa.Column(sa.Integer, sa.ForeignKey(CustomerDiscount.id))
40 |
41 | class Meta:
42 | order_by = "username"
43 |
44 |
45 | class Vendor(BaseUser, UserInfo):
46 | vendor_discount_id = sa.Column(sa.Integer, sa.ForeignKey(VendorDiscount.id))
47 |
48 |
49 | class Orders(BaseTable):
50 | staff = sa.Column(sa.String(length=100), nullable=False)
51 | price = sa.Column(sa.Integer, nullable=False)
52 | customer_id = sa.Column(sa.Integer, sa.ForeignKey(CustomerB.id), nullable=True)
53 | vendor_id = sa.Column(sa.Integer, sa.ForeignKey(Vendor.id), nullable=True)
54 |
--------------------------------------------------------------------------------
/tests/test_selenium/static/css/app.641d82c2.css:
--------------------------------------------------------------------------------
1 | body{font-family:sans-serif;font-size:100%;background-color:#f2f2f2;color:#000;margin:0;padding:0}#navbar{overflow:hidden;background-color:#092e3e}#navbar a{float:left;display:block;color:#fc4915;text-align:center;padding:14px;text-decoration:none}.content{margin-right:15px;margin-left:15px}.error_container_title{text-align:center;color:#092e3e}.error_container_title h2{margin-top:-15px}.error_container{text-align:left;color:#092e3e}.crax_mark{color:#fc4915}.trace{list-style:none}.trace_line{color:#092e3e;font-weight:700;padding:20px;margin:5px;background:#dfe5e5}.default_crax{text-align:center;margin-top:10%;border-radius:5px}.title{text-align:center;color:#fc4915}.customer_details{width:60%;border:1px solid #000;background:#092e3e;color:#fff;border-radius:5px;padding:10px;margin-bottom:20px}.customer_button{border:none;color:#fff;position:relative;display:inline-block;padding:.8em;padding-top:.8em;padding-bottom:.8em;border-radius:3px;background-color:#fc4915;font-size:13px;text-align:center;text-decoration:none;cursor:pointer}.form_input{padding:10px;margin:5px;width:100%}.form_block{max-width:30%;margin-left:34%}.gap{line-height:1px;color:#fff;margin-top:20px;margin-bottom:20px}.default_crax h1{font-size:4em;background-color:#565656;color:transparent;text-shadow:0 2px 3px hsla(0,0%,100%,.5);-webkit-background-clip:text;-moz-background-clip:text;background-clip:text}.default_crax h4{color:#fc4915;font-size:2em;margin-top:-35px}@media (max-width:375px){.default_crax h1{font-size:3em}}@media (max-width:320px){.default_crax h1{font-size:3em}}
--------------------------------------------------------------------------------
/crax/commands/command.py:
--------------------------------------------------------------------------------
1 | """
2 | Base command class. In case if user wants to use crax commands, "from_shell" function
3 | should be placed to main application file.
4 | """
5 | import argparse
6 | import os
7 | import re
8 | import sys
9 | import shlex
10 | import subprocess
11 |
12 | import typing
13 |
14 | sys.path = ["", ".."] + sys.path[1:]
15 | COMMANDS_URL = f"{os.path.dirname(os.path.dirname(os.path.abspath(__file__)))}/commands"
16 |
17 |
18 | def from_shell(
19 | args: typing.Union[tuple, list], settings: str = None
20 | ) -> None: # pragma: no cover
21 | command = args[1]
22 | commands = [x for x in os.listdir(COMMANDS_URL) if x.split(".")[0] == command]
23 | if commands:
24 | if settings:
25 | os.environ["CRAX_SETTINGS"] = settings
26 | else:
27 | os.environ["CRAX_SETTINGS"] = "crax.conf"
28 | str_args = " ".join(args[1:])
29 | keys = re.findall(r"--?\w+ ?.* ?", str_args)
30 | command_str = f'python {COMMANDS_URL}/{commands[0]} {" ".join(keys)}'
31 | subprocess.call(shlex.split(command_str))
32 |
33 | else:
34 | raise RuntimeError(f"Unknown command {command} \n")
35 |
36 |
37 | class BaseCommand:
38 | def __init__(
39 | self, opts: typing.List[typing.Union[tuple]], **kwargs: typing.Any
40 | ) -> None:
41 | self.opts = opts
42 | self.kwargs = kwargs
43 | self.args = self.collect_args()
44 |
45 | def collect_args(self):
46 | parser = argparse.ArgumentParser()
47 | if self.opts:
48 | for option in self.opts:
49 | parser.add_argument(*option[0], **option[1])
50 | args = parser.parse_args()
51 | return args
52 |
--------------------------------------------------------------------------------
/crax/templates/swagger.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Swagger UI
7 |
8 |
9 |
10 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/tests/test_app_nested/urls.py:
--------------------------------------------------------------------------------
1 | from crax.urls import Route, Url, include
2 |
3 | from .routers import (
4 | TestGetParams,
5 | TestGetParamsRegex,
6 | TestMissedTemplate,
7 | TestNotFoundTemplate,
8 | TestInnerTemplateExceptions,
9 | TestJSONView,
10 | TestSendCookiesBack,
11 | TestSetCookies,
12 | TestEmptyMethods,
13 | TestMasquerade,
14 | TestMasqueradeNoScope,
15 | TestUrlCreation,
16 | )
17 |
18 | url_list = [
19 | Route(urls=(Url("/test_json_view")), handler=TestJSONView),
20 | Route(urls=(Url("/test_send_cookies_back")), handler=TestSendCookiesBack),
21 | Route(urls=(Url("/test_set_cookies")), handler=TestSetCookies),
22 | Route(urls=(Url("/test_masquerade/", masquerade=True)), handler=TestMasquerade),
23 | Route(
24 | urls=(Url("/masquerade_no_scope/", masquerade=True)),
25 | handler=TestMasqueradeNoScope,
26 | ),
27 | Route(
28 | urls=(
29 | Url("/test_param///", name="test_param"),
30 | Url("/test_param"),
31 | ),
32 | handler=TestGetParams,
33 | ),
34 | Route(urls=(Url("/test_create_url")), handler=TestUrlCreation),
35 | Route(urls=(Url("/test_missed_template")), handler=TestMissedTemplate),
36 | Route(urls=(Url("/test_not_found_template")), handler=TestNotFoundTemplate),
37 | Route(urls=(Url("/inner_template_ex")), handler=TestInnerTemplateExceptions),
38 | Route(urls=(Url("/test_empty_method")), handler=TestEmptyMethods),
39 | Route(
40 | urls=(
41 | Url(
42 | r"/test_param_regex/(?P\w{0,30})/(?P\w{0,30})/",
43 | type="re_path",
44 | )
45 | ),
46 | handler=TestGetParamsRegex,
47 | ),
48 | ]
49 |
--------------------------------------------------------------------------------
/tests/config.yaml:
--------------------------------------------------------------------------------
1 | "COMMAND_OPTIONS:\n- !!python/tuple\n - - --database\n - -d\n - help: specify\
2 | \ database to run migrations\n type: &id001 !!python/name:builtins.str ''\n-\
3 | \ !!python/tuple\n - - --apps\n - -a\n - help: generate migrations for app\n\
4 | \ nargs: '*'\n type: *id001\n- !!python/tuple\n - - --message\n - -m\n\
5 | \ - help: specify revision message\n type: *id001\n- !!python/tuple\n - - --cov\n\
6 | \ - {}\n- !!python/tuple\n - - --cov-config\n - {}\n- !!python/tuple\n - - --sql\n\
7 | \ - -s\n - action: store_true\n dest: sql\n help: generate sql script\n\
8 | - !!python/tuple\n - - --down\n - -n\n - action: store_true\n dest: down\n\
9 | \ help: downgrade migrations\n- !!python/tuple\n - - --revision\n - -r\n\
10 | \ - help: specify revision you want to migrate\n type: *id001\n- !!python/tuple\n\
11 | \ - - --latest\n - -l\n - action: store_true\n dest: latest\n help: get\
12 | \ latest revisions\n- !!python/tuple\n - - --cov-append\n - action: store_true\n\
13 | - !!python/tuple\n - - test_file_1\n - {}\n- !!python/tuple\n - - test_file_2\n\
14 | \ - default: null\n nargs: '?'\nENGINES:\n mysql:\n - mysql+pymysql://crax:CraxPassword@127.0.0.1/test_crax\n\
15 | \ - mysql+pymysql://root:@127.0.0.1/test_crax\n postgresql:\n - postgresql://crax:CraxPassword@127.0.0.1/test_crax\n\
16 | \ - postgresql://postgres:@127.0.0.1/test_crax\nTEST_USERS:\n- first_name: James\n\
17 | \ id: 1\n password: qwerty\n username: jamie\n- first_name: Robert\n id: 2\n\
18 | \ password: qwerty\n username: rob\n- first_name: Tomas\n id: 3\n password:\
19 | \ qwerty\n username: tom\nmysql-container: mysql+pymysql://crax:CraxPassword@mysql-container/test_crax\n\
20 | postgres-container: postgresql://crax:CraxPassword@postgres-container/test_crax\n"
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from setuptools import setup
4 |
5 |
6 | def get_full_description():
7 | with open("README.md", encoding="utf8") as f:
8 | return f.read()
9 |
10 |
11 | def get_packages(package):
12 | return [
13 | root
14 | for root, _, _ in os.walk(package)
15 | if os.path.exists(os.path.join(root, "__init__.py"))
16 | ]
17 |
18 |
19 | setup(
20 | name="crax",
21 | version="0.1.5",
22 | python_requires=">=3.7",
23 | url="https://github.com/ephmann/crax",
24 | license="BSD",
25 | description="Python Asynchronous Web Development Switz Knife.",
26 | long_description=get_full_description(),
27 | long_description_content_type="text/markdown",
28 | author="Eugene Mercousheu",
29 | author_email="crax.info@gmail.com",
30 | packages=get_packages("crax"),
31 | install_requires=["aiofiles", "jinja2", "python-multipart", "itsdangerous"],
32 | extras_require={
33 | "postgresql": [
34 | "sqlalchemy",
35 | "databases",
36 | "alembic",
37 | "asyncpg",
38 | "psycopg2-binary",
39 | ],
40 | "mysql": ["sqlalchemy", "databases", "alembic", "aiomysql", "pymysql==0.9.2"],
41 | "sqlite": ["sqlalchemy", "databases", "alembic", "aiosqlite"],
42 | },
43 | include_package_data=True,
44 | classifiers=[
45 | "Development Status :: 3 - Alpha",
46 | "Environment :: Web Environment",
47 | "Intended Audience :: Developers",
48 | "License :: OSI Approved :: BSD License",
49 | "Operating System :: OS Independent",
50 | "Topic :: Internet :: WWW/HTTP",
51 | "Programming Language :: Python :: 3",
52 | "Programming Language :: Python :: 3 :: Only",
53 | "Programming Language :: Python :: 3.7",
54 | "Programming Language :: Python :: 3.8",
55 | ],
56 | zip_safe=False,
57 | )
58 |
--------------------------------------------------------------------------------
/tests/app_one/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | ALLOWED_HOSTS = ["*"]
6 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 | SECRET_KEY = "qwerty1234567"
8 | MIDDLEWARE = [
9 | "crax.auth.middleware.AuthMiddleware",
10 | "crax.middleware.x_frame.XFrameMiddleware",
11 | "crax.middleware.max_body.MaxBodySizeMiddleware",
12 | "crax.auth.middleware.SessionMiddleware",
13 | "crax.middleware.cors.CorsHeadersMiddleware",
14 | ]
15 |
16 | APPLICATIONS = ["app_one", "app_two", "app_three"]
17 | URL_PATTERNS = []
18 | STATIC_DIRS = ["static", "test_app/static"]
19 | TEST_MODE = os.environ["CRAX_TEST_MODE"]
20 | DB_USERS = {"postgresql": "postgres", "mysql": "root", "sqlite": "root"}
21 | DB_NAMES = {
22 | "postgresql": "test_crax",
23 | "mysql": "test_crax",
24 | "sqlite": f"/{BASE_URL}/test_crax.sqlite",
25 | }
26 | if TEST_MODE != "sqlite":
27 | OPTIONS = {"min_size": 5, "max_size": 20}
28 | else:
29 | OPTIONS = {}
30 |
31 |
32 | def get_db_host():
33 | docker_db_host = os.environ.get("DOCKER_DATABASE_HOST", None)
34 | if docker_db_host:
35 | host = docker_db_host
36 | else:
37 | host = "127.0.0.1"
38 | return host
39 |
40 |
41 | if "TRAVIS" not in os.environ:
42 | DATABASES = {
43 | "default": {
44 | "driver": TEST_MODE,
45 | "host": get_db_host(),
46 | "user": "crax",
47 | "password": "CraxPassword",
48 | "name": DB_NAMES[TEST_MODE],
49 | "options": OPTIONS,
50 | },
51 | }
52 |
53 | else:
54 | DATABASES = {
55 | "default": {
56 | "driver": TEST_MODE,
57 | "host": "127.0.0.1",
58 | "user": DB_USERS[TEST_MODE],
59 | "password": "",
60 | "name": DB_NAMES[TEST_MODE],
61 | "options": OPTIONS,
62 | },
63 | }
64 |
65 |
66 | X_FRAME_OPTIONS = "DENY"
67 | app = Crax(settings='app_one.conf')
68 |
--------------------------------------------------------------------------------
/tests/test_selenium/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 | from crax.swagger.types import SwaggerInfo
5 | from .urls import url_list
6 | from crax.swagger import urls
7 |
8 | ALLOWED_HOSTS = ["*"]
9 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
10 | SECRET_KEY = "qwerty1234567"
11 | MIDDLEWARE = [
12 | "crax.auth.middleware.AuthMiddleware",
13 | "crax.middleware.x_frame.XFrameMiddleware",
14 | "crax.middleware.max_body.MaxBodySizeMiddleware",
15 | "crax.auth.middleware.SessionMiddleware",
16 | "crax.middleware.cors.CorsHeadersMiddleware",
17 | ]
18 |
19 | APPLICATIONS = ["test_selenium"]
20 | URL_PATTERNS = url_list + urls
21 | STATIC_DIRS = ["static", "test_selenium/static"]
22 |
23 | DATABASES = {
24 | "default": {"driver": "sqlite", "name": f"/{os.path.dirname(os.path.abspath(__file__))}/test.sqlite"},
25 | }
26 |
27 | X_FRAME_OPTIONS = "DENY"
28 | ENABLE_CSRF = True
29 |
30 | SWAGGER = SwaggerInfo(
31 | description="This is a simple example of OpenAPI (Swagger) documentation. "
32 | " You can find out more about Swagger at "
33 | "[http://swagger.io](http://swagger.io) or on "
34 | "[irc.freenode.net, #swagger](http://swagger.io/irc/). ",
35 | version="0.0.3",
36 | title="Crax Swagger Example",
37 | termsOfService="https://github.com/ephmann/crax",
38 | contact={"email": "ephmanns@gmail.com"},
39 | license={"name": "MIT", "url": "https://opensource.org/licenses/MIT"},
40 | servers=[
41 | {"url": "http://127.0.0.1:8000", "description": "Development server http"},
42 | {"url": "https://127.0.0.1:8000", "description": "Staging server"},
43 | ],
44 | basePath="/api",
45 | )
46 |
47 |
48 | def square_(a):
49 | return a * a
50 |
51 |
52 | def hello():
53 | return "Hello world"
54 |
55 |
56 | TEMPLATE_FUNCTIONS = [square_, hello]
57 |
58 | CORS_OPTIONS = {
59 | "origins": ["*"],
60 | "methods": ["*"],
61 | "headers": ["content-type"],
62 | "cors_cookie": "Allow-By-Cookie",
63 | }
64 | DISABLE_LOGS = False
65 | app = Crax(settings="test_selenium.app", debug=True)
66 |
--------------------------------------------------------------------------------
/tests/config_files/conf_rest.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | try:
6 | from test_selenium.urls import url_list
7 | except ImportError:
8 | from ..test_selenium.urls import url_list
9 |
10 |
11 | ALLOWED_HOSTS = ["*"]
12 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13 | SECRET_KEY = "qwerty1234567"
14 | MIDDLEWARE = [
15 | "crax.auth.middleware.AuthMiddleware",
16 | "crax.middleware.x_frame.XFrameMiddleware",
17 | "crax.middleware.max_body.MaxBodySizeMiddleware",
18 | "crax.auth.middleware.SessionMiddleware",
19 | "crax.middleware.cors.CorsHeadersMiddleware",
20 | ]
21 |
22 | APPLICATIONS = ["test_selenium"]
23 | URL_PATTERNS = url_list
24 | STATIC_DIRS = ["static", "test_selenium/static"]
25 |
26 |
27 | TEST_MODE = os.environ["CRAX_TEST_MODE"]
28 | DB_USERS = {"postgresql": "postgres", "mysql": "root", "sqlite": "root"}
29 | DB_NAMES = {
30 | "postgresql": "test_crax",
31 | "mysql": "test_crax",
32 | "sqlite": f"/{BASE_URL}/test_crax.sqlite",
33 | }
34 | if TEST_MODE != "sqlite":
35 | OPTIONS = {"min_size": 5, "max_size": 20}
36 | else:
37 | OPTIONS = {}
38 |
39 |
40 | def get_db_host():
41 | docker_db_host = os.environ.get("DOCKER_DATABASE_HOST", None)
42 | if docker_db_host:
43 | host = docker_db_host
44 | else:
45 | host = "127.0.0.1"
46 | return host
47 |
48 |
49 | if "TRAVIS" not in os.environ:
50 | DATABASES = {
51 | "default": {
52 | "driver": TEST_MODE,
53 | "host": get_db_host(),
54 | "user": "crax",
55 | "password": "CraxPassword",
56 | "name": DB_NAMES[TEST_MODE],
57 | "options": OPTIONS,
58 | },
59 | }
60 |
61 | else:
62 | DATABASES = {
63 | "default": {
64 | "driver": TEST_MODE,
65 | "host": "127.0.0.1",
66 | "user": DB_USERS[TEST_MODE],
67 | "password": "",
68 | "name": DB_NAMES[TEST_MODE],
69 | "options": OPTIONS,
70 | },
71 | }
72 |
73 |
74 | X_FRAME_OPTIONS = "DENY"
75 | app = Crax(settings='tests.config_files.conf_rest')
76 |
--------------------------------------------------------------------------------
/tests/config_files/conf_auth_right_no_db_options.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | try:
6 | from test_app_auth.routers import Handler500
7 | from test_app_auth.urls_auth import url_list
8 | except ImportError:
9 | from ..test_app_auth.routers import Handler500
10 | from ..test_app_auth.urls_auth import url_list
11 |
12 | ALLOWED_HOSTS = ["*"]
13 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14 | SECRET_KEY = "qwerty1234567"
15 | MIDDLEWARE = [
16 | "crax.auth.middleware.AuthMiddleware",
17 | "crax.middleware.x_frame.XFrameMiddleware",
18 | "crax.middleware.max_body.MaxBodySizeMiddleware",
19 | "crax.auth.middleware.SessionMiddleware",
20 | "crax.middleware.cors.CorsHeadersMiddleware",
21 | ]
22 |
23 | APPLICATIONS = ["test_app", "test_app_auth"]
24 | URL_PATTERNS = url_list
25 | STATIC_DIRS = ["static", "test_app/static"]
26 |
27 | TEST_MODE = os.environ["CRAX_TEST_MODE"]
28 | DB_USERS = {"postgresql": "postgres", "mysql": "root", "sqlite": "root"}
29 | DB_NAMES = {
30 | "postgresql": "test_crax",
31 | "mysql": "test_crax",
32 | "sqlite": f"/{BASE_URL}/test_crax.sqlite",
33 | }
34 |
35 |
36 | def get_db_host():
37 | docker_db_host = os.environ.get("DOCKER_DATABASE_HOST", None)
38 | if docker_db_host:
39 | host = docker_db_host
40 | else:
41 | host = "127.0.0.1"
42 | return host
43 |
44 |
45 | if "TRAVIS" not in os.environ:
46 | DATABASES = {
47 | "default": {
48 | "driver": TEST_MODE,
49 | "host": get_db_host(),
50 | "user": "crax",
51 | "password": "CraxPassword",
52 | "name": DB_NAMES[TEST_MODE],
53 | },
54 | }
55 |
56 | else:
57 | DATABASES = {
58 | "default": {
59 | "driver": TEST_MODE,
60 | "host": "127.0.0.1",
61 | "user": DB_USERS[TEST_MODE],
62 | "password": "",
63 | "name": DB_NAMES[TEST_MODE],
64 | },
65 | }
66 |
67 |
68 | ERROR_HANDLERS = {
69 | "500_handler": Handler500,
70 | }
71 |
72 | X_FRAME_OPTIONS = "DENY"
73 | app = Crax(settings='tests.config_files.conf_auth_right_no_db_options')
74 |
--------------------------------------------------------------------------------
/tests/test_app_nested/routers.py:
--------------------------------------------------------------------------------
1 | from crax.views import TemplateView, JSONView
2 |
3 |
4 | class TestGetParams(TemplateView):
5 | template = "test_params.html"
6 | methods = ["GET"]
7 |
8 | async def get(self):
9 | params = self.request.params
10 | query = self.request.query
11 | self.context = {"query": query, "params": params}
12 |
13 |
14 | class TestMissedTemplate(TemplateView):
15 | methods = ["GET"]
16 |
17 |
18 | class TestNotFoundTemplate(TemplateView):
19 | template = "not_found_template"
20 | methods = ["GET"]
21 |
22 |
23 | class TestInnerTemplateExceptions(TemplateView):
24 | template = "not_found_template"
25 | methods = ["GET"]
26 |
27 |
28 | class TestGetParamsRegex(TemplateView):
29 | template = "test_params.html"
30 | methods = ["GET", "POST"]
31 |
32 | async def get(self):
33 | params = self.request.params
34 | query = self.request.query
35 | self.context = {"query": query, "params": params}
36 |
37 |
38 | class TestJSONView(JSONView):
39 | methods = ["GET"]
40 |
41 | async def get(self):
42 | self.context = {"data": "Test_data"}
43 |
44 |
45 | class TestSendCookiesBack(JSONView):
46 | methods = ["GET"]
47 |
48 | async def get(self):
49 | self.context = {"data": self.request.headers["cookie"]}
50 |
51 |
52 | class TestSetCookies(JSONView):
53 | methods = ["GET"]
54 |
55 | async def create_context(self):
56 | self.context = {"data": "Test_data"}
57 | response = await super(TestSetCookies, self).create_context()
58 | response.set_cookies(
59 | "test_cookie", "test_cookie_value", {"path": "/", "httponly": "true"}
60 | )
61 | return response
62 |
63 |
64 | class TestEmptyMethods(TemplateView):
65 | template = "index.html"
66 | methods = []
67 |
68 |
69 | class TestMasquerade(TemplateView):
70 | template = "index.html"
71 | scope = [
72 | "index.html",
73 | "masquerade_1.html",
74 | "masquerade_2.html",
75 | "masquerade_3.html",
76 | ]
77 |
78 |
79 | class TestMasqueradeNoScope(TemplateView):
80 | template = "index.html"
81 |
82 |
83 | class TestUrlCreation(TemplateView):
84 | template = "create_url.html"
85 |
--------------------------------------------------------------------------------
/docker/app/launch_tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | cd tests
4 | cp -r ../venv .
5 | PYTHON_PATH='venv/bin/python3'
6 | LOG_FILE=$(echo $(pwd)'/docker/test.log')
7 |
8 | echo -e '\e[32mTESTS STARTED. OPEN YOUR BROWSER AT 127.0.0.1:5000 TO SEE DETAILS\032'
9 |
10 | function pyTestCrax() {
11 | export CRAX_TEST_MODE='mysql'
12 | export DOCKER_DATABASE_HOST='mysql-container'
13 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-config=.coveragerc test_files/auth_tests.py test_files/command_one.py
14 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_two.py
15 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_three.py
16 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_four.py
17 |
18 | export CRAX_TEST_MODE='postgresql'
19 | export DOCKER_DATABASE_HOST='postgres-container'
20 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-config=.coveragerc test_files/auth_tests.py test_files/command_one.py
21 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_two.py
22 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_three.py
23 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_four.py
24 |
25 | export CRAX_TEST_MODE='sqlite'
26 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-config=.coveragerc test_files/auth_tests.py test_files/command_one.py
27 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_two.py
28 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_three.py
29 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/command_four.py
30 |
31 | ${PYTHON_PATH} -m pip uninstall --yes sqlalchemy databases alembic
32 | ${PYTHON_PATH} -m pytest --cov=../crax --cov-append --cov-config=.coveragerc test_files/common_tests.py
33 | }
34 |
35 | function runTests() {
36 | rm -f ${LOG_FILE}
37 | touch ${LOG_FILE}
38 | pyTestCrax | tee ${LOG_FILE}
39 | }
40 |
41 | runTests
42 | echo 'ALL TESTS DONE' >> ${LOG_FILE}
43 | echo -e '\e[32mTESTS FINISHED.\032'
44 |
--------------------------------------------------------------------------------
/crax/request.py:
--------------------------------------------------------------------------------
1 | """
2 | Crax Request. Sure, i've read about standard lib SimpleCookie problem
3 | and I hope it will be fixed soon. So I do not like to replace stdlib
4 | code with my own.
5 | """
6 | from http.cookies import SimpleCookie
7 | from urllib import parse
8 | import typing
9 |
10 |
11 | class Request:
12 | def __init__(self, scope: typing.MutableMapping[str, typing.Any]) -> None:
13 | self.scope = scope
14 | self.params = {}
15 | self.query = {}
16 | self.data = None
17 | self.headers = None
18 | self.server = None
19 | self.client = None
20 | self.cookies = {}
21 | self.session = {}
22 | self.user = None
23 | self.ws_secret = None
24 | self.scheme = scope.get("type", "http")
25 | self.method = scope.get("method", None)
26 | self.path = scope.get("path", None)
27 | self.content_type = None
28 | self.prepare_request()
29 | self.post = {}
30 | self.files = {}
31 | self.messages = []
32 | self.response_headers = {}
33 |
34 | @property
35 | def session(self) -> dict:
36 | return self._session
37 |
38 | @session.setter
39 | def session(self, val) -> None:
40 | self._session = val
41 |
42 | def prepare_request(self) -> None:
43 | self.headers = dict(
44 | [
45 | (x[0].decode("utf-8"), x[1].decode("utf-8"))
46 | for x in self.scope["headers"]
47 | ]
48 | )
49 | self.server = ":".join([str(x) for x in self.scope["server"]])
50 | self.client = ":".join([str(x) for x in self.scope["client"]])
51 | self.content_type = self.headers.get("content-type", None)
52 | if "cookie" in self.headers:
53 | cookie = SimpleCookie()
54 | cookie.load(self.headers["cookie"])
55 | for key, morsel in cookie.items():
56 | self.cookies[key] = morsel.value
57 |
58 | if "query_string" in self.scope and self.scope["query_string"]:
59 | self.query = parse.parse_qs(self.scope["query_string"].decode("utf-8"))
60 |
61 | if self.scheme == "websocket":
62 | if "sec-websocket-key" in self.headers:
63 | self.cookies.update({"ws_secret": self.headers["sec-websocket-key"]})
64 |
--------------------------------------------------------------------------------
/tests/config_files/conf_auth_right.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | try:
6 | from test_app_auth.routers import Handler500
7 | from test_app_auth.urls_auth import url_list
8 | except ImportError:
9 | from ..test_app_auth.routers import Handler500
10 | from ..test_app_auth.urls_auth import url_list
11 |
12 | ALLOWED_HOSTS = ["*"]
13 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14 | SECRET_KEY = "qwerty1234567"
15 | MIDDLEWARE = [
16 | "crax.auth.middleware.AuthMiddleware",
17 | "crax.middleware.x_frame.XFrameMiddleware",
18 | "crax.middleware.max_body.MaxBodySizeMiddleware",
19 | "crax.auth.middleware.SessionMiddleware",
20 | "crax.middleware.cors.CorsHeadersMiddleware",
21 | ]
22 |
23 | APPLICATIONS = ["test_app_common", "test_app_auth"]
24 | URL_PATTERNS = url_list
25 | STATIC_DIRS = ["static", "test_app/static"]
26 |
27 | TEST_MODE = os.environ["CRAX_TEST_MODE"]
28 | DB_USERS = {"postgresql": "postgres", "mysql": "root", "sqlite": "root"}
29 | DB_NAMES = {
30 | "postgresql": "test_crax",
31 | "mysql": "test_crax",
32 | "sqlite": f"/{BASE_URL}/test_crax.sqlite",
33 | }
34 | if TEST_MODE != "sqlite":
35 | OPTIONS = {"min_size": 5, "max_size": 20}
36 | else:
37 | OPTIONS = {}
38 |
39 |
40 | def get_db_host():
41 | docker_db_host = os.environ.get("DOCKER_DATABASE_HOST", None)
42 | if docker_db_host:
43 | host = docker_db_host
44 | else:
45 | host = "127.0.0.1"
46 | return host
47 |
48 |
49 | if "TRAVIS" not in os.environ:
50 | DATABASES = {
51 | "default": {
52 | "driver": TEST_MODE,
53 | "host": get_db_host(),
54 | "user": "crax",
55 | "password": "CraxPassword",
56 | "name": DB_NAMES[TEST_MODE],
57 | "options": OPTIONS,
58 | },
59 | }
60 |
61 | else:
62 | DATABASES = {
63 | "default": {
64 | "driver": TEST_MODE,
65 | "host": "127.0.0.1",
66 | "user": DB_USERS[TEST_MODE],
67 | "password": "",
68 | "name": DB_NAMES[TEST_MODE],
69 | "options": OPTIONS,
70 | },
71 | }
72 |
73 |
74 | ERROR_HANDLERS = {
75 | "500_handler": Handler500,
76 | }
77 |
78 | X_FRAME_OPTIONS = "DENY"
79 | app = Crax(settings='tests.config_files.conf_auth_right')
80 |
--------------------------------------------------------------------------------
/tests/app_four/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | ALLOWED_HOSTS = ["*"]
6 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 | SECRET_KEY = "qwerty1234567"
8 | MIDDLEWARE = [
9 | "crax.auth.middleware.AuthMiddleware",
10 | "crax.middleware.x_frame.XFrameMiddleware",
11 | "crax.middleware.max_body.MaxBodySizeMiddleware",
12 | "crax.auth.middleware.SessionMiddleware",
13 | "crax.middleware.cors.CorsHeadersMiddleware",
14 | ]
15 |
16 | APPLICATIONS = ["app_four", "app_six"]
17 | URL_PATTERNS = []
18 | STATIC_DIRS = ["static", "test_app/static"]
19 | TEST_MODE = os.environ["CRAX_TEST_MODE"]
20 | DB_USERS = {"postgresql": "postgres", "mysql": "root", "sqlite": "root"}
21 | DB_NAMES = {
22 | "postgresql": "test_crax",
23 | "mysql": "test_crax",
24 | "sqlite": f"/{BASE_URL}/test_crax.sqlite",
25 | }
26 | if TEST_MODE != "sqlite":
27 | OPTIONS = {"min_size": 5, "max_size": 20}
28 | else:
29 | OPTIONS = {}
30 |
31 |
32 | def get_db_host():
33 | docker_db_host = os.environ.get("DOCKER_DATABASE_HOST", None)
34 | if docker_db_host:
35 | host = docker_db_host
36 | else:
37 | host = "127.0.0.1"
38 | return host
39 |
40 |
41 | if "TRAVIS" not in os.environ:
42 | DATABASES = {
43 | "default": {
44 | "driver": TEST_MODE,
45 | "host": get_db_host(),
46 | "user": "crax",
47 | "password": "CraxPassword",
48 | "name": DB_NAMES[TEST_MODE],
49 | "options": OPTIONS,
50 | },
51 | "custom": {
52 | "driver": TEST_MODE,
53 | "host": get_db_host(),
54 | "user": "crax",
55 | "password": "CraxPassword",
56 | "name": DB_NAMES[TEST_MODE],
57 | "options": OPTIONS,
58 | },
59 | }
60 |
61 | else:
62 | DATABASES = {
63 | "default": {
64 | "driver": TEST_MODE,
65 | "host": "127.0.0.1",
66 | "user": DB_USERS[TEST_MODE],
67 | "password": "",
68 | "name": DB_NAMES[TEST_MODE],
69 | "options": OPTIONS,
70 | },
71 | "custom": {
72 | "driver": TEST_MODE,
73 | "host": "127.0.0.1",
74 | "user": DB_USERS[TEST_MODE],
75 | "password": "",
76 | "name": DB_NAMES[TEST_MODE],
77 | "options": OPTIONS,
78 | },
79 | }
80 |
81 |
82 | X_FRAME_OPTIONS = "DENY"
83 | app = Crax(settings='app_four.conf')
84 |
--------------------------------------------------------------------------------
/crax/exceptions.py:
--------------------------------------------------------------------------------
1 | """
2 | Base Exception classes. In case if debug mode is set to ON, exceptions
3 | will be shown in browser, otherwise crax will try to find user defined
4 | views according to error status code. If no error view will be found
5 | exception will be raised common way. If logging system was enabled
6 | in project settings all exceptions will be logged.
7 | """
8 | import typing
9 |
10 |
11 | class BaseCraxException(Exception):
12 | message = None
13 | status_code = 500
14 |
15 | def __init__(self, *args: typing.Optional[typing.Any]) -> None:
16 | super(BaseCraxException, self).__init__("%s %s" % (self.message, *args))
17 | raise self
18 |
19 |
20 | class CraxUnauthorized(BaseCraxException):
21 | message = "Not Authorized"
22 | status_code = 401
23 |
24 |
25 | class CraxForbidden(BaseCraxException):
26 | message = "Access Denied"
27 | status_code = 403
28 |
29 |
30 | class CraxTemplateNotFound(BaseCraxException):
31 | message = "Template not found"
32 | status_code = 404
33 |
34 |
35 | class CraxPathNotFound(BaseCraxException):
36 | message = "Path not found"
37 | status_code = 404
38 |
39 |
40 | class CraxImproperlyConfigured(BaseCraxException):
41 | message = "Invalid configuration: "
42 | status_code = 500
43 |
44 |
45 | class CraxNoTemplateGiven(BaseCraxException):
46 | message = "No template given"
47 | status_code = 500
48 |
49 |
50 | class CraxNoMethodGiven(BaseCraxException):
51 | message = "No method was given for the view"
52 | status_code = 500
53 |
54 |
55 | class CraxEmptyMethods(BaseCraxException):
56 | message = "No methods was specified for the view"
57 | status_code = 500
58 |
59 |
60 | class CraxMethodNotAllowed(BaseCraxException):
61 | message = "Method not allowed for this view"
62 | status_code = 405
63 |
64 |
65 | class CraxNoRootPath(BaseCraxException):
66 | message = "No root path in url lists found"
67 | status_code = 500
68 |
69 |
70 | class CraxDataBaseImproperlyConfigured(BaseCraxException):
71 | message = "Database connection improperly configured"
72 | status_code = 500
73 |
74 |
75 | class CraxDataBaseNotFound(BaseCraxException):
76 | message = "Database not found"
77 | status_code = 500
78 |
79 |
80 | class CraxMigrationsError(BaseCraxException):
81 | message = "Migration Error"
82 | status_code = 500
83 |
84 |
85 | class CraxUnknownUrlParameterTypeError(BaseCraxException):
86 | message = "Swagger Error"
87 | status_code = 500
88 |
--------------------------------------------------------------------------------
/tests/app_five/conf.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 |
5 | ALLOWED_HOSTS = ["*"]
6 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 | SECRET_KEY = "qwerty1234567"
8 | MIDDLEWARE = [
9 | "crax.auth.middleware.AuthMiddleware",
10 | "crax.middleware.x_frame.XFrameMiddleware",
11 | "crax.middleware.max_body.MaxBodySizeMiddleware",
12 | "crax.auth.middleware.SessionMiddleware",
13 | "crax.middleware.cors.CorsHeadersMiddleware",
14 | ]
15 |
16 | APPLICATIONS = ["app_five", "app_two", "app_three"]
17 | URL_PATTERNS = []
18 | STATIC_DIRS = ["static", "test_app/static"]
19 | TEST_MODE = os.environ["CRAX_TEST_MODE"]
20 | DB_USERS = {"postgresql": "postgres", "mysql": "root", "sqlite": "root"}
21 | DB_NAMES = {
22 | "postgresql": "test_crax",
23 | "mysql": "test_crax",
24 | "sqlite": f"/{BASE_URL}/test_crax.sqlite",
25 | }
26 | if TEST_MODE != "sqlite":
27 | OPTIONS = {"min_size": 5, "max_size": 20}
28 | else:
29 | OPTIONS = {}
30 |
31 |
32 | def get_db_host():
33 | docker_db_host = os.environ.get("DOCKER_DATABASE_HOST", None)
34 | if docker_db_host:
35 | host = docker_db_host
36 | else:
37 | host = "127.0.0.1"
38 | return host
39 |
40 |
41 | if "TRAVIS" not in os.environ:
42 | DATABASES = {
43 | "default": {
44 | "driver": TEST_MODE,
45 | "host": get_db_host(),
46 | "user": DB_NAMES[TEST_MODE],
47 | "password": "CraxPassword",
48 | "name": "test_crax",
49 | "options": OPTIONS,
50 | },
51 | "custom": {
52 | "driver": TEST_MODE,
53 | "host": get_db_host(),
54 | "user": "crax",
55 | "password": "CraxPassword",
56 | "name": DB_NAMES[TEST_MODE],
57 | "options": OPTIONS,
58 | },
59 | }
60 |
61 | else:
62 | DATABASES = {
63 | "default": {
64 | "driver": TEST_MODE,
65 | "host": "127.0.0.1",
66 | "user": DB_USERS[TEST_MODE],
67 | "password": "",
68 | "name": DB_NAMES[TEST_MODE],
69 | "options": OPTIONS,
70 | },
71 | "custom": {
72 | "driver": TEST_MODE,
73 | "host": "127.0.0.1",
74 | "user": DB_USERS[TEST_MODE],
75 | "password": "",
76 | "name": DB_NAMES[TEST_MODE],
77 | "options": OPTIONS,
78 | },
79 | }
80 |
81 |
82 | X_FRAME_OPTIONS = "DENY"
83 | app = Crax(settings='app_five.conf')
84 |
--------------------------------------------------------------------------------
/tests/README.md:
--------------------------------------------------------------------------------
1 | # CRAX Tests
2 | ## Explanation of Crax Framework testing
3 | Here explained what is Crax tests, how you should launch test, what have been tested,
4 | what was missed and why and finally here described tests structure.
5 |
6 | ### Test files
7 | The entry point to launch tests is `launch_tests.sh` script. It is simple bash script
8 | file that launches tests one by one. Crax is supposed to be able to work with several
9 | database backends e.g. `MySQL`, `PostgreSQL` and `SQLite`. Thus all tests have to run
10 | against all of backends listed above. Launcher starts test files in right order for all
11 | database backend. If any errors found on any step launcher stops it's job and
12 | exited. What does `right order` mean: First is to check all database depended stuff.
13 | Tests that written to check commands are chained to prepare python executables for
14 | next part of tests. For example `command_two.py` runs it's tests and prepares files
15 | for `command_three.py`. All sources for this kind of tests are placed in
16 | `app_*` directories. It is necessary to run something like migration tests. All
17 | test files that will be launched by `launch_tests.sh` are stored in `test_files`
18 | directory. First will be called all of `command_?.py` files. All commands will be
19 | tested against all of database backends. Next step is to test `authentication` against
20 | all of database backends. And finally will be launched common tests which are
21 | database independent. Why so? It is about ability to work without database backend
22 | at all. Any Crax application could be built without database, models, authentication
23 | and so on.
24 |
25 | ### Config files
26 | All config files for all common tests. Config files for command testing are placed
27 | in `app_*` directories.
28 |
29 | ### Test app auth
30 | All source files for testing authentication backend. However config files are in
31 | `config_files` directory.
32 |
33 | ### Test app common
34 | All files that serves common application tests are stored here. Also here placed
35 | python file named `urls_two_apps.py` - it is file with all common test urls including
36 | nested apps tests.
37 |
38 | ### Test app nested
39 | It is a part of common tests that shows how nested apps, namespaces and url resolving
40 | for nested applications should work. Another one goal is to show that does not matter
41 | how some of files are named. Three levels of this directory contain crax applications
42 | with files like `controllers.py`, `handlers.py` or `views.py` that do the same things.
--------------------------------------------------------------------------------
/crax/database/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from importlib import import_module, resources
4 | from itertools import chain
5 | from typing import Optional
6 |
7 | import typing
8 |
9 | try:
10 | from sqlalchemy import MetaData
11 | except ImportError: # pragma: no cover
12 | MetaData = None
13 |
14 | sys.path = ["", ".."] + sys.path[1:]
15 |
16 |
17 | def get_metadata(app: str) -> Optional[MetaData]:
18 | meta = []
19 | control = []
20 | if callable(MetaData):
21 | metadata = MetaData()
22 |
23 | for name in resources.contents(app):
24 | if name == "models.py":
25 | _module = import_module(f"{app}.{name[:-3]}")
26 | for model in dir(_module):
27 | table = getattr(_module, model)
28 | if hasattr(table, "metadata") and hasattr(table, "database"):
29 | table_module = getattr(table, "__module__", None)
30 | if (
31 | table_module == _module.__name__
32 | and table.database == os.environ["CRAX_DB_NAME"]
33 | ):
34 | meta.append(table)
35 | control.append(table.table.name)
36 |
37 | for base in meta:
38 | for (table_name, table) in base.metadata.tables.items():
39 | if table_name in control:
40 | metadata._add_table(table_name, table.schema, table)
41 | return metadata
42 |
43 |
44 | def sort_applications(
45 | table_map: typing.Mapping[str, typing.List[set]], revers=False
46 | ) -> typing.List[str]:
47 | def get_index(lst, elem):
48 | for x in lst:
49 | if x == elem:
50 | return lst.index(x)
51 |
52 | p = []
53 | c = {x: [] for x in table_map.keys()}
54 | for x, y in table_map.items():
55 | diff = set.difference(y[0], y[1])
56 | if diff:
57 | for k, v in table_map.items():
58 | if set.intersection(diff, v[1]):
59 | c[k].append(x)
60 | for k, v in c.items():
61 | for i in c.keys():
62 | if i in v:
63 | if k not in p:
64 | p.append(k)
65 | ind = get_index(p, k)
66 | if i in p:
67 | p.remove(i)
68 | p.insert(ind + 1, i)
69 | else:
70 | if i not in p:
71 | p.append(i)
72 | res = list(chain([x for x in p if c[x]], [x for x in p if not c[x]]))
73 | if revers is True:
74 | return res[::-1]
75 | return res
76 |
--------------------------------------------------------------------------------
/crax/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | System helpers.
3 | """
4 | import os
5 |
6 | import typing
7 | from crax.response_types import TextResponse
8 |
9 |
10 | def get_settings(settings: str = None) -> typing.Any:
11 | if settings is None:
12 | settings = os.environ.get("CRAX_SETTINGS", "crax.conf")
13 | try:
14 | spl_settings = settings.split(".")
15 | return __import__(settings, fromlist=spl_settings)
16 | except (ImportError, ModuleNotFoundError) as ex:
17 | raise ex.__class__(ex) from ex
18 |
19 |
20 | def get_settings_variable(variable: str, default=None) -> typing.Any:
21 | settings = get_settings()
22 | if hasattr(settings, variable):
23 | return getattr(settings, variable)
24 | return default
25 |
26 |
27 | async def collect_middleware(based: str) -> typing.Any:
28 | middleware = get_settings_variable("MIDDLEWARE")
29 | middleware_list = []
30 | if middleware:
31 | for m in middleware:
32 | spl_middleware = m.split(".")
33 | try:
34 | module = __import__(
35 | ".".join(spl_middleware[:-1]), fromlist=spl_middleware[:-1]
36 | )
37 | middle = getattr(module, spl_middleware[-1])
38 | if based in [x.__name__ for x in middle.__bases__]:
39 | middleware_list.append(middle)
40 | except (ImportError, AttributeError, ModuleNotFoundError) as ex:
41 | return ex
42 | return middleware_list
43 |
44 |
45 | def unpack_urls(nest: typing.Any) -> typing.Generator:
46 | if isinstance(nest, list):
47 | for lst in nest:
48 | for x in unpack_urls(lst):
49 | yield x
50 | else:
51 | yield nest
52 |
53 |
54 | def get_error_handler(error: typing.Any) -> typing.Callable:
55 | error_handlers = get_settings_variable("ERROR_HANDLERS")
56 | if hasattr(error, 'status_code'):
57 | status_code = error.status_code
58 | else:
59 | status_code = 500
60 | handler = None
61 | if error_handlers is not None:
62 | if isinstance(error_handlers, dict) and error_handlers:
63 | try:
64 | if hasattr(error, "status_code"):
65 | handler = error_handlers[f"{status_code}_handler"]
66 | else:
67 | handler = error_handlers["500_handler"]
68 | except KeyError: # pragma: no cover
69 | pass
70 | else:
71 | if hasattr(error, 'message'):
72 | message = error.message
73 | else:
74 | message = 'Internal server error'
75 | handler = TextResponse(None, message, status_code=status_code)
76 | return handler
77 |
--------------------------------------------------------------------------------
/tests/docker/streams/templates/get_stream.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CRAX Tests
6 |
9 |
10 |
11 |
14 | Realtime logs
15 |
16 |
17 |
18 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/crax/middleware/base.py:
--------------------------------------------------------------------------------
1 | """
2 | Base classes to write middleware. All middleware should inherit from this
3 | classes. To modify Request should be used RequestMiddleware. To modify
4 | Response should be used ResponseMiddleware. The difference is that the
5 | first ones will be processed BEFORE application will launched. If any
6 | errors will raised during middleware processing application process
7 | will not be continued. ResponseMiddleware modifies headers and body
8 | AFTER request processed.
9 | """
10 | import asyncio
11 | from abc import ABC, abstractmethod
12 |
13 | import typing
14 | from crax.request import Request
15 | from crax.response_types import StreamingResponse
16 |
17 |
18 | class ResponseMiddleware(ABC):
19 | # Thanks to Starlette for the idea of implementing the Response Middleware call stack.
20 |
21 | def __init__(self, app) -> None:
22 | self.app = app
23 | self.request = app.request
24 | self.headers = []
25 |
26 | async def __call__(self, scope, receive, send) -> None:
27 | self.receive = receive
28 | response = await self.process_headers()
29 | await response(scope, receive, send)
30 |
31 | async def call_next(self, request: Request):
32 | loop = asyncio.get_event_loop()
33 | queue = asyncio.Queue()
34 | scope = request.scope
35 | receive = self.receive
36 | send = queue.put
37 |
38 | async def task() -> None:
39 | try:
40 | await self.app(scope, receive, send)
41 | finally:
42 | await queue.put(None)
43 |
44 | _task = loop.create_task(task())
45 | message = await queue.get()
46 | if message is None:
47 | _task.result()
48 | raise RuntimeError("No response returned.")
49 |
50 | async def body_stream() -> typing.AsyncGenerator[bytes, None]:
51 | while True:
52 | m = await queue.get()
53 | if m is None:
54 | break
55 | yield m["body"]
56 | _task.result()
57 |
58 | response = StreamingResponse(
59 | self.request, status_code=message["status"], content=body_stream()
60 | )
61 | response.headers = message["headers"]
62 | return response
63 |
64 | @abstractmethod
65 | async def process_headers(self):
66 | response = await self.call_next(self.request)
67 | return response
68 |
69 |
70 | class RequestMiddleware(ABC):
71 | def __init__(self, request: Request) -> None:
72 | self.request = request
73 | self.headers = self.request.response_headers
74 |
75 | @abstractmethod
76 | async def process_headers(self) -> typing.Any: # pragma: no cover
77 | pass
78 |
--------------------------------------------------------------------------------
/crax/database/connection.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | import typing
3 |
4 | from databases import Database
5 |
6 | from crax.exceptions import CraxDataBaseImproperlyConfigured
7 |
8 | try:
9 | from sqlalchemy import MetaData, Table, UniqueConstraint, Column, Integer, select
10 | except ImportError: # pragma: no cover
11 | raise
12 |
13 | from crax.utils import get_settings
14 |
15 |
16 | @dataclass
17 | class Connection:
18 | pool: typing.Any = None
19 | driver: str = None
20 | main: bool = True
21 |
22 |
23 | async def create_connections() -> dict:
24 | configuration = get_settings()
25 | connections = {}
26 | if not hasattr(configuration, "DATABASES"):
27 | raise CraxDataBaseImproperlyConfigured(
28 | "Improperly configured project settings. "
29 | 'Missed required parameter "DATABASES"'
30 | )
31 | databases = configuration.DATABASES
32 | if not isinstance(databases, dict):
33 | raise CraxDataBaseImproperlyConfigured(
34 | "Improperly configured project settings."
35 | " DATABASES parameter should be a dict"
36 | )
37 |
38 | elif "default" not in databases:
39 | raise CraxDataBaseImproperlyConfigured(
40 | "Improperly configured project settings. "
41 | 'DATABASES dictionary should contain "default" database'
42 | )
43 | for table in configuration.DATABASES:
44 | table_base = configuration.DATABASES[table]
45 | driver = table_base["driver"]
46 |
47 | if "options" in table_base:
48 | connection_options = table_base["options"]
49 | else:
50 | connection_options = {}
51 | if table_base["driver"] != 'sqlite':
52 | port = table_base.get('port')
53 | if port:
54 | connection_string = (
55 | f'{table_base["driver"]}://{table_base["user"]}:'
56 | f'{table_base["password"]}@{table_base["host"]}:{port}/{table_base["name"]}'
57 | )
58 | else:
59 | connection_string = (
60 | f'{table_base["driver"]}://{table_base["user"]}:'
61 | f'{table_base["password"]}@{table_base["host"]}/{table_base["name"]}'
62 | )
63 | else:
64 | connection_string = (
65 | f'{table_base["driver"]}://{table_base["name"]}'
66 | )
67 | connection = Database(connection_string, **connection_options)
68 | await connection.connect()
69 |
70 | connection = Connection(pool=connection, driver=driver)
71 | connections[table] = connection
72 |
73 | return connections
74 |
75 |
76 | async def close_pool(connections):
77 | if connections:
78 | for connection in connections:
79 | await connection.pool.disconnect()
80 |
--------------------------------------------------------------------------------
/crax/commands/history.py:
--------------------------------------------------------------------------------
1 | """
2 | Command to get migrations history, latest migration according to project application.
3 | All revision branches are linked to applications defined in project settings
4 | """
5 |
6 | import os
7 | import sys
8 | from typing import Optional
9 |
10 | import typing
11 | from alembic.command import history
12 | from alembic.script import ScriptDirectory
13 |
14 | from crax.database.command import DataBaseCommands
15 | from crax.exceptions import CraxMigrationsError
16 |
17 | options = [
18 | (["--database", "-b"], {"type": str, "help": "specify database to run migrations"}),
19 | (["--apps", "-a"], {"type": str, "help": "specify app", "nargs": "*"},),
20 | (
21 | ["--latest", "-l"],
22 | {"help": "get latest revisions", "action": "store_true", "dest": "latest"},
23 | ),
24 | (["--step", "-s"], {"help": "specify step"}),
25 | ]
26 |
27 |
28 | class DBHistory(DataBaseCommands):
29 | def __init__(self, opts: typing.List[typing.Union[tuple]], **kwargs) -> None:
30 | super(DBHistory, self).__init__(opts, **kwargs)
31 | self.kwargs = kwargs
32 |
33 | def get_revision(self, app: str, index_: int) -> Optional[str]:
34 | script = ScriptDirectory.from_config(self.config)
35 | rev = script.walk_revisions(
36 | f"{self.db_name}/{app}@base", f"{self.db_name}/{app}@head"
37 | )
38 | revision = next(filter(lambda x: x[0] == index_, enumerate(rev)), None)
39 | if revision:
40 | return revision[1]
41 |
42 | def show_history(self) -> None:
43 | step = self.get_option("step")
44 | latest = self.get_option("latest")
45 | dir_exists = os.path.exists(f"{self.project_url}/{self.alembic_dir}")
46 | if self.config is None or not dir_exists:
47 | raise CraxMigrationsError(
48 | "You can not run show history command before migrations not created"
49 | )
50 |
51 | for app in self.applications:
52 | if self.check_branch_exists(f"{self.db_name}/{app}"):
53 | os.environ["CRAX_ONLINE"] = "true"
54 | os.environ["CRAX_CURRENT"] = app
55 | top_formatter = (
56 | f'{app} history. Database: {self.db_name} \n {"*" * 40} \n'
57 | )
58 | bottom_formatter = f'\n {"*" * 40} \n'
59 | if step is not None:
60 | revision = self.get_revision(app, int(step))
61 | elif latest is True:
62 | revision = self.get_revision(app, 0)
63 | else:
64 | revision = None
65 | history(self.config, f":{self.db_name}/{app}@head")
66 | if revision:
67 | sys.stdout.write(top_formatter)
68 | self.config.print_stdout(revision)
69 | sys.stdout.write(bottom_formatter)
70 |
71 |
72 | if __name__ == "__main__": # pragma: no cover
73 | show_history = DBHistory(options).show_history
74 | show_history()
75 |
--------------------------------------------------------------------------------
/tests/config_files/conf_csrf.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from crax import Crax
4 | from crax.swagger.types import SwaggerInfo
5 | from crax.swagger import urls
6 |
7 | try:
8 | from test_selenium.urls import url_list
9 | except ImportError:
10 | from ..test_selenium.urls import url_list
11 |
12 |
13 | ALLOWED_HOSTS = ["*"]
14 | BASE_URL = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15 | SECRET_KEY = "qwerty1234567"
16 | MIDDLEWARE = [
17 | "crax.auth.middleware.AuthMiddleware",
18 | "crax.middleware.x_frame.XFrameMiddleware",
19 | "crax.middleware.max_body.MaxBodySizeMiddleware",
20 | "crax.auth.middleware.SessionMiddleware",
21 | "crax.middleware.cors.CorsHeadersMiddleware",
22 | ]
23 |
24 | APPLICATIONS = ["test_selenium"]
25 | URL_PATTERNS = url_list + urls
26 | STATIC_DIRS = ["static", "test_selenium/static"]
27 |
28 |
29 | TEST_MODE = os.environ["CRAX_TEST_MODE"]
30 | DB_USERS = {"postgresql": "postgres", "mysql": "root", "sqlite": "root"}
31 | DB_NAMES = {
32 | "postgresql": "test_crax",
33 | "mysql": "test_crax",
34 | "sqlite": f"/{BASE_URL}/test_crax.sqlite",
35 | }
36 | if TEST_MODE != "sqlite":
37 | OPTIONS = {"min_size": 5, "max_size": 20}
38 | else:
39 | OPTIONS = {}
40 |
41 |
42 | def get_db_host():
43 | docker_db_host = os.environ.get("DOCKER_DATABASE_HOST", None)
44 | if docker_db_host:
45 | host = docker_db_host
46 | else:
47 | host = "127.0.0.1"
48 | return host
49 |
50 |
51 | if "TRAVIS" not in os.environ:
52 | DATABASES = {
53 | "default": {
54 | "driver": TEST_MODE,
55 | "host": get_db_host(),
56 | "user": "crax",
57 | "password": "CraxPassword",
58 | "name": DB_NAMES[TEST_MODE],
59 | "options": OPTIONS,
60 | },
61 | }
62 |
63 | else:
64 | DATABASES = {
65 | "default": {
66 | "driver": TEST_MODE,
67 | "host": "127.0.0.1",
68 | "user": DB_USERS[TEST_MODE],
69 | "password": "",
70 | "name": DB_NAMES[TEST_MODE],
71 | "options": OPTIONS,
72 | },
73 | }
74 |
75 |
76 | SWAGGER = SwaggerInfo(
77 | description="This is a simple example of OpenAPI (Swagger) documentation. "
78 | " You can find out more about Swagger at "
79 | "[http://swagger.io](http://swagger.io) or on "
80 | "[irc.freenode.net, #swagger](http://swagger.io/irc/). ",
81 | version="0.0.3",
82 | title="Crax Swagger Example",
83 | termsOfService="https://github.com/ephmann/crax",
84 | contact={"email": "ephmanns@gmail.com"},
85 | license={"name": "MIT", "url": "https://opensource.org/licenses/MIT"},
86 | servers=[
87 | {"url": "http://127.0.0.1:8000", "description": "Development server http"},
88 | {"url": "https://127.0.0.1:8000", "description": "Staging server"},
89 | ],
90 | basePath="/api",
91 | )
92 |
93 |
94 | X_FRAME_OPTIONS = "DENY"
95 | ENABLE_CSRF = True
96 |
97 |
98 | def square_(a):
99 | return a * a
100 |
101 |
102 | def hello():
103 | return "Hello world"
104 |
105 |
106 | TEMPLATE_FUNCTIONS = [square_, hello]
107 | app = Crax(settings='tests.config_files.conf_csrf')
108 |
--------------------------------------------------------------------------------
/tests/test_files/command_three.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | import os
4 |
5 | import pytest
6 |
7 | from tests.app_one.models import CustomerB
8 | from tests.app_two.models import CustomerDiscount
9 | from tests.test_files.command_utils import Commands, replacement
10 |
11 |
12 | @pytest.fixture
13 | def create_command(request):
14 | return Commands(request.param)
15 |
16 |
17 | @pytest.mark.asyncio
18 | @pytest.mark.parametrize("create_command", [["app_one.conf", False, {}]], indirect=True)
19 | async def test_initial_migrations(create_command):
20 | make_migrations = create_command.make_migrations()
21 | make_migrations()
22 | migrate = Commands(["app_one.conf", False, {}]).migrate()
23 | migrate()
24 | values = {
25 | "username": "chris",
26 | "password": "qwerty",
27 | "bio": "Nil!",
28 | "first_name": "Chris",
29 | "age": 27,
30 | }
31 | await CustomerB.query.insert(values=values)
32 | await CustomerDiscount.query.insert(
33 | values={
34 | "name": "Customer Discount",
35 | "percent": 10,
36 | "start_date": datetime.datetime.now(),
37 | }
38 | )
39 | config = create_command.config
40 | versions = config.get_main_option("crax_latest_revisions")
41 | versions = json.loads(versions)
42 | version = [
43 | x
44 | for x in os.listdir("app_two/migrations")
45 | if x != "__pycache__" and versions["default/app_two"] not in x
46 | ][0][:-4]
47 |
48 | migrate = Commands(
49 | ["app_one.conf", False, {"down": True, "revision": version}]
50 | ).migrate()
51 | migrate()
52 | try:
53 | await CustomerDiscount.query.insert(
54 | values={
55 | "name": "Customer Discount",
56 | "percent": 10,
57 | "start_date": datetime.datetime.now(),
58 | }
59 | )
60 | except Exception as e:
61 | assert "start_date" in str(e)
62 | replacement(
63 | "app_two/models.py",
64 | "start_date = sa.Column(sa.DateTime(), nullable=True)",
65 | "# start_date = sa.Column(sa.DateTime(), nullable=True)",
66 | )
67 | replacement(
68 | "app_one/models.py",
69 | "age = sa.Column(sa.Integer(), nullable=True)",
70 | "# age = sa.Column(sa.Integer(), nullable=True)",
71 | )
72 |
73 |
74 | @pytest.mark.asyncio
75 | @pytest.mark.parametrize(
76 | "create_command", [["app_four.conf", True, {"database": "custom"}]], indirect=True
77 | )
78 | async def test_migrations_multi(create_command):
79 |
80 | make_migrations = create_command.make_migrations()
81 | make_migrations()
82 | migrate = create_command.migrate()
83 | migrate()
84 |
85 | try:
86 | values = {
87 | "id": 33,
88 | "username": "chris",
89 | "password": "qwerty",
90 | "bio": "Nil!",
91 | "first_name": "Chris",
92 | "age": 27,
93 | }
94 | await CustomerB.query.insert(values=values)
95 | except Exception as e:
96 | assert "age" in str(e)
97 |
98 | replacement(
99 | "app_six/models.py",
100 | "# start_date = sa.Column(sa.DateTime(), nullable=True)",
101 | "start_date = sa.Column(sa.DateTime(), nullable=True)",
102 | )
103 | replacement(
104 | "app_four/models.py",
105 | "# age = sa.Column(sa.Integer(), nullable=True)",
106 | "age = sa.Column(sa.Integer(), nullable=True)",
107 | )
108 |
--------------------------------------------------------------------------------
/tests/test_files/command_one.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | import shutil
5 | import pytest
6 | from alembic.config import Config
7 |
8 | from tests.test_files.command_utils import (
9 | get_config_variable,
10 | cleaner,
11 | Commands,
12 | replacement,
13 | check_table_exists,
14 | )
15 |
16 | OPTIONS = get_config_variable("COMMAND_OPTIONS")
17 |
18 |
19 | @pytest.fixture
20 | def create_command(request):
21 | return Commands(request.param)
22 |
23 |
24 | @pytest.mark.asyncio
25 | @pytest.mark.parametrize(
26 | "create_command", [["app_five.conf", True, {"database": "custom"}]], indirect=True
27 | )
28 | async def test_initial_migrations_custom_db(create_command):
29 | replacement("app_two/models.py", "# database = 'custom'", "database = 'custom'")
30 | replacement("app_three/models.py", "# database = 'custom'", "database = 'custom'")
31 | replacement(
32 | "app_three/models.py",
33 | "from tests.app_one.models import CustomerB, Vendor",
34 | "from tests.app_five.models import CustomerB, Vendor",
35 | )
36 | with cleaner("alembic_env"):
37 | make_migrations = create_command.make_migrations()
38 | make_migrations()
39 | config = Config("alembic.ini")
40 | db_map = json.loads(config.get_main_option("crax_db_map"))["custom"]
41 | assert ["app_five", "app_two", "app_three"] == list(db_map)
42 | assert config.get_main_option("crax_migrated") == "not migrated"
43 | assert os.path.exists("alembic_env")
44 | assert os.path.isfile("alembic.ini")
45 | assert os.path.exists("app_three/migrations/custom")
46 | assert os.path.exists("app_five/migrations/custom")
47 | assert os.path.exists("app_two/migrations/custom")
48 |
49 | create_all = Commands(
50 | ["app_five.conf", True, {"database": "custom"}]
51 | ).create_all()
52 | create_all()
53 | check = await check_table_exists(
54 | create_command.default_connection, "customer_b"
55 | )
56 | assert check is not False
57 | drop_all = Commands(["app_five.conf", True, {"database": "custom"}]).drop_all()
58 | drop_all()
59 | check = await check_table_exists(
60 | create_command.default_connection, "customer_b"
61 | )
62 | assert check is False
63 | if os.path.exists("alembic_env"):
64 | shutil.rmtree("alembic_env")
65 | os.remove("alembic.ini")
66 | shutil.rmtree("app_five/migrations")
67 | shutil.rmtree("app_two/migrations")
68 | shutil.rmtree("app_three/migrations")
69 | make_migrations()
70 | assert os.path.exists("alembic_env")
71 | assert os.path.isfile("alembic.ini")
72 | assert os.path.exists("app_five/migrations/custom")
73 | assert os.path.exists("app_two/migrations/custom")
74 | assert os.path.exists("app_three/migrations/custom")
75 | migrate = Commands(["app_five.conf", True, {"database": "custom"}]).migrate()
76 | migrate()
77 | check = await check_table_exists(
78 | create_command.default_connection, "customer_b"
79 | )
80 | assert check is True
81 | replacement("app_two/models.py", "database = 'custom'", "# database = 'custom'")
82 | replacement(
83 | "app_three/models.py", "database = 'custom'", "# database = 'custom'"
84 | )
85 | replacement(
86 | "app_three/models.py",
87 | "from tests.app_five.models import CustomerB, Vendor",
88 | "from tests.app_one.models import CustomerB, Vendor",
89 | )
90 |
--------------------------------------------------------------------------------
/crax/logger.py:
--------------------------------------------------------------------------------
1 | """
2 | Base Logger class. All loggers should inherit from this one or
3 | "get_logger" method should be defined if custom logger does not inherit
4 | from BaseLogger class. By default logging disabled and should be set to
5 | on in project settings.
6 | """
7 | from abc import ABC, abstractmethod
8 | import logging
9 | import sys
10 | from logging.handlers import TimedRotatingFileHandler
11 |
12 | import typing
13 |
14 | from crax.utils import get_settings_variable
15 |
16 | try:
17 | import sentry_sdk
18 | from sentry_sdk.integrations.logging import LoggingIntegration
19 |
20 | except ImportError:
21 | LoggingIntegration: typing.Any
22 | sentry_sdk = LoggingIntegration = None
23 |
24 |
25 | class BaseLogger(ABC):
26 | def __init__(self):
27 | log_format = get_settings_variable(
28 | "LOG_FORMAT", default="%(asctime)s — %(name)s — %(levelname)s — %(message)s"
29 | )
30 | project_base_url = get_settings_variable("BASE_URL", default=".")
31 | self.formatter = logging.Formatter(log_format)
32 | self.logger_name = get_settings_variable("LOGGER_NAME", default="crax")
33 | self.log_file = get_settings_variable(
34 | "LOG_FILE", default=f"{project_base_url}/crax.log"
35 | )
36 | self.log_level = get_settings_variable("LOG_LEVEL", default="INFO")
37 | self.log_rotate_time = get_settings_variable(
38 | "LOG_ROTATE_TIME", default="midnight"
39 | )
40 | self.console = get_settings_variable("LOG_CONSOLE", default=False)
41 | self.streams = get_settings_variable(
42 | "LOG_STREAMS", default=[sys.stdout, sys.stderr]
43 | )
44 |
45 | enable_sentry = get_settings_variable("ENABLE_SENTRY", default=False)
46 | if enable_sentry is True: # pragma: no cover
47 | assert sentry_sdk is not None and LoggingIntegration is not None
48 | sentry_dsn = get_settings_variable("LOG_SENTRY_DSN")
49 | assert sentry_dsn is not None
50 | sentry_log_level = get_settings_variable(
51 | "SENTRY_LOG_LEVEL", default=self.log_level
52 | )
53 | sentry_event_level = get_settings_variable(
54 | "SENTRY_EVENT_LEVEL", default="ERROR"
55 | )
56 | sentry_logging = LoggingIntegration(
57 | level=getattr(logging, sentry_log_level),
58 | event_level=getattr(logging, sentry_event_level),
59 | )
60 | sentry_sdk.init(dsn=sentry_dsn, integrations=[sentry_logging])
61 |
62 | @abstractmethod
63 | def get_logger(self):
64 | raise NotImplementedError # pragma: no cover
65 |
66 |
67 | class CraxLogger(BaseLogger):
68 | def get_console_handler(self):
69 | assert isinstance(self.streams, list)
70 | console_handlers = []
71 | for stream in self.streams:
72 | console_handler = logging.StreamHandler(stream)
73 | console_handler.setFormatter(self.formatter)
74 | console_handlers.append(console_handler)
75 | return console_handlers
76 |
77 | def get_file_handler(self) -> TimedRotatingFileHandler:
78 | file_handler = TimedRotatingFileHandler(
79 | self.log_file, when=self.log_rotate_time
80 | )
81 | file_handler.setFormatter(self.formatter)
82 | return file_handler
83 |
84 | def get_logger(self) -> logging.Logger:
85 | logger = logging.getLogger(self.logger_name)
86 | logger.setLevel(getattr(logging, self.log_level))
87 | if self.console is True:
88 | handlers = self.get_console_handler()
89 | for handler in handlers:
90 | logger.addHandler(handler)
91 |
92 | logger.addHandler(self.get_file_handler())
93 | logger.propagate = False
94 | return logger
95 |
--------------------------------------------------------------------------------
/crax/auth/models.py:
--------------------------------------------------------------------------------
1 | """
2 | Common models for authentication backend
3 | """
4 | try:
5 | from sqlalchemy import (
6 | MetaData,
7 | Table,
8 | UniqueConstraint,
9 | CheckConstraint,
10 | Column,
11 | ForeignKey,
12 | Integer,
13 | String,
14 | Boolean,
15 | DateTime,
16 | )
17 | from crax.database.model import BaseTable
18 | except ImportError:
19 | raise ModuleNotFoundError("SQLAlchemy should be installed to use Crax Auth Models")
20 |
21 |
22 | class Group(BaseTable):
23 | table_name = "groups"
24 |
25 | name = Column(String(length=100), nullable=False)
26 |
27 |
28 | class User(BaseTable):
29 | table_name = "users"
30 |
31 | def __init__(self):
32 | self._pk = 0
33 | self._full_name = ""
34 | self._session = None
35 |
36 | @property
37 | def is_authenticated(self) -> bool:
38 | return True
39 |
40 | @property
41 | def pk(self) -> int:
42 | return self._pk
43 |
44 | @pk.setter
45 | def pk(self, val) -> None:
46 | self._pk = val
47 |
48 | @property
49 | def session(self) -> str:
50 | return self._session
51 |
52 | @session.setter
53 | def session(self, val) -> None:
54 | self._session = val
55 |
56 | @property
57 | def full_name(self) -> str:
58 | return self._full_name
59 |
60 | @full_name.setter
61 | def full_name(self, val) -> None:
62 | self._full_name = val
63 |
64 | def __str__(self) -> str:
65 | return self.full_name
66 |
67 | username = Column(String(length=50), nullable=False)
68 | password = Column(String(length=250), nullable=False)
69 | first_name = Column(String(length=50),)
70 | middle_name = Column(String(length=50), nullable=True)
71 | last_name = Column(String(length=50), nullable=True)
72 | phone = Column(String(length=20), nullable=True)
73 | email = Column(String(length=150), nullable=True)
74 | is_active = Column(Boolean(), nullable=True, default=True)
75 | is_staff = Column(Boolean(), nullable=True, default=False)
76 | is_superuser = Column(Boolean(), nullable=True, default=False)
77 | date_joined = Column(DateTime(), nullable=True)
78 | last_login = Column(DateTime(), nullable=True)
79 | unique = UniqueConstraint("username", name="username")
80 |
81 |
82 | class Permission(BaseTable):
83 | table_name = "permissions"
84 |
85 | name = Column(String(length=100), nullable=False)
86 | model = Column(String(length=50), nullable=False)
87 | can_read = Column(Boolean(), default=True)
88 | can_write = Column(Boolean(), default=False)
89 | can_create = Column(Boolean(), default=False)
90 | can_delete = Column(Boolean(), default=False)
91 |
92 |
93 | class UserGroup(BaseTable):
94 | table_name = "user_groups"
95 | user_id = Column(Integer, ForeignKey(User.id))
96 | group_id = Column(Integer, ForeignKey(Group.id))
97 |
98 |
99 | class UserPermission(BaseTable):
100 | table_name = "user_permissions"
101 | user_id = Column(Integer, ForeignKey(User.id))
102 | permission_id = Column(Integer, ForeignKey(Permission.id))
103 |
104 |
105 | class GroupPermission(BaseTable):
106 | table_name = "group_permissions"
107 | group_id = Column(Integer, ForeignKey(Group.id))
108 | permission_id = Column(Integer, ForeignKey(Permission.id))
109 |
110 |
111 | class AnonymousUser:
112 | @property
113 | def is_authenticated(self) -> bool:
114 | return False
115 |
116 | @property
117 | def pk(self) -> int:
118 | return 0
119 |
120 | @property
121 | def username(self) -> str:
122 | return ""
123 |
124 | @property
125 | def is_staff(self) -> bool:
126 | return False
127 |
128 | @property
129 | def is_superuser(self) -> bool:
130 | return False
131 |
132 | @property
133 | def is_active(self) -> bool:
134 | return False
135 |
136 | @property
137 | def session(self) -> None:
138 | return None
139 |
--------------------------------------------------------------------------------
/tests/test_files/command_four.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 | import os
4 | import shutil
5 |
6 | import pytest
7 | from databases import Database
8 |
9 | from tests.app_six.models import CustomerDiscount
10 | from tests.test_files.command_utils import Commands, replacement, cleaner
11 |
12 |
13 | @pytest.fixture
14 | def create_command(request):
15 | return Commands(request.param)
16 |
17 |
18 | @pytest.mark.asyncio
19 | @pytest.mark.parametrize(
20 | "create_command", [["app_four.conf", False, {"database": "custom"}]], indirect=True
21 | )
22 | async def test_migrations_multi(create_command):
23 | make_migrations = create_command.make_migrations()
24 | make_migrations()
25 | migrate = Commands(
26 | [
27 | "app_four.conf",
28 | False,
29 | {"database": "custom", "revision": "custom/app_four@head", "sql": True},
30 | ]
31 | ).migrate()
32 | migrate()
33 | assert os.path.exists("app_four/migrations/custom/sql")
34 | migrate = Commands(
35 | [
36 | "app_four.conf",
37 | False,
38 | {"database": "custom", "revision": "custom/app_four@head"},
39 | ]
40 | ).migrate()
41 | migrate()
42 | connection = create_command.default_connection
43 | if "mysql+pymysql" in connection:
44 | connection = connection.replace("mysql+pymysql", "mysql")
45 | database = Database(connection)
46 | query = (
47 | "INSERT INTO customer_b("
48 | "username, password, bio, first_name, age)"
49 | " VALUES (:username, :password, :bio, :first_name, :age)"
50 | )
51 | await database.connect()
52 | values = {
53 | "username": "chris",
54 | "password": "qwerty",
55 | "bio": "Nil!",
56 | "first_name": "Chris",
57 | "age": 27,
58 | }
59 | await database.execute(query=query, values=values)
60 | config = create_command.config
61 | versions = config.get_main_option("crax_latest_revisions")
62 | versions = json.loads(versions)
63 | migrate = Commands(
64 | [
65 | "app_four.conf",
66 | False,
67 | {
68 | "down": True,
69 | "revision": versions["custom/app_six"],
70 | "database": "custom",
71 | },
72 | ]
73 | ).migrate()
74 | migrate()
75 | try:
76 | await CustomerDiscount.query.insert(
77 | values={
78 | "name": "Customer Discount",
79 | "percent": 10,
80 | "start_date": datetime.datetime.now(),
81 | }
82 | )
83 | except Exception as e:
84 | assert "start_date" in str(e)
85 | replacement(
86 | "app_six/models.py",
87 | "start_date = sa.Column(sa.DateTime(), nullable=True)",
88 | "# start_date = sa.Column(sa.DateTime(), nullable=True)",
89 | )
90 | replacement(
91 | "app_four/models.py",
92 | "age = sa.Column(sa.Integer(), nullable=True)",
93 | "# age = sa.Column(sa.Integer(), nullable=True)",
94 | )
95 |
96 | if os.path.exists("app_one/migrations"):
97 | shutil.rmtree("app_one/migrations")
98 | if os.path.exists("app_two/migrations"):
99 | shutil.rmtree("app_two/migrations")
100 | if os.path.exists("app_three/migrations"):
101 | shutil.rmtree("app_three/migrations")
102 | if os.path.exists("app_four/migrations"):
103 | shutil.rmtree("app_four/migrations")
104 | if os.path.exists("app_five/migrations"):
105 | shutil.rmtree("app_five/migrations")
106 | if os.path.exists("app_six/migrations"):
107 | shutil.rmtree("app_six/migrations")
108 |
109 |
110 | @pytest.mark.asyncio
111 | @pytest.mark.parametrize("create_command", [["app_one.conf", True, {}]], indirect=True)
112 | async def test_double_migrations(create_command, capsys):
113 | with cleaner("alembic_env"):
114 | make_migrations = create_command.make_migrations()
115 | make_migrations()
116 | try:
117 | make_migrations = Commands(["app_one.conf", True, {}]).make_migrations()
118 | make_migrations()
119 | except:
120 | captured = capsys.readouterr()
121 | assert (
122 | "You have unapplied migrations. "
123 | "Please run migrate command first" in captured.err
124 | )
125 |
--------------------------------------------------------------------------------
/crax/database/env.py:
--------------------------------------------------------------------------------
1 | """
2 | Alembic environment file. This file will be placed into the Alembic working
3 | directory while creating initial migrations and alembic infrastructure
4 | """
5 | import configparser
6 | import json
7 | import os
8 | from itertools import chain
9 | from logging.config import fileConfig
10 |
11 | from alembic import context
12 | from sqlalchemy import engine_from_config
13 |
14 | from crax.database import get_metadata
15 |
16 |
17 | def run_migrations_online() -> None: # pragma: no cover
18 | # This file will never be called, but copy of this file will.
19 | # Migrations won't work without it and it is assumed that it is tested
20 | # if migrations work properly
21 | try:
22 | config = context.config
23 | fileConfig(config.config_file_name)
24 | except AttributeError:
25 | config = None
26 |
27 | if config:
28 | current_app = os.environ["CRAX_CURRENT"]
29 | current_db = os.environ["CRAX_DB_NAME"]
30 | target_metadata = get_metadata(os.environ["CRAX_CURRENT"])
31 |
32 | def filter_actual(*args):
33 | get_map = os.environ.get("CRAX_DB_TABLES", None)
34 | control_metadata = get_metadata(current_app)
35 | meta_tables = [x.name for x in control_metadata.sorted_tables]
36 | if get_map:
37 | db_map = json.loads(get_map)
38 | tables = list(
39 | chain(*[v for k, v in db_map.items() if k in current_app])
40 | )
41 | check = tables if len(tables) > len(meta_tables) else meta_tables
42 | res = args[0] in check
43 | else:
44 | res = args[0] in meta_tables
45 | return res
46 |
47 | def process_revision_directives(*args):
48 | script = args[2][0]
49 | if script.upgrade_ops.is_empty():
50 | args[2][:] = []
51 |
52 | config.set_main_option("sqlalchemy.url", os.environ["CRAX_DB_CONNECTION"])
53 | engine = engine_from_config(
54 | config.get_section(config.config_ini_section), prefix="sqlalchemy."
55 | )
56 | online = os.environ.get("CRAX_ONLINE", None)
57 | with engine.connect() as connection:
58 | if online:
59 | context.configure(
60 | render_as_batch=True,
61 | include_symbol=filter_actual,
62 | compare_type=True,
63 | process_revision_directives=process_revision_directives,
64 | connection=connection,
65 | target_metadata=target_metadata,
66 | )
67 | if context.get_context().opts["fn"].__name__ in [
68 | "upgrade",
69 | "downgrade",
70 | ]:
71 | try:
72 | end_version = context.get_revision_argument()
73 | conf_latest = config.get_main_option(
74 | "crax_latest_revisions", None
75 | )
76 | if conf_latest is not None:
77 | latest = json.loads(conf_latest)
78 | latest.update({f"{current_db}/{current_app}": end_version})
79 | latest = json.dumps(latest)
80 | else:
81 | latest = json.dumps(
82 | {f"{current_db}/{current_app}": end_version}
83 | )
84 | conf = configparser.ConfigParser()
85 | conf.read(config.config_file_name)
86 | conf["alembic"]["crax_latest_revisions"] = latest
87 | config.set_main_option("crax_latest_revisions", latest)
88 | with open(config.config_file_name, "w") as cf:
89 | conf.write(cf)
90 | except KeyError:
91 | pass
92 | context.run_migrations()
93 | else:
94 | path = os.environ.get("CRAX_SQL_PATH", None)
95 | _file = f"{context.get_revision_argument()}_.sql"
96 | if path and _file:
97 | if not os.path.exists(path):
98 | os.mkdir(path)
99 | if os.path.isfile(f"{path}/{_file}"):
100 | os.remove(f"{path}/{_file}")
101 | context.configure(
102 | connection=connection,
103 | transactional_ddl=False,
104 | output_buffer=open(f"{path}/{_file}", "a"),
105 | )
106 | context.run_migrations()
107 |
108 |
109 | run_migrations_online()
110 |
--------------------------------------------------------------------------------
/tests/test_app_common/routers.py:
--------------------------------------------------------------------------------
1 | import json
2 | import sys
3 |
4 | from crax.response_types import JSONResponse, TextResponse
5 | from crax.views import TemplateView, WsView
6 | from jinja2 import Environment, PackageLoader
7 |
8 |
9 | class Home(TemplateView):
10 | template = "index.html"
11 | methods = ["GET"]
12 |
13 |
14 | class GuestView:
15 | def __init__(self, request):
16 | self.request = request
17 |
18 | async def __call__(self, scope, receive, send):
19 | response = TextResponse(self.request, "Testing Custom View")
20 | await response(scope, receive, send)
21 |
22 |
23 | class EmptyView:
24 | def __init__(self, request):
25 | self.request = request
26 |
27 | async def __call__(self, scope, receive, send):
28 | response = TextResponse(self.request, None)
29 | await response(scope, receive, send)
30 |
31 |
32 | class BytesView:
33 | def __init__(self, request):
34 | self.request = request
35 |
36 | async def __call__(self, scope, receive, send):
37 | response = TextResponse(self.request, b"Testing bytes")
38 | await response(scope, receive, send)
39 |
40 |
41 | async def guest_view_coroutine(request, scope, receive, send):
42 | env = Environment(
43 | loader=PackageLoader("tests.test_app_common", "templates/"), autoescape=True
44 | )
45 | template = env.get_template("index.html")
46 | content = template.render()
47 | response = TextResponse(request, content)
48 | await response(scope, receive, send)
49 |
50 |
51 | class PostView:
52 | methods = ["GET", "POST"]
53 |
54 | def __init__(self, request):
55 | self.request = request
56 |
57 | async def __call__(self, scope, receive, send):
58 | if self.request.method == "POST":
59 | self.context = {"data": self.request.post["data"]}
60 | response = JSONResponse(self.request, self.context)
61 | else:
62 | response = TextResponse(self.request, "Text content")
63 | await response(scope, receive, send)
64 |
65 |
66 | class PostViewTemplateView(TemplateView):
67 | template = "index.html"
68 | methods = ["GET", "POST"]
69 |
70 | async def post(self):
71 | self.context = {"data": self.request.post["data"]}
72 | env = Environment(
73 | loader=PackageLoader("tests.test_app_common", "templates/"),
74 | autoescape=True,
75 | )
76 | template = env.get_template("index.html")
77 | content = template.render(self.context)
78 | response = TextResponse(self.request, content)
79 | return response
80 |
81 |
82 | class PostViewTemplateRender:
83 | methods = ["GET", "POST"]
84 |
85 | def __init__(self, request):
86 | self.request = request
87 |
88 | async def __call__(self, scope, receive, send):
89 | if self.request.method == "POST":
90 | if isinstance(self.request.post, str):
91 | data = json.loads(self.request.post)
92 | else:
93 | data = self.request.post
94 | self.context = {"data": data["data"], "files": self.request.files}
95 | env = Environment(
96 | loader=PackageLoader("tests.test_app_common", "templates/"),
97 | autoescape=True,
98 | )
99 | template = env.get_template("index.html")
100 | content = template.render(self.context)
101 | response = TextResponse(self.request, content)
102 | await response(scope, receive, send)
103 |
104 |
105 | async def guest_coroutine_view(request, scope, receive, send):
106 | env = Environment(
107 | loader=PackageLoader("tests.test_app_common", "templates/"), autoescape=True
108 | )
109 | template = env.get_template("index.html")
110 | content = template.render()
111 | response = TextResponse(request, content)
112 | await response(scope, receive, send)
113 |
114 |
115 | class ZeroDivision(TemplateView):
116 | template = "index.html"
117 | methods = ["GET"]
118 |
119 | async def get(self):
120 | result = 1 / 0
121 | return result
122 |
123 |
124 | class Handler500(TemplateView):
125 | template = "500.html"
126 |
127 | async def get(self):
128 | self.request.status_code = 500
129 |
130 |
131 | class Handler404:
132 | def __init__(self, request):
133 | self.request = request
134 |
135 | async def __call__(self, scope, receive, send):
136 | response = TextResponse(self.request, "Testing Not Found")
137 | response.status_code = 404
138 | await response(scope, receive, send)
139 |
140 |
141 | class Handler405:
142 | def __init__(self, request):
143 | self.request = request
144 |
145 | async def __call__(self, scope, receive, send):
146 | response = JSONResponse(self.request, {"Error": "Testing Method Not Allowed"})
147 | response.status_code = 405
148 | await response(scope, receive, send)
149 |
150 |
151 | class WsEcho(WsView):
152 | async def on_connect(self, scope, receive, send) -> None:
153 | sys.stdout.write("Accepted\n")
154 | await send({"type": "websocket.accept"})
155 |
156 | async def on_receive(self, scope, receive, send):
157 | await send({"type": "websocket.send", "text": self.kwargs["text"]})
158 |
--------------------------------------------------------------------------------
/crax/auth/middleware.py:
--------------------------------------------------------------------------------
1 | """
2 | Authentication backend middleware classes that might be activated in project settings
3 | """
4 | import binascii
5 | import json
6 | import time
7 | from base64 import b64decode
8 |
9 | import typing
10 |
11 | from crax.data_types import Request
12 | from itsdangerous import BadTimeSignature, SignatureExpired, BadSignature
13 |
14 | from crax.auth.models import User
15 | from crax.auth.authentication import (
16 | AnonymousUser,
17 | create_session_signer,
18 | create_session_cookie,
19 | )
20 | from crax.middleware.base import RequestMiddleware, ResponseMiddleware
21 |
22 |
23 | class AuthMiddleware(RequestMiddleware):
24 | def __init__(self, **kwargs: typing.Any) -> None:
25 | super(AuthMiddleware, self).__init__(**kwargs)
26 | self.signer, self.max_age, self.cookie_name, _ = create_session_signer()
27 |
28 | async def process_headers(self) -> Request:
29 | if self.cookie_name in self.request.cookies:
30 | session_cookie = self.request.cookies[self.cookie_name]
31 | try:
32 | session_cookie = b64decode(session_cookie)
33 | user = self.signer.unsign(session_cookie, max_age=self.max_age)
34 | user = user.decode("utf-8")
35 | user_id = user.split(":")[1]
36 | if user_id != "0":
37 | query = User.select().where(User.c.id == int(user_id))
38 | user = await User.query.fetch_one(query=query)
39 | if user:
40 | if user["last_name"] is not None:
41 | full_name = (
42 | f'{user["username"]}'
43 | f' {user["first_name"]} {user["last_name"]}'
44 | )
45 | else:
46 | full_name = f'{user["username"]} {user["first_name"]}'
47 | request_user = User
48 | request_user.pk = user["id"]
49 | request_user.username = user["username"]
50 | request_user.is_staff = bool(user["is_staff"])
51 | request_user.is_superuser = bool(user["is_superuser"])
52 | request_user.is_active = bool(user["is_active"])
53 | request_user.full_name = full_name
54 | request_user.session = self.request.cookies[self.cookie_name]
55 | self.request.user = User()
56 | self.request.user.session = self.request.cookies[
57 | self.cookie_name
58 | ]
59 | else: # pragma: no cover
60 | # Stupid case if user was removed from database but session cookies are sent
61 | self.request.user = AnonymousUser()
62 | else:
63 | self.request.user = AnonymousUser()
64 | except (binascii.Error, BadTimeSignature, BadSignature, SignatureExpired):
65 | self.request.user = AnonymousUser()
66 | else:
67 | self.request.user = AnonymousUser()
68 | return self.request
69 |
70 |
71 | class SessionMiddleware(ResponseMiddleware):
72 | def __init__(self, **kwargs: typing.Any):
73 | super(SessionMiddleware, self).__init__(**kwargs)
74 | self.signer, self.max_age, self.cookie_name, _ = create_session_signer()
75 |
76 | async def process_headers(self) -> None:
77 | response = await super(SessionMiddleware, self).process_headers()
78 | anonymous_cookie = create_session_cookie(str(int(time.time())), 0)[1]
79 | if self.request.session:
80 | session = json.loads(self.request.session)
81 | session_value = list(session.values())[0]
82 | try:
83 | spl = list(session)[0].split(":")
84 | if int(spl[1]) != 0:
85 | session_cookie = b64decode(session_value)
86 | self.signer.unsign(session_cookie, max_age=self.max_age)
87 | cookie = create_session_cookie(
88 | spl[0], spl[1], session=session_value
89 | )[1]
90 | else:
91 | cookie = anonymous_cookie
92 | self.request.session = {}
93 | except (
94 | binascii.Error,
95 | BadTimeSignature,
96 | BadSignature,
97 | SignatureExpired,
98 | ):
99 | cookie = anonymous_cookie
100 | self.headers.append((b"Set-Cookie", cookie.encode("latin-1")))
101 | else:
102 | if self.cookie_name in self.request.cookies:
103 | session_cookie = self.request.cookies[self.cookie_name]
104 | try:
105 | session_cookie = b64decode(session_cookie)
106 | self.signer.unsign(session_cookie, max_age=self.max_age)
107 | except (
108 | binascii.Error,
109 | BadTimeSignature,
110 | BadSignature,
111 | SignatureExpired,
112 | ): # pragma: no cover
113 | # No need to cover this case 'cause same cases covered above several times
114 | self.headers.append(
115 | (b"Set-Cookie", anonymous_cookie.encode("latin-1"))
116 | )
117 | else:
118 | self.headers.append(
119 | (b"Set-Cookie", anonymous_cookie.encode("latin-1"))
120 | )
121 | response.headers += self.headers
122 | return response
123 |
--------------------------------------------------------------------------------
/crax/urls.py:
--------------------------------------------------------------------------------
1 | """
2 | Route and Url objects that provides Crax url resolving system.
3 | """
4 | import re
5 | from typing import Optional
6 |
7 | import typing
8 |
9 | from crax.data_types import Request
10 | from crax.exceptions import CraxImproperlyConfigured, CraxPathNotFound
11 | from crax.views import DefaultError
12 |
13 |
14 | def include(module: str) -> Optional[list]:
15 | try:
16 | spl_module = module.split(".")
17 | urls = __import__(module, fromlist=spl_module)
18 | url_list = urls.url_list
19 | for route in url_list:
20 | for url in route.urls:
21 | if not url.namespace:
22 | url.namespace = urls.__package__
23 | return url_list
24 | except (ModuleNotFoundError, ImportError, AttributeError) as e:
25 | raise e.__class__(e) from e
26 |
27 |
28 | class Url:
29 | def __init__(self, path: str, **kwargs: typing.Any) -> None:
30 | self.path = path
31 | self.name = kwargs.pop("name", None)
32 | self.type_ = kwargs.pop("type", "path")
33 | self.scheme = kwargs.pop("scheme", ["http", "http.request"])
34 | self.masquerade = kwargs.pop("masquerade", False)
35 | self.namespace = kwargs.pop("namespace", "")
36 | self.tag = kwargs.get("tag")
37 | self.methods = kwargs.get("methods")
38 |
39 |
40 | class Route:
41 | def __init__(self, urls: typing.Any, handler: typing.Callable) -> None:
42 | self.handler = handler
43 | try:
44 | self.urls = tuple(urls)
45 | except TypeError:
46 | self.urls = (urls,)
47 |
48 | @staticmethod
49 | def create_path(url: Url, path: str) -> typing.Tuple[str, str]:
50 | if url.masquerade is False and not path.endswith("/"):
51 | path = path + "/"
52 | else:
53 | path = path
54 | if url.namespace:
55 | namespace = "/".join(url.namespace.split("."))
56 | final_path = f"/{namespace}{url.path}"
57 | else:
58 | final_path = url.path
59 | return final_path, path
60 |
61 | def check_len(self, url: Url, request_path: str) -> typing.Tuple[bool, dict]:
62 | find_path, path = self.create_path(url, request_path)
63 | matched = False
64 | params = {}
65 | if find_path == path:
66 | matched = True
67 | elif url.masquerade is True:
68 | split_req_path = [x for x in request_path.split("/") if x]
69 | split_path = [x for x in find_path.split("/") if x]
70 | if split_path == split_req_path[:-1]:
71 | matched = True
72 | else:
73 | matched = False
74 | else:
75 | split_req_path = [x for x in request_path.split("/") if x]
76 | split_path = [x for x in find_path.split("/") if x]
77 | intersection = set(split_req_path).intersection(split_path)
78 | if url.namespace:
79 | if len(intersection) == len(url.namespace.split(".")) + 1 and len(
80 | split_path
81 | ) == len(split_req_path):
82 | if all([x in split_req_path for x in split_path if "<" not in x]):
83 | matched = True
84 | else:
85 | if len(intersection) > 0 and len(split_path) == len(split_req_path):
86 | if all([x in split_req_path for x in split_path if "<" not in x]):
87 | matched = True
88 | if matched is True:
89 | pattern = re.compile("<([a-zA-Z0-9_:]+)>")
90 | names = [
91 | "".join(re.split(pattern, x)).split(":")[0]
92 | for x in split_path
93 | if re.match(pattern, x)
94 | ]
95 | values = [x for x in split_req_path if x not in intersection]
96 | params = dict(zip(names, values))
97 | return matched, params
98 |
99 | def get_match(self, request: Request) -> Optional[typing.Callable]:
100 | scheme = request.scheme
101 | handler = None
102 | matched = False
103 | params = {}
104 | for url in self.urls:
105 | if isinstance(url, Url):
106 | find_path, path = self.create_path(url, request.path)
107 | if url.type_ == "re_path":
108 | match = re.match(find_path, path)
109 | if match:
110 | params = match.groupdict()
111 | matched = True
112 | else:
113 | matched, params = self.check_len(url, path)
114 | if matched is True and scheme in url.scheme:
115 | handler = self.handler
116 | request.params = params
117 | if url.masquerade is True:
118 | if hasattr(handler, "scope"):
119 | masqueraded = [
120 | x
121 | for x in handler.scope
122 | if x == request.path.split("/")[-1]
123 | ]
124 | if not masqueraded:
125 | handler = DefaultError(
126 | request, CraxPathNotFound(request.path)
127 | )
128 | else:
129 | handler.template = masqueraded[0]
130 | else:
131 | handler = DefaultError(
132 | request, CraxPathNotFound(request.path)
133 | )
134 | else:
135 | handler = DefaultError(
136 | request,
137 | CraxImproperlyConfigured(f'{url} should be instance of "Url"'),
138 | )
139 | return handler
140 |
--------------------------------------------------------------------------------
/crax/commands/migrate.py:
--------------------------------------------------------------------------------
1 | """
2 | Command that applies migrations. Every time you
3 | changes are made, "makemigrations" command should ba ran
4 | and then created migrations have to be applied with this command.
5 | If no tables detected in target database (or some of tables are not present in database)
6 | it will be created. If no changes detected no error will be raised. All above id for
7 | "online mode". If for any reasons migrations should not be processed against target
8 | database, command should be ran in "offline mode" that just creates sql file.
9 | It is the common behaviour of alembic "upgrade" command. Command, launched with
10 | -d flag will work same way with alembic "downgrade".
11 | """
12 |
13 | import json
14 | import os
15 | import sys
16 |
17 | import typing
18 | from alembic.command import downgrade, upgrade
19 | from alembic.script import ScriptDirectory
20 | from alembic.util import CommandError
21 |
22 | from crax.database import sort_applications
23 | from crax.database.command import DataBaseCommands, OPTIONS
24 | from crax.exceptions import CraxMigrationsError
25 |
26 | OPTIONS += [
27 | (
28 | ["--sql", "-s"],
29 | {"help": "generate sql script", "action": "store_true", "dest": "sql"},
30 | ),
31 | (
32 | ["--revision", "-r"],
33 | {"type": str, "help": "specify revision you want to migrate"},
34 | ),
35 | ]
36 |
37 |
38 | class Migrate(DataBaseCommands):
39 | def __init__(
40 | self, opts: typing.List[typing.Union[tuple]], **kwargs: typing.Any
41 | ) -> None:
42 | super(Migrate, self).__init__(opts, **kwargs)
43 | dir_exists = os.path.exists(f"{self.project_url}/{self.alembic_dir}")
44 | if self.config is None or not dir_exists:
45 | raise CraxMigrationsError(
46 | "You can not run migrate command before migrations not created"
47 | )
48 |
49 | self.dependency_map = self.create_dependency_map()
50 | self.script = ScriptDirectory.from_config(self.config)
51 | self.kwargs = kwargs
52 |
53 | def run_migrations(self) -> None:
54 | sql = self.get_option("sql")
55 | down = self.get_option("down")
56 |
57 | sorted_applications = sort_applications(
58 | self.dependency_map, revers=self.args.down
59 | )
60 | for app in sorted_applications:
61 | if app == "crax.auth" or "migrations" in os.listdir(f"{self.project_url}/{app}"):
62 | os.environ["CRAX_CURRENT"] = app
63 | conf_latest = self.config.get_main_option("crax_latest_revisions", None)
64 | if conf_latest is None:
65 | try:
66 | rev = self.script.get_revision(f"{self.db_name}/{app}@head")
67 | crax_latest_revisions = json.dumps(
68 | {f"{self.db_name}/{app}": rev.revision}
69 | )
70 | self.config.set_main_option(
71 | "crax_latest_revisions", crax_latest_revisions
72 | )
73 | except CommandError:
74 | raise CraxMigrationsError(
75 | f"No such revision or branch {self.db_name}/{app}"
76 | )
77 | if sql:
78 | os.environ["CRAX_SQL_PATH"] = f"{self.create_version_dir(app)}/sql"
79 | kwargs = {"sql": True}
80 | if "CRAX_ONLINE" in os.environ:
81 | del os.environ["CRAX_ONLINE"]
82 | else:
83 | os.environ["CRAX_ONLINE"] = "true"
84 | kwargs = {}
85 | if self.check_branch_exists(f"{self.db_name}/{app}"):
86 | if down is True:
87 | downgrade(self.config, f"{self.db_name}/{app}@base", **kwargs)
88 | else:
89 | upgrade(self.config, f"{self.db_name}/{app}@head", **kwargs)
90 | self.create_db_map()
91 | else:
92 | sys.stdout.write(f"No migrations found in {app} application. Skipping.\n")
93 |
94 | def migrate(self) -> None:
95 | revision = self.get_option("revision")
96 | sql = self.get_option("sql")
97 | down = self.get_option("down")
98 | if revision:
99 | try:
100 | rev = self.script.get_revision(revision)
101 | os.environ["CRAX_CURRENT"] = list(rev.branch_labels)[0].split("/")[1]
102 | if sql:
103 | if "CRAX_ONLINE" in os.environ:
104 | del os.environ["CRAX_ONLINE"]
105 | if len(self.databases) > 1:
106 | sql_path = (
107 | f"{self.project_url}/{list(rev.branch_labels)[0].split('/')[1]}/"
108 | f"{self.migrations_dir}/{self.db_name}/sql"
109 | )
110 | else:
111 | sql_path = (
112 | f"{self.project_url}/{list(rev.branch_labels)[0].split('/')[1]}/"
113 | f"{self.migrations_dir}/sql"
114 | )
115 | os.environ["CRAX_SQL_PATH"] = sql_path
116 | if down is True:
117 | downgrade(self.config, revision, sql=True)
118 | else:
119 | upgrade(self.config, revision, sql=True)
120 | else:
121 | os.environ["CRAX_ONLINE"] = "true"
122 | if down is True:
123 | downgrade(self.config, revision)
124 | else:
125 | upgrade(self.config, revision)
126 | except AttributeError:
127 | raise CraxMigrationsError("Failed to get revision")
128 | else:
129 | self.run_migrations()
130 | self.write_config("alembic", "crax_migrated", "migrated")
131 |
132 |
133 | if __name__ == "__main__": # pragma: no cover
134 | migrate = Migrate(OPTIONS).migrate
135 | migrate()
136 |
--------------------------------------------------------------------------------
/crax/auth/authentication.py:
--------------------------------------------------------------------------------
1 | """
2 | Common functions for default auth backend
3 | """
4 | import datetime
5 | import hashlib
6 | import hmac
7 | import json
8 | from base64 import b64decode, b64encode
9 |
10 | import itsdangerous
11 | import typing
12 |
13 | from crax.auth.models import AnonymousUser, User
14 | from crax.data_types import Request
15 | from crax.exceptions import CraxImproperlyConfigured
16 | from crax.utils import get_settings_variable
17 |
18 |
19 | def create_password(password: str) -> str:
20 | secret = get_settings_variable("SECRET_KEY")
21 | if not secret:
22 | raise CraxImproperlyConfigured(
23 | '"SECRET_KEY" variable should be defined to use Authentication backends'
24 | )
25 | secret = secret.encode()
26 | hashed = hashlib.pbkdf2_hmac("sha256", password.encode(), secret, 100000)
27 | return hashed.hex()
28 |
29 |
30 | def check_password(hashed: str, password: str) -> bool:
31 | secret = get_settings_variable("SECRET_KEY")
32 | if not secret:
33 | raise CraxImproperlyConfigured(
34 | '"SECRET_KEY" variable should be defined to use Authentication backends'
35 | )
36 | secret = secret.encode()
37 | return hmac.compare_digest(
38 | bytearray.fromhex(hashed),
39 | hashlib.pbkdf2_hmac("sha256", password.encode(), secret, 100000),
40 | )
41 |
42 |
43 | def create_session_signer() -> tuple:
44 | secret_key = get_settings_variable("SECRET_KEY")
45 | if secret_key is None:
46 | raise CraxImproperlyConfigured(
47 | '"SECRET_KEY" string should be defined in settings to use Crax Sessions'
48 | )
49 | signer = itsdangerous.TimestampSigner(str(secret_key), algorithm=itsdangerous.signer.HMACAlgorithm())
50 | max_age = get_settings_variable("SESSION_EXPIRES", default=1209600)
51 | cookie_name = get_settings_variable("SESSION_COOKIE_NAME", default="session_id")
52 | same_site = get_settings_variable("SAME_SITE_COOKIE_MODE", default="lax")
53 | return signer, max_age, cookie_name, same_site
54 |
55 |
56 | def create_session_cookie(username: str, pk: int, session: str = None) -> tuple:
57 | signer, max_age, cookie_name, same_site = create_session_signer()
58 | if session is None:
59 | sign = signer.sign(f"{username}:{pk}")
60 | encoded = b64encode(sign)
61 | session = encoded.decode("utf-8")
62 | session_cookie = (
63 | f"{cookie_name}={session}; path=/;"
64 | f" Max-Age={max_age}; httponly; samesite={same_site}"
65 | )
66 | return session, session_cookie
67 |
68 |
69 | async def set_user(
70 | request: Request, username: str, password: str, user_pk: int = 0
71 | ) -> None:
72 | request.session = {}
73 | query = User.select().where(User.c.username == username)
74 | user = await User.query.fetch_one(query=query)
75 | if user:
76 | hashed = user["password"]
77 | pk = user["id"]
78 | res = check_password(hashed, password)
79 | if res is True:
80 | if user["last_name"] is not None:
81 | full_name = f'{username} {user["first_name"]} {user["last_name"]}'
82 | else:
83 | full_name = f'{username} {user["first_name"]}'
84 | request_user = User
85 | request_user.pk = pk
86 | request_user.username = username
87 | request_user.is_staff = bool(user["is_staff"])
88 | request_user.is_superuser = bool(user["is_superuser"])
89 | request_user.is_active = bool(user["is_active"])
90 | request_user.full_name = full_name
91 | session_cookie = create_session_cookie(username, pk)[0]
92 | signed = {f"{username}:{pk}": session_cookie}
93 | if user_pk == 0:
94 | request.session = json.dumps(signed)
95 | request_user.session = signed
96 | request.user = request_user()
97 | else:
98 | request.user = AnonymousUser()
99 | else:
100 | request.user = AnonymousUser()
101 |
102 |
103 | async def login(
104 | request: Request, username: str, password: str
105 | ) -> typing.Union[User, AnonymousUser]:
106 | secret = get_settings_variable("SECRET_KEY")
107 | signer = itsdangerous.TimestampSigner(str(secret))
108 | max_age = get_settings_variable("SESSION_EXPIRES", default=1209600)
109 | cookie_name = get_settings_variable("SESSION_COOKIE_NAME", default="session_id")
110 |
111 | if not secret:
112 | raise CraxImproperlyConfigured(
113 | '"SECRET_KEY" variable should be defined to use Authentication backends'
114 | )
115 | if hasattr(request, "cookies"):
116 | cookies = request.cookies
117 | if cookie_name in cookies:
118 | session_cookie = cookies[cookie_name]
119 | session_cookie = b64decode(session_cookie)
120 | user = signer.unsign(session_cookie, max_age=max_age)
121 | user = user.decode("utf-8")
122 | await set_user(request, username, password, user_pk=int(user.split(":")[1]))
123 | else:
124 | await set_user(request, username, password)
125 | return request.user
126 |
127 |
128 | async def logout(request: Request) -> None:
129 | if "cookie" in request.headers:
130 | del request.headers["cookie"]
131 | request.user = None
132 |
133 |
134 | async def create_user(username: str, password: str, **kwargs) -> None:
135 | password = create_password(password)
136 | values = {
137 | "username": username,
138 | "password": password,
139 | "first_name": kwargs.get("first_name"),
140 | "middle_name": kwargs.get("middle_name", ""),
141 | "last_name": kwargs.get("last_name", ""),
142 | "phone": kwargs.get("phone", ""),
143 | "email": kwargs.get("email", ""),
144 | "is_active": kwargs.get("is_active", True),
145 | "is_staff": kwargs.get("is_staff", False),
146 | "is_superuser": kwargs.get("is_superuser", False),
147 | "date_joined": datetime.datetime.now(),
148 | "last_login": datetime.datetime.now(),
149 | }
150 | await User.query.insert(query=User.table.insert(), values=values)
151 |
--------------------------------------------------------------------------------
/crax/middleware/cors.py:
--------------------------------------------------------------------------------
1 | """
2 | Cross Origin Request Middleware. Checks if it was preflight request
3 | and can be real request processed. In basic scheme only preflight request's
4 | headers will be modified. So we can get data that was sent from cross origin.
5 | However response will possibly can not be read. To make response modified as
6 | cors special cookie name should be defined in project settings.
7 | """
8 | import typing
9 |
10 | from crax.response_types import TextResponse
11 |
12 | from crax.middleware.base import ResponseMiddleware
13 | from crax.utils import get_settings_variable
14 |
15 |
16 | class CorsHeadersMiddleware(ResponseMiddleware):
17 | @staticmethod
18 | def check_allowed(param: typing.Any, check: set) -> typing.Optional[str]:
19 | if param != "*":
20 | if not isinstance(param, list):
21 | param = set([x.strip() for x in param.split(",")])
22 | if "*" in param:
23 | param = "*"
24 |
25 | if param == "*":
26 | return param
27 | else:
28 | if len(check.intersection(param)) == len(check):
29 | if isinstance(param, (set, list)):
30 | return ", ".join(param)
31 | else:
32 | return param
33 |
34 | async def process_headers(self) -> typing.Any:
35 | response = await super(CorsHeadersMiddleware, self).process_headers()
36 | cors_options = get_settings_variable("CORS_OPTIONS", default={})
37 | preflight = True
38 | error = None
39 | status_code = 200
40 | if self.request.method == "OPTIONS":
41 | if not isinstance(cors_options, dict):
42 | error = RuntimeError("Cors options should be a dict")
43 | status_code = 500
44 | response = TextResponse(
45 | self.request, str(error), status_code=status_code
46 | )
47 | return response
48 |
49 | origin = cors_options.get("origins", "*")
50 | method = cors_options.get("methods", "*")
51 | header = cors_options.get("headers", "*")
52 | expose_headers = cors_options.get("expose_headers", None)
53 | max_age = cors_options.get("max_age", "600")
54 |
55 | request_origin = self.request.headers.get("origin")
56 | request_method = self.request.headers.get("access-control-request-method")
57 | request_headers = self.request.headers.get("access-control-request-headers")
58 |
59 | if request_method is None:
60 | request_method = self.request.scope["method"]
61 |
62 | if self.request.method == "OPTIONS":
63 | if request_headers:
64 | request_headers = set(request_headers.split(","))
65 | else:
66 | request_headers = {"content-type"}
67 | request_method = {request_method}
68 | request_origin = {request_origin}
69 | if header != "*" and "cors_cookie" in cors_options:
70 | if "*" in header:
71 | pass # pragma: no cover
72 | else:
73 | header = [x for x in header]
74 | header.append(cors_options["cors_cookie"].lower())
75 |
76 | cors_headers = self.check_allowed(header, request_headers)
77 | if cors_headers is None:
78 | error = RuntimeError(
79 | f"Cross Origin Request with headers: "
80 | f'"{list(request_headers)[0]}" not allowed on this server'
81 | )
82 | status_code = 400
83 | cors_methods = self.check_allowed(method, request_method)
84 | if cors_methods is None:
85 | error = RuntimeError(
86 | f"Cross Origin Request with method: "
87 | f'"{list(request_method)[0]}" not allowed on this server'
88 | )
89 | status_code = 400
90 |
91 | cors_origins = self.check_allowed(origin, request_origin)
92 | if cors_origins is None:
93 | error = RuntimeError(
94 | f"Cross Origin Request from: "
95 | f'"{list(request_origin)[0]}" not allowed on this server'
96 | )
97 | status_code = 400
98 |
99 | elif (
100 | "cors_cookie" in cors_options
101 | and cors_options["cors_cookie"].lower() in self.request.headers
102 | ):
103 | preflight = False
104 | if not isinstance(origin, str):
105 | cors_origins = ", ".join(origin)
106 | else:
107 | cors_origins = origin
108 |
109 | if not isinstance(method, str):
110 | cors_methods = ", ".join(method)
111 | else:
112 | cors_methods = method
113 | if not isinstance(header, str):
114 | cors_headers = ", ".join(header)
115 | else:
116 | cors_headers = header
117 | else:
118 | return response
119 |
120 | if error is None:
121 | cors_headers = [
122 | (b"Access-Control-Allow-Origin", cors_origins.encode("latin-1")),
123 | (b"Access-Control-Allow-Methods", cors_methods.encode("latin-1")),
124 | (b"Access-Control-Allow-Headers", cors_headers.encode("latin-1")),
125 | (b"Access-Control-Max-Age", max_age.encode("latin-1")),
126 | (b"Vary", b"Origin"),
127 | ]
128 |
129 | if expose_headers is not None and preflight is True:
130 | if isinstance(expose_headers, str):
131 | expose_headers = [x.strip() for x in expose_headers.split(",")]
132 | assert type(expose_headers) == list
133 | cors_headers.append(
134 | (b"Access-Control-Expose-Headers", ", ".join(expose_headers).encode("latin-1"))
135 | )
136 |
137 | if preflight is False:
138 | if self.request.content_type is not None:
139 | cors_headers.append(
140 | (b"Content-Type", self.request.content_type.encode("latin-1"))
141 | )
142 | self.headers.append(cors_headers)
143 | response.headers.extend(*self.headers)
144 | else:
145 | response = TextResponse(self.request, str(error), status_code=status_code)
146 |
147 | return response
148 |
--------------------------------------------------------------------------------