├── 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 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ephmann/crax) 3 | [![Build Status](https://travis-ci.com/crax-framework/crax.svg?branch=master)](https://travis-ci.com/crax-framework/crax) 4 | [![codecov](https://codecov.io/gh/crax-framework/crax/branch/master/graph/badge.svg)](https://codecov.io/gh/crax-framework/crax) 5 | 6 | ### Python Asynchronous Web Development Switz Knife 7 | 8 | logo 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 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 |
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 | --------------------------------------------------------------------------------