├── .deepsource.toml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── docker ├── entrypoint.sh └── network-entrypoint.sh ├── hooks └── post_push ├── install_jet.sh ├── packages ├── __init__.py ├── jet_bridge │ ├── LICENSE │ ├── MANIFEST.in │ ├── README.md │ ├── jet_bridge │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── app.py │ │ ├── commands │ │ │ └── __init__.py │ │ ├── configuration.py │ │ ├── handlers │ │ │ ├── __init__.py │ │ │ ├── not_found.py │ │ │ ├── temporary_redirect.py │ │ │ └── view.py │ │ ├── media.py │ │ ├── router.py │ │ ├── settings.py │ │ ├── tasks │ │ │ ├── __init__.py │ │ │ └── release_inactive_graphql_schemas.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── async_exec.py │ │ │ ├── create_config.py │ │ │ └── settings.py │ ├── setup.py │ └── upload.sh ├── jet_bridge_base │ ├── LICENSE │ ├── MANIFEST.in │ ├── README.md │ ├── __init__.py │ ├── jet_bridge_base │ │ ├── __init__.py │ │ ├── automap.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ └── check_token.py │ │ ├── configuration.py │ │ ├── db.py │ │ ├── db_types │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── db.py │ │ │ ├── discover.py │ │ │ ├── metadata_file.py │ │ │ ├── mongo │ │ │ │ ├── __init__.py │ │ │ │ ├── common.py │ │ │ │ ├── mongo_base.py │ │ │ │ ├── mongo_column.py │ │ │ │ ├── mongo_db.py │ │ │ │ ├── mongo_declarative_meta.py │ │ │ │ ├── mongo_desc.py │ │ │ │ ├── mongo_engine.py │ │ │ │ ├── mongo_mapper.py │ │ │ │ ├── mongo_metadata.py │ │ │ │ ├── mongo_metadata_file.py │ │ │ │ ├── mongo_operator.py │ │ │ │ ├── mongo_queryset.py │ │ │ │ ├── mongo_record.py │ │ │ │ ├── mongo_reflect.py │ │ │ │ ├── mongo_session.py │ │ │ │ └── mongo_table.py │ │ │ ├── queryset.py │ │ │ ├── sql │ │ │ │ ├── __init__.py │ │ │ │ ├── common.py │ │ │ │ ├── sql_db.py │ │ │ │ ├── sql_metadata_file.py │ │ │ │ ├── sql_reflect.py │ │ │ │ ├── timezones.py │ │ │ │ └── type_codes.py │ │ │ └── timezones.py │ │ ├── encoders.py │ │ ├── exceptions │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── missing_argument_error.py │ │ │ ├── not_found.py │ │ │ ├── permission_denied.py │ │ │ ├── request_error.py │ │ │ ├── sql.py │ │ │ └── validation_error.py │ │ ├── external_auth │ │ │ ├── __init__.py │ │ │ ├── mixin.py │ │ │ ├── pipeline.py │ │ │ ├── storage.py │ │ │ ├── strategy.py │ │ │ └── utils.py │ │ ├── fields │ │ │ ├── __init__.py │ │ │ ├── array.py │ │ │ ├── binary.py │ │ │ ├── boolean.py │ │ │ ├── char.py │ │ │ ├── datetime.py │ │ │ ├── field.py │ │ │ ├── float.py │ │ │ ├── integer.py │ │ │ ├── json.py │ │ │ ├── raw.py │ │ │ ├── sql_params.py │ │ │ └── wkt.py │ │ ├── filters │ │ │ ├── __init__.py │ │ │ ├── binary_filter.py │ │ │ ├── boolean_filter.py │ │ │ ├── char_filter.py │ │ │ ├── datetime_filter.py │ │ │ ├── filter.py │ │ │ ├── filter_class.py │ │ │ ├── filter_for_dbfield.py │ │ │ ├── float_filter.py │ │ │ ├── integer_filter.py │ │ │ ├── lookups.py │ │ │ ├── model.py │ │ │ ├── model_aggregate.py │ │ │ ├── model_group.py │ │ │ ├── model_m2m.py │ │ │ ├── model_relation.py │ │ │ ├── model_search.py │ │ │ ├── model_segment.py │ │ │ ├── order_by.py │ │ │ └── wkt_filter.py │ │ ├── logger.py │ │ ├── media_cache.py │ │ ├── messages.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── column.py │ │ │ ├── data_types.py │ │ │ ├── model_relation_override.py │ │ │ └── table.py │ │ ├── paginators │ │ │ ├── __init__.py │ │ │ ├── page_number.py │ │ │ └── pagination.py │ │ ├── permissions.py │ │ ├── request.py │ │ ├── responses │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── json.py │ │ │ ├── not_found.py │ │ │ ├── optional_json.py │ │ │ ├── redirect.py │ │ │ └── template.py │ │ ├── router.py │ │ ├── sentry.py │ │ ├── serializers │ │ │ ├── __init__.py │ │ │ ├── message.py │ │ │ ├── model.py │ │ │ ├── model_description.py │ │ │ ├── model_group.py │ │ │ ├── model_serializer.py │ │ │ ├── proxy_request.py │ │ │ ├── relationship_override.py │ │ │ ├── reorder.py │ │ │ ├── reset_order.py │ │ │ ├── serializer.py │ │ │ ├── sql.py │ │ │ └── table.py │ │ ├── settings.py │ │ ├── ssh_tunnel.py │ │ ├── status.py │ │ ├── store.py │ │ ├── templates │ │ │ ├── 403.html │ │ │ ├── 404.html │ │ │ ├── 500.debug.html │ │ │ ├── 500.html │ │ │ └── external_auth_complete.html │ │ ├── utils │ │ │ ├── __init__.py │ │ │ ├── backend.py │ │ │ ├── base62.py │ │ │ ├── classes.py │ │ │ ├── common.py │ │ │ ├── compress.py │ │ │ ├── conf.py │ │ │ ├── crypt.py │ │ │ ├── datetime.py │ │ │ ├── db_types.py │ │ │ ├── exceptions.py │ │ │ ├── gql.py │ │ │ ├── graphql.py │ │ │ ├── http.py │ │ │ ├── process.py │ │ │ ├── relations.py │ │ │ ├── siblings.py │ │ │ ├── tables.py │ │ │ ├── text.py │ │ │ ├── timezones.py │ │ │ ├── token.py │ │ │ ├── track_database.py │ │ │ ├── track_model.py │ │ │ └── utc.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ ├── base │ │ │ ├── __init__.py │ │ │ ├── api.py │ │ │ └── generic_api.py │ │ │ ├── discover_connection.py │ │ │ ├── discover_table.py │ │ │ ├── external_auth │ │ │ ├── __init__.py │ │ │ ├── complete.py │ │ │ └── login.py │ │ │ ├── file_upload.py │ │ │ ├── graphql.py │ │ │ ├── image_resize.py │ │ │ ├── inspect_token.py │ │ │ ├── message.py │ │ │ ├── mixins │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ ├── destroy.py │ │ │ ├── list.py │ │ │ ├── model.py │ │ │ ├── retrieve.py │ │ │ └── update.py │ │ │ ├── model.py │ │ │ ├── model_description.py │ │ │ ├── model_description_relationship_override.py │ │ │ ├── proxy_request.py │ │ │ ├── register.py │ │ │ ├── reload.py │ │ │ ├── sql.py │ │ │ ├── status.py │ │ │ ├── table.py │ │ │ ├── table_column.py │ │ │ └── trigger_exception.py │ ├── setup.py │ └── upload.sh └── jet_django │ ├── LICENSE │ ├── MANIFEST.in │ ├── README.rst │ ├── jet_django │ ├── __init__.py │ ├── apps.py │ ├── configuration.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20181014_2002.py │ │ ├── 0003_auto_20191007_2005.py │ │ └── __init__.py │ ├── route_view.py │ ├── router.py │ ├── settings.py │ └── urls.py │ ├── manage.py │ ├── setup.py │ └── upload.sh ├── promotion.png ├── release-tasks.sh ├── requirements.txt ├── runtime.txt ├── scripts ├── set_tags.sh └── upload_all.sh ├── static ├── customize.jpg ├── customize.png ├── dashboard.jpeg ├── dashboard.png ├── export.jpeg ├── filters.jpeg ├── filters.png ├── kanban.jpeg ├── kanban.png ├── list.jpeg ├── list.png ├── overview.gif ├── promotion.png ├── promotion2.png ├── segment.jpeg ├── segment.png ├── users.jpeg └── users.png └── update_jet.sh /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = [] 4 | 5 | exclude_patterns = [] 6 | 7 | [[ analyzers ]] 8 | name = 'python' 9 | enabled = true 10 | 11 | [[ analyzers ]] 12 | name = 'docker' 13 | enabled = true 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.DS_Store 3 | *.egg* 4 | *.db 5 | *.sqlite3 6 | 7 | /.idea 8 | /packages/*/dist/ 9 | /packages/*/build/ 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM jetadmin/jet-bridge-base:1.3.2 2 | 3 | COPY packages /packages 4 | RUN pip install -e /packages/jet_bridge_base 5 | RUN pip install -e /packages/jet_bridge 6 | 7 | RUN mkdir /jet 8 | VOLUME /jet 9 | WORKDIR /jet 10 | 11 | #USER jet 12 | 13 | COPY docker/entrypoint.sh / 14 | COPY docker/network-entrypoint.sh / 15 | RUN chmod +x /entrypoint.sh 16 | RUN chmod +x /network-entrypoint.sh 17 | ENTRYPOINT ["/entrypoint.sh"] 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: jet_bridge --address=0.0.0.0 --port=$PORT --project=$PROJECT --token=$TOKEN --database_engine=$DATABASE_ENGINE --database_host=$DATABASE_HOST --database_port=$DATABASE_PORT --database_user=$DATABASE_USER --database_password=$DATABASE_PASSWORD --database_name=$DATABASE_NAME --database_extra=$DATABASE_EXTRA 2 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jet Bridge", 3 | "description": "Universal bridge for Jet Admin – Admin panel framework for your application", 4 | "repository": "https://github.com/jet-admin/jet-bridge", 5 | "website": "https://jetadmin.io/", 6 | "logo": "https://app.jetadmin.io/assets/images/logo-tile.svg", 7 | "keywords": [ 8 | "admin", 9 | "interface", 10 | "backoffice", 11 | "responsive", 12 | "developers", 13 | "sql" 14 | ], 15 | "success_url": "/api/register/", 16 | "scripts": { 17 | "postdeploy": "./release-tasks.sh" 18 | }, 19 | "buildpacks": [ 20 | { 21 | "url": "heroku/python" 22 | } 23 | ], 24 | "env": { 25 | "PROJECT": { 26 | "description": "Unique project name from app.jetadmin.io" 27 | }, 28 | "TOKEN": { 29 | "description": "Jet Bridge token from app.jetadmin.io" 30 | }, 31 | "DATABASE_ENGINE": { 32 | "description": "Database engine, one of: postgresql, mysql, oracle, mssql+pyodbc, bigquery, snowflake, cockroachdb, awsathena+rest, clickhouse+native, databricks, sqlite" 33 | }, 34 | "DATABASE_URL": { 35 | "description": "database url", 36 | "required": false 37 | }, 38 | "DATABASE_HOST": { 39 | "description": "database host" 40 | }, 41 | "DATABASE_PORT": { 42 | "description": "database port" 43 | }, 44 | "DATABASE_NAME": { 45 | "description": "database name or path (SQLite)" 46 | }, 47 | "DATABASE_USER": { 48 | "description": "database user" 49 | }, 50 | "DATABASE_PASSWORD": { 51 | "description": "database password" 52 | }, 53 | "DATABASE_EXTRA": { 54 | "description": "database extra", 55 | "required": false 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | jet_bridge --environment_type=docker --media_root=/jet/jet_media --use_default_config=project,token,address,config 4 | -------------------------------------------------------------------------------- /docker/network-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if ping -q -c 1 -W 1 host.docker.internal &> /dev/null; then 4 | printf host.docker.internal 5 | else 6 | printf "$(netstat -nr | grep '^0\.0\.0\.0' | awk '{print $2}')" 7 | fi 8 | -------------------------------------------------------------------------------- /hooks/post_push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ $DOCKER_TAG == 'latest' ]]; then 4 | REF='master' 5 | else 6 | REF=$DOCKER_TAG 7 | fi 8 | 9 | if [[ $REF == 'dev' || $REF == 'stage' || $REF == 'master' ]]; then 10 | curl -X POST \ 11 | --fail \ 12 | -F token=$BUILD_TRIGGER_TOKEN \ 13 | -F ref=$REF \ 14 | -F "variables[component]=jet_bridge, jet_cloud" \ 15 | -F "variables[jet_bridge]=true" \ 16 | -F "variables[jet_cloud]=true" \ 17 | https://gitlab.com/api/v4/projects/26239898/trigger/pipeline 18 | fi -------------------------------------------------------------------------------- /packages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/jet_bridge/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | recursive-include jet_bridge * 4 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '1.12.0' 2 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | import sys 4 | 5 | from tornado.httpserver import HTTPServer 6 | from tornado.ioloop import IOLoop, PeriodicCallback 7 | 8 | from jet_bridge_base import configuration 9 | from jet_bridge.configuration import JetBridgeConfiguration 10 | 11 | conf = JetBridgeConfiguration() 12 | configuration.set_configuration(conf) 13 | 14 | from jet_bridge_base.commands.check_token import check_token_command 15 | from jet_bridge_base.db import connect_database_from_settings 16 | from jet_bridge_base.logger import logger 17 | 18 | from jet_bridge import settings, VERSION 19 | from jet_bridge.settings import missing_options, required_options_without_default 20 | from jet_bridge.tasks.release_inactive_graphql_schemas import run_release_inactive_graphql_schemas_task 21 | 22 | 23 | def main(): 24 | args = sys.argv[1:] 25 | 26 | if 'ARGS' in os.environ: 27 | args = os.environ['ARGS'].split(' ') 28 | 29 | logger.info(datetime.now().strftime('%B %d, %Y - %H:%M:%S %Z')) 30 | logger.info('Jet Bridge version {}'.format(VERSION)) 31 | 32 | if (len(args) >= 1 and args[0] == 'config') or missing_options == required_options_without_default: 33 | from jet_bridge.utils.create_config import create_config 34 | create_config(missing_options == required_options_without_default) 35 | return 36 | elif missing_options and len(missing_options) < len(required_options_without_default): 37 | logger.info('Required options are not specified: {}'.format(', '.join(missing_options))) 38 | return 39 | 40 | ssl = settings.SSL_CERT or settings.SSL_KEY 41 | 42 | address = 'localhost' if settings.ADDRESS == '0.0.0.0' else settings.ADDRESS 43 | protocol = 'https' if ssl else 'http' 44 | url = '{}://{}:{}/'.format(protocol, address, settings.PORT) 45 | api_url = '{}api/'.format(url) 46 | 47 | if len(args) >= 1: 48 | if args[0] == 'check_token': 49 | check_token_command(api_url) 50 | return 51 | 52 | connect_database_from_settings() 53 | 54 | from jet_bridge.app import make_app 55 | 56 | app = make_app() 57 | workers = settings.WORKERS if not settings.DEBUG else 1 58 | 59 | ssl_options = { 60 | 'certfile': settings.SSL_CERT, 61 | 'keyfile': settings.SSL_KEY 62 | } if ssl else None 63 | server = HTTPServer(app, ssl_options=ssl_options, idle_connection_timeout=75) 64 | server.bind(settings.PORT, settings.ADDRESS) 65 | server.start(workers) 66 | 67 | if settings.WORKERS > 1 and settings.DEBUG: 68 | logger.warning('Multiple workers are not supported in DEBUG mode') 69 | 70 | logger.info('Starting server at {} (WORKERS:{}, THREADS:{})'.format(url, workers, settings.THREADS)) 71 | 72 | if settings.RELEASE_INACTIVE_GRAPHQL_SCHEMAS_TIMEOUT: 73 | logger.info('RELEASE_INACTIVE_GRAPHQL_SCHEMAS_TIMEOUT task is enabled (TIMEOUT={}s)'.format( 74 | settings.RELEASE_INACTIVE_GRAPHQL_SCHEMAS_TIMEOUT 75 | )) 76 | 77 | release_inactive_graphql_schemas_task = PeriodicCallback( 78 | lambda: run_release_inactive_graphql_schemas_task(), 79 | 60 * 1000 # every minute 80 | ) 81 | release_inactive_graphql_schemas_task.start() 82 | 83 | if settings.DEBUG: 84 | logger.warning('Server is running in DEBUG mode') 85 | 86 | logger.info('Quit the server with CONTROL-C') 87 | 88 | check_token_command(api_url) 89 | 90 | IOLoop.current().start() 91 | 92 | if __name__ == '__main__': 93 | main() 94 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge/jet_bridge/commands/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge/jet_bridge/handlers/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/handlers/not_found.py: -------------------------------------------------------------------------------- 1 | import tornado.web 2 | 3 | 4 | class NotFoundHandler(tornado.web.RequestHandler): 5 | 6 | def get(self, *args, **kwargs): 7 | self.set_status(404) 8 | self.render('404.html', **{ 9 | 'path': self.request.path, 10 | }) 11 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/handlers/temporary_redirect.py: -------------------------------------------------------------------------------- 1 | from tornado.web import RedirectHandler 2 | 3 | 4 | class TemporaryRedirectHandler(RedirectHandler): 5 | 6 | def initialize(self, url): 7 | super(TemporaryRedirectHandler, self).initialize(url, False) 8 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/media.py: -------------------------------------------------------------------------------- 1 | MEDIA_STORAGE_FILE = 'file' 2 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/router.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from tornado import gen 3 | 4 | from jet_bridge.utils.async_exec import as_future 5 | 6 | 7 | class Router(object): 8 | routes = [ 9 | { 10 | 'path': '', 11 | 'method_mapping': { 12 | 'get': 'list', 13 | 'post': 'create' 14 | }, 15 | 'detail': False 16 | }, 17 | { 18 | 'path': '(?P[^/]+)/', 19 | 'method_mapping': { 20 | 'get': 'retrieve', 21 | 'put': 'update', 22 | 'patch': 'partial_update', 23 | 'delete': 'destroy' 24 | }, 25 | 'detail': True 26 | } 27 | ] 28 | urls = [] 29 | 30 | def add_handler(self, view, url, actions): 31 | class ActionHandler(view): 32 | pass 33 | 34 | for method, method_action in actions.items(): 35 | def create_action_method(action): 36 | @gen.coroutine 37 | def action_method(inner_self, *args, **kwargs): 38 | request = inner_self.get_request() 39 | request.action = action 40 | 41 | def execute(): 42 | inner_self.before_dispatch(request) 43 | 44 | try: 45 | result = inner_self.view.dispatch(action, request, *args, **kwargs) 46 | finally: 47 | inner_self.after_dispatch(request) 48 | 49 | return result 50 | 51 | try: 52 | response = yield as_future(execute) 53 | yield inner_self.write_response(response) 54 | except Exception: 55 | exc_type, exc, traceback = sys.exc_info() 56 | response = inner_self.view.error_response(request, exc_type, exc, traceback) 57 | yield inner_self.write_response(response) 58 | finally: 59 | raise gen.Return() 60 | 61 | return action_method 62 | 63 | func = create_action_method(method_action) 64 | setattr(ActionHandler, method, func) 65 | 66 | self.urls.append((url, ActionHandler)) 67 | 68 | def add_route_actions(self, view, route, prefix): 69 | viewset = view.view 70 | actions = route['method_mapping'] 71 | actions = dict(filter(lambda x: hasattr(viewset, x[1]), actions.items())) 72 | 73 | if len(actions) == 0: 74 | return 75 | 76 | url = '{}{}'.format(prefix, route['path']) 77 | self.add_handler(view, url, actions) 78 | 79 | def add_route_extra_actions(self, view, route, prefix): 80 | viewset = view.view 81 | for attr in dir(viewset): 82 | method = getattr(viewset, attr) 83 | bind_to_methods = getattr(method, 'bind_to_methods', None) 84 | 85 | if bind_to_methods is None: 86 | continue 87 | 88 | detail = getattr(method, 'detail', None) 89 | 90 | if detail != route['detail']: 91 | continue 92 | 93 | extra_actions = dict(map(lambda x: (x, attr), bind_to_methods)) 94 | 95 | url = '{}{}{}/'.format(prefix, route['path'], attr) 96 | self.add_handler(view, url, extra_actions) 97 | 98 | def register(self, prefix, view): 99 | for route in self.routes: 100 | self.add_route_extra_actions(view, route, prefix) 101 | 102 | for route in self.routes: 103 | self.add_route_actions(view, route, prefix) 104 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/tasks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge/jet_bridge/tasks/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/tasks/release_inactive_graphql_schemas.py: -------------------------------------------------------------------------------- 1 | import gc 2 | 3 | from jet_bridge_base.db import release_inactive_graphql_schemas 4 | 5 | 6 | def run_release_inactive_graphql_schemas_task(): 7 | release_inactive_graphql_schemas() 8 | gc.collect() 9 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge/jet_bridge/utils/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/utils/async_exec.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | from concurrent.futures import Executor, ThreadPoolExecutor 3 | 4 | from tornado.concurrent import Future, chain_future 5 | from tornado.ioloop import IOLoop 6 | 7 | 8 | class _AsyncExecution: 9 | """Tiny wrapper around ThreadPoolExecutor. This class is not meant to be 10 | instantiated externally, but internally we just use it as a wrapper around 11 | ThreadPoolExecutor so we can control the pool size and make the 12 | `as_future` function public. 13 | """ 14 | 15 | def __init__(self, max_workers=None): 16 | self._max_workers = ( 17 | max_workers or multiprocessing.cpu_count() 18 | ) # type: int 19 | self._pool = None # type: Optional[Executor] 20 | 21 | def init_pool(self): 22 | self._pool = ThreadPoolExecutor(max_workers=self._max_workers) 23 | 24 | def set_max_workers(self, count): 25 | if self._pool: 26 | self._pool.shutdown(wait=True) 27 | 28 | self._max_workers = count 29 | self.init_pool() 30 | 31 | def as_future(self, query): 32 | # concurrent.futures.Future is not compatible with the "new style" 33 | # asyncio Future, and awaiting on such "old-style" futures does not 34 | # work. 35 | # 36 | # tornado includes a `run_in_executor` function to help with this 37 | # problem, but it's only included in version 5+. Hence, we copy a 38 | # little bit of code here to handle this incompatibility. 39 | 40 | if not self._pool: 41 | self.init_pool() 42 | 43 | old_future = self._pool.submit(query) 44 | new_future = Future() # type: Future 45 | 46 | IOLoop.current().add_future( 47 | old_future, lambda f: chain_future(f, new_future) 48 | ) 49 | 50 | return new_future 51 | 52 | def submit(self, query, *args, **kwargs): 53 | if not self._pool: 54 | self.init_pool() 55 | 56 | self._pool.submit(query, *args, **kwargs) 57 | 58 | 59 | _async_exec = _AsyncExecution() 60 | 61 | as_future = _async_exec.as_future 62 | 63 | set_max_workers = _async_exec.set_max_workers 64 | 65 | pool_submit = _async_exec.submit 66 | -------------------------------------------------------------------------------- /packages/jet_bridge/jet_bridge/utils/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from six.moves import configparser 3 | 4 | from tornado.options import Error 5 | 6 | 7 | def parse_environment(self, final=True): 8 | for name in os.environ: 9 | normalized = self._normalize_name(name) 10 | normalized = normalized.lower() 11 | if normalized in self._options: 12 | option = self._options[normalized] 13 | if option.multiple: 14 | if not isinstance(os.environ[name], (list, str)): 15 | raise Error("Option %r is required to be a list of %s " 16 | "or a comma-separated string" % 17 | (option.name, option.type.__name__)) 18 | 19 | if type(os.environ[name]) == str and option.type != str: 20 | option.parse(os.environ[name]) 21 | else: 22 | option.set(os.environ[name]) 23 | 24 | 25 | def parse_config_file(self, path, section, final=True): 26 | try: 27 | config_parser = configparser.ConfigParser(interpolation=None) 28 | except TypeError: 29 | config_parser = configparser.ConfigParser() 30 | 31 | if not config_parser.read(path): 32 | raise IOError('Config file at path "{}" not found'.format(path)) 33 | 34 | try: 35 | config = config_parser.items(section) 36 | except KeyError: 37 | raise ValueError('Config file does not have [{}] section]'.format(section)) 38 | 39 | for (name, value) in config: 40 | normalized = self._normalize_name(name) 41 | normalized = normalized.lower() 42 | if normalized in self._options: 43 | option = self._options[normalized] 44 | if option.multiple: 45 | if not isinstance(value, (list, str)): 46 | raise Error("Option %r is required to be a list of %s " 47 | "or a comma-separated string" % 48 | (option.name, option.type.__name__)) 49 | 50 | if type(value) == str and option.type != str: 51 | option.parse(value) 52 | else: 53 | option.set(value) 54 | -------------------------------------------------------------------------------- /packages/jet_bridge/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(fname): 6 | path = os.path.join(os.path.dirname(__file__), fname) 7 | try: 8 | file = open(path, encoding='utf-8') 9 | except TypeError: 10 | file = open(path) 11 | return file.read() 12 | 13 | 14 | def get_install_requires(): 15 | install_requires = [ 16 | 'tornado==5.1.1', 17 | 'six', 18 | 'jet-bridge-base==1.12.0', 19 | 'paramiko==2.8.1', 20 | 'sshtunnel', 21 | ] 22 | 23 | return install_requires 24 | 25 | setup( 26 | name='jet-bridge', 27 | version=__import__('jet_bridge').VERSION, 28 | description='', 29 | long_description=read('README.md'), 30 | long_description_content_type='text/markdown', 31 | author='Denis Kildishev', 32 | author_email='support@jetadmin.io', 33 | url='https://github.com/jet-admin/jet-bridge', 34 | packages=find_packages(), 35 | license='MIT', 36 | classifiers=[ 37 | 38 | ], 39 | zip_safe=False, 40 | include_package_data=True, 41 | install_requires=get_install_requires(), 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'jet_bridge = jet_bridge.__main__:main', 45 | ], 46 | }, 47 | ) 48 | -------------------------------------------------------------------------------- /packages/jet_bridge/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | 4 | rm -rf dist/* 5 | python setup.py sdist bdist_wheel 6 | twine check dist/* 7 | twine upload dist/* 8 | rm -rf build/* 9 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2018 Google, Inc. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | recursive-include jet_bridge_base * 4 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/__init__.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import settings 2 | 3 | VERSION = '1.12.0' 4 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/automap.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext import automap 2 | from sqlalchemy.ext.automap import automap_base 3 | 4 | 5 | def _is_many_to_many(automap_base, table): 6 | return None, None, None 7 | 8 | 9 | # Monkey patch to make M2M reflect as a normal Entity class 10 | setattr(automap, '_is_many_to_many', _is_many_to_many) 11 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/commands/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/commands/check_token.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | 3 | from requests import RequestException 4 | 5 | from jet_bridge_base import settings 6 | from jet_bridge_base.utils.backend import is_resource_token_activated 7 | from jet_bridge_base.logger import logger 8 | 9 | 10 | def check_token_command(api_url): 11 | try: 12 | if not is_resource_token_activated(settings.PROJECT, settings.TOKEN): 13 | logger.warning('[!] Your resource token is not activated') 14 | logger.warning('[!] Project: {}'.format(settings.PROJECT)) 15 | logger.warning('[!] Token: {}'.format(settings.TOKEN)) 16 | 17 | if settings.DATABASE_ENGINE != 'none' and settings.AUTO_OPEN_REGISTER and api_url.startswith('http'): 18 | register_url = '{}register/'.format(api_url) 19 | 20 | if settings.TOKEN: 21 | register_url += '?token={}'.format(settings.TOKEN) 22 | 23 | if webbrowser.open(register_url): 24 | logger.warning('[!] Activation page was opened in your browser - {}'.format(register_url)) 25 | else: 26 | register_url = '{}register/'.format(api_url) 27 | logger.warning('[!] Go to {} to activate it'.format(register_url)) 28 | except RequestException: 29 | logger.error('[!] Can\'t connect to Jet Admin API') 30 | logger.error('[!] Token verification failed') 31 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/configuration.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from jet_bridge_base.settings import set_settings 4 | 5 | 6 | class Configuration(object): 7 | 8 | def __init__(self): 9 | self.init_time = time.time() 10 | 11 | def get_type(self): 12 | pass 13 | 14 | def get_version(self): 15 | pass 16 | 17 | def get_model_description(self, db_table): 18 | pass 19 | 20 | def get_hidden_model_description(self): 21 | return [] 22 | 23 | def get_settings(self): 24 | pass 25 | 26 | def on_model_pre_create(self, model, pk): 27 | pass 28 | 29 | def on_model_post_create(self, model, instance): 30 | pass 31 | 32 | def on_model_pre_update(self, model, instance): 33 | pass 34 | 35 | def on_model_post_update(self, model, instance): 36 | pass 37 | 38 | def on_model_pre_delete(self, model, instance): 39 | pass 40 | 41 | def on_model_post_delete(self, model, instance): 42 | pass 43 | 44 | def media_get_available_name(self, path): 45 | pass 46 | 47 | def media_exists(self, path): 48 | pass 49 | 50 | def media_listdir(self, path): 51 | pass 52 | 53 | def media_get_modified_time(self, path): 54 | pass 55 | 56 | def media_open(self, path, mode='rb'): 57 | pass 58 | 59 | def media_save(self, path, content): 60 | pass 61 | 62 | def media_delete(self, path): 63 | pass 64 | 65 | def media_url(self, path, request): 66 | pass 67 | 68 | def session_set(self, request, name, value, secure=True): 69 | pass 70 | 71 | def session_get(self, request, name, default=None, decode=True, secure=True): 72 | pass 73 | 74 | def session_clear(self, request, name): 75 | pass 76 | 77 | def clean_sso_application_name(self, name): 78 | return name.lower().replace('-', '') 79 | 80 | def clean_sso_applications(self, applications): 81 | return dict(map(lambda x: (self.clean_sso_application_name(x[0]), x[1]), applications.items())) 82 | 83 | def run_async(self, func, *args, **kwargs): 84 | func(*args, **kwargs) 85 | 86 | 87 | configuration = Configuration() 88 | 89 | 90 | def set_configuration(new_configuration): 91 | global configuration 92 | configuration = new_configuration 93 | set_settings(configuration.get_settings()) 94 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import inspect_uniform, aliased_uniform, get_session_engine 2 | from .db import init_database_connection, load_mapped_base 3 | from .discover import discover_connection, discover_tables 4 | from .metadata_file import dump_metadata_file, remove_metadata_file 5 | from .queryset import desc_uniform, empty_filter, get_queryset_order_by, get_queryset_limit, apply_default_ordering, \ 6 | queryset_count_optimized, queryset_aggregate, queryset_group, get_sql_aggregate_func_by_name, \ 7 | get_sql_group_func_lookup, queryset_search 8 | from .timezones import fetch_default_timezone, apply_session_timezone 9 | from .mongo import MongoDeclarativeMeta, MongoColumn, MongoDesc, MongoQueryset, MongoSession 10 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/common.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import DeclarativeMeta, aliased 2 | 3 | from .mongo import MongoDeclarativeMeta, MongoSession, mongo_inspect 4 | from .sql import sql_inspect, sql_get_session_engine 5 | 6 | 7 | def inspect_uniform(cls): 8 | if isinstance(cls, DeclarativeMeta): 9 | return sql_inspect(cls) 10 | elif isinstance(cls, MongoDeclarativeMeta): 11 | return mongo_inspect(cls) 12 | 13 | 14 | def aliased_uniform(cls): 15 | if isinstance(cls, DeclarativeMeta): 16 | return aliased(cls) 17 | elif isinstance(cls, MongoDeclarativeMeta): 18 | return cls 19 | 20 | 21 | def get_session_engine(session): 22 | if isinstance(session, MongoSession): 23 | return 'mongo' 24 | else: 25 | return sql_get_session_engine(session) 26 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/db.py: -------------------------------------------------------------------------------- 1 | from .mongo import mongodb_init_database_connection, MongoBase, mongo_load_mapped_base 2 | from .sql import sql_init_database_connection, sql_load_mapped_base 3 | 4 | 5 | def init_database_connection(conf, tunnel, id_short, connection_name, schema, pending_connection): 6 | if conf.get('engine') == 'mongo': 7 | return mongodb_init_database_connection( 8 | conf, 9 | tunnel, 10 | id_short, 11 | connection_name, 12 | schema, 13 | pending_connection 14 | ) 15 | else: 16 | return sql_init_database_connection( 17 | conf, 18 | tunnel, 19 | id_short, 20 | connection_name, 21 | schema, 22 | pending_connection 23 | ) 24 | 25 | 26 | def load_mapped_base(MappedBase, clear=False): 27 | if isinstance(MappedBase, MongoBase): 28 | mongo_load_mapped_base(MappedBase, clear) 29 | else: 30 | sql_load_mapped_base(MappedBase, clear) 31 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/discover.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | from sqlalchemy import MetaData, inspection 3 | from sqlalchemy.orm import scoped_session, sessionmaker 4 | from sqlalchemy.sql.base import _bind_or_error 5 | 6 | from jet_bridge_base.db_types.sql import sql_create_connection_engine, sql_get_tables 7 | from jet_bridge_base.utils.conf import get_connection_schema 8 | 9 | 10 | def discover_connection(conf, tunnel): 11 | bind = None 12 | client = None 13 | 14 | try: 15 | if conf.get('engine') == 'mongo': 16 | database_url = conf.get('url') 17 | database_name = conf.get('name') 18 | 19 | client = MongoClient(database_url) 20 | 21 | if database_name not in client.list_database_names(): 22 | raise Exception('No such database found or database is empty: {}'.format(database_name)) 23 | 24 | db = client[conf.get('name')] 25 | db.list_collection_names() 26 | 27 | return True 28 | else: 29 | bind = sql_create_connection_engine(conf, tunnel) 30 | 31 | Session = scoped_session(sessionmaker(bind=bind)) 32 | session = Session() 33 | 34 | with session.connection(): 35 | return True 36 | except Exception as e: 37 | raise e 38 | finally: 39 | if bind: 40 | bind.dispose() 41 | 42 | if client: 43 | client.close() 44 | 45 | if tunnel: 46 | tunnel.close() 47 | 48 | 49 | def discover_tables(conf, tunnel): 50 | bind = None 51 | client = None 52 | 53 | try: 54 | if conf.get('engine') == 'mongo': 55 | database_url = conf.get('url') 56 | database_name = conf.get('name') 57 | 58 | client = MongoClient(database_url) 59 | 60 | if database_name not in client.list_database_names(): 61 | raise Exception('No such database found or database is empty: {}'.format(database_name)) 62 | 63 | db = client[conf.get('name')] 64 | 65 | return db.list_collection_names() 66 | else: 67 | schema = get_connection_schema(conf) 68 | bind = sql_create_connection_engine(conf, tunnel) 69 | 70 | Session = scoped_session(sessionmaker(bind=bind)) 71 | session = Session() 72 | 73 | with session.connection() as connection: 74 | metadata = MetaData(schema=schema, bind=connection) 75 | 76 | if bind is None: 77 | bind = _bind_or_error(metadata) 78 | 79 | with inspection.inspect(bind)._inspection_context() as insp: 80 | if schema is None: 81 | schema = metadata.schema 82 | 83 | load, view_names = sql_get_tables(insp, metadata, bind, schema, foreign=True, views=True) 84 | return load 85 | except Exception as e: 86 | raise e 87 | finally: 88 | if bind: 89 | bind.dispose() 90 | 91 | if client: 92 | client.close() 93 | 94 | if tunnel: 95 | tunnel.close() 96 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/metadata_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jet_bridge_base import settings 4 | from jet_bridge_base.logger import logger 5 | from jet_bridge_base.utils.conf import get_connection_id, get_connection_schema, get_connection_name, \ 6 | get_metadata_file_path 7 | 8 | from .mongo import mongo_dump_metadata_file, MongoMetadata 9 | from .sql import sql_dump_metadata_file 10 | 11 | 12 | def dump_metadata_file(conf, metadata): 13 | if isinstance(metadata, MongoMetadata): 14 | mongo_dump_metadata_file(conf, metadata) 15 | else: 16 | sql_dump_metadata_file(conf, metadata) 17 | 18 | 19 | def remove_metadata_file(conf): 20 | if not settings.CACHE_METADATA: 21 | return 22 | 23 | connection_id = get_connection_id(conf) 24 | schema = get_connection_schema(conf) 25 | connection_name = get_connection_name(conf, schema) 26 | id_short = connection_id[:4] 27 | 28 | file_path = get_metadata_file_path(conf) 29 | 30 | if not os.path.exists(file_path): 31 | logger.info('[{}] Schema cache clear skipped (file not found) for "{}"'.format(id_short, connection_name)) 32 | return 33 | 34 | try: 35 | os.unlink(file_path) 36 | logger.info('[{}] Schema cache cleared for "{}"'.format(id_short, connection_name)) 37 | 38 | return file_path 39 | except Exception as e: 40 | logger.error('[{}] Schema cache clear failed for "{}"'.format(id_short, connection_name), exc_info=e) 41 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import mongo_inspect 2 | from .mongo_base import MongoBase, MongoBaseClasses 3 | from .mongo_column import MongoColumn 4 | from .mongo_declarative_meta import MongoDeclarativeMeta 5 | from .mongo_desc import MongoDesc 6 | from .mongo_mapper import MongoMapper 7 | from .mongo_metadata import MongoMetadata 8 | from .mongo_metadata_file import mongo_dump_metadata_file, mongo_load_metadata_file 9 | from .mongo_operator import MongoOperator 10 | from .mongo_queryset import MongoQueryset 11 | from .mongo_record import MongoRecordMeta, MongoRecord 12 | from .mongo_session import MongoSession 13 | from .mongo_table import MongoTable 14 | from .mongo_reflect import reflect_mongodb 15 | from .mongo_db import mongodb_init_database_connection, mongo_load_mapped_base 16 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/common.py: -------------------------------------------------------------------------------- 1 | def mongo_inspect(cls): 2 | return getattr(cls, '_mapper') 3 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_base.py: -------------------------------------------------------------------------------- 1 | from .mongo_declarative_meta import MongoDeclarativeMeta 2 | 3 | 4 | class MongoBaseClasses(dict): 5 | def __init__(self, tables, *args, **kwargs): 6 | super(MongoBaseClasses, self).__init__(*args, **kwargs) 7 | 8 | for table in tables: 9 | self[table.name] = MongoDeclarativeMeta(table) 10 | 11 | def __iter__(self): 12 | for value in self.values(): 13 | yield value 14 | 15 | 16 | class MongoBase(object): 17 | def __init__(self, metadata): 18 | self.metadata = metadata 19 | self.classes = MongoBaseClasses(metadata.tables) 20 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_column.py: -------------------------------------------------------------------------------- 1 | from .mongo_operator import MongoOperator 2 | 3 | 4 | class MongoColumn(object): 5 | def __init__(self, table, name, type, nullable=True, mixed_types=None, autoincrement=False, default=None, 6 | server_default=None, foreign_keys=None, comment=None, params=None): 7 | self.table = table 8 | self.name = name 9 | self.key = name 10 | self.type = type 11 | self.nullable = nullable 12 | self.mixed_types = mixed_types 13 | self.autoincrement = autoincrement 14 | self.default = default 15 | self.server_default = server_default 16 | self.foreign_keys = foreign_keys or list() 17 | self.comment = comment 18 | self.params = params or dict() 19 | 20 | @staticmethod 21 | def deserialize(table, obj): 22 | name = obj.pop('name') 23 | type = obj.pop('type') 24 | return MongoColumn(table, name, type, **obj) 25 | 26 | def serialize(self): 27 | return { 28 | 'name': self.name, 29 | 'type': self.type, 30 | 'nullable': self.nullable, 31 | 'mixed_types': self.mixed_types, 32 | 'autoincrement': self.autoincrement, 33 | 'default': self.default, 34 | 'server_default': self.server_default, 35 | 'foreign_keys': self.foreign_keys, 36 | 'comment': self.comment, 37 | 'params': self.params 38 | } 39 | 40 | def __eq__(self, other): 41 | return MongoOperator('__eq__', self, other) 42 | 43 | def __gt__(self, other): 44 | return MongoOperator('__gt__', self, other) 45 | 46 | def __ge__(self, other): 47 | return MongoOperator('__ge__', self, other) 48 | 49 | def __lt__(self, other): 50 | return MongoOperator('__lt__', self, other) 51 | 52 | def __le__(self, other): 53 | return MongoOperator('__le__', self, other) 54 | 55 | def isnot(self, other): 56 | return MongoOperator('not', MongoOperator('__eq__', self, other)) 57 | 58 | def ilike(self, value): 59 | return MongoOperator('ilike', self, value) 60 | 61 | def exists(self, value): 62 | return MongoOperator('exists', self, value) 63 | 64 | def in_(self, value): 65 | return MongoOperator('in', self, value) 66 | 67 | def json_icontains(self, value): 68 | return MongoOperator('json_icontains', self, value) 69 | 70 | def __repr__(self): 71 | return '{}.{}'.format(self.table.name, self.name) 72 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_db.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from jet_bridge_base import settings 4 | from jet_bridge_base.logger import logger 5 | from jet_bridge_base.utils.conf import get_connection_only_predicate 6 | from jet_bridge_base.utils.process import get_memory_usage, get_memory_usage_human 7 | 8 | from .mongo_session import MongoSession 9 | from .mongo_metadata_file import mongo_load_metadata_file, mongo_dump_metadata_file 10 | from .mongo_reflect import reflect_mongodb 11 | from .mongo_base import MongoBase 12 | from .mongo_engine import MongoEngine 13 | 14 | 15 | def mongodb_init_database_connection(conf, tunnel, id_short, connection_name, schema, pending_connection): 16 | engine = MongoEngine() 17 | pending_connection['engine'] = engine 18 | 19 | logger.info('[{}] Connecting to database "{}"...'.format(id_short, connection_name)) 20 | 21 | connect_start = time.time() 22 | 23 | database_url = conf.get('url') 24 | database_name = conf.get('name') 25 | 26 | engine.connect(database_url) 27 | db = engine.get_db(database_name) 28 | Session = lambda: MongoSession(db) 29 | 30 | connect_end = time.time() 31 | connect_time = round(connect_end - connect_start, 3) 32 | 33 | default_timezone = None 34 | # default_timezone = fetch_default_timezone(session) 35 | # if default_timezone is not None: 36 | # logger.info('[{}] Default timezone detected: "{}"'.format(id_short, default_timezone)) 37 | # else: 38 | # logger.info('[{}] Failed to detect default timezone'.format(id_short)) 39 | 40 | metadata_dump = mongo_load_metadata_file(conf) 41 | 42 | if metadata_dump: 43 | metadata = metadata_dump['metadata'] 44 | 45 | reflect_time = None 46 | reflect_memory_usage_approx = None 47 | 48 | logger.info('[{}] Loaded schema cache for "{}"'.format(id_short, connection_name)) 49 | else: 50 | logger.info('[{}] Getting schema for "{}"...'.format(id_short, connection_name)) 51 | 52 | reflect_start_time = time.time() 53 | reflect_start_memory_usage = get_memory_usage() 54 | 55 | only = get_connection_only_predicate(conf) 56 | metadata = reflect_mongodb( 57 | id_short, 58 | db, 59 | only=only, 60 | pending_connection=pending_connection, 61 | max_read_records=settings.DATABASE_REFLECT_MAX_RECORDS or 1000000 62 | ) 63 | 64 | reflect_end_time = time.time() 65 | reflect_end_memory_usage = get_memory_usage() 66 | reflect_time = round(reflect_end_time - reflect_start_time, 3) 67 | reflect_memory_usage_approx = reflect_end_memory_usage - reflect_start_memory_usage 68 | 69 | mongo_dump_metadata_file(conf, metadata) 70 | 71 | logger.info('[{}] Connected to "{}" (Mem:{})'.format(id_short, connection_name, get_memory_usage_human())) 72 | 73 | MappedBase = MongoBase(metadata) 74 | 75 | result = { 76 | 'engine': engine, 77 | 'Session': Session, 78 | 'MappedBase': MappedBase, 79 | 'default_timezone': default_timezone, 80 | 'connect_time': connect_time, 81 | 'reflect_time': reflect_time, 82 | 'reflect_memory_usage_approx': reflect_memory_usage_approx, 83 | 'reflect_metadata_dump': metadata_dump['file_path'] if metadata_dump else None 84 | } 85 | 86 | return result 87 | 88 | 89 | def mongo_load_mapped_base(MappedBase, clear=False): 90 | raise NotImplementedError 91 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_declarative_meta.py: -------------------------------------------------------------------------------- 1 | from .mongo_mapper import MongoMapper 2 | from .mongo_record import MongoRecord 3 | 4 | 5 | class MongoDeclarativeMeta(object): 6 | def __init__(self, table): 7 | self._mapper = MongoMapper(table) 8 | 9 | for column in table.columns: 10 | setattr(self, column.name, column) 11 | 12 | def get_name(self): 13 | return self._mapper.selectable.name 14 | 15 | def __call__(self, **kwargs): 16 | table_name = self._mapper.selectable.name 17 | return MongoRecord(table_name, **kwargs) 18 | 19 | def __repr__(self): 20 | return self.get_name() 21 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_desc.py: -------------------------------------------------------------------------------- 1 | class MongoDesc(object): 2 | def __init__(self, column): 3 | self.column = column 4 | 5 | def __repr__(self): 6 | return 'desc({})'.format(repr(self.column)) 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_engine.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | 3 | 4 | class MongoEngine(object): 5 | client = None 6 | 7 | def connect(self, url): 8 | self.url = url 9 | self.client = MongoClient(url) 10 | 11 | def get_db(self, name): 12 | return self.client[name] 13 | 14 | def dispose(self): 15 | if self.client: 16 | self.client.close() 17 | self.client = None 18 | 19 | def __repr__(self): 20 | return self.url 21 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_mapper.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.utils.common import CollectionDict 2 | 3 | 4 | class MongoMapper(object): 5 | def __init__(self, table): 6 | self.tables = [table] 7 | self.relationships = CollectionDict() 8 | self.selectable = table 9 | self.primary_key = [table.columns['_id']] 10 | self.columns = table.columns 11 | 12 | def __repr__(self): 13 | return self.selectable.name 14 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_metadata.py: -------------------------------------------------------------------------------- 1 | from .mongo_table import MongoTable 2 | 3 | 4 | class MongoMetadata(object): 5 | def __init__(self, tables=None, schema=None): 6 | self.tables = list(map(lambda x: MongoTable.deserialize(x), tables)) if tables else list() 7 | self.schema = schema 8 | 9 | def append_table(self, table): 10 | self.tables.append(table) 11 | 12 | @staticmethod 13 | def deserialize(obj): 14 | return MongoMetadata(**obj) 15 | 16 | def serialize(self): 17 | return { 18 | 'tables': list(map(lambda x: x.serialize(), self.tables)), 19 | 'schema': self.schema 20 | } 21 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_metadata_file.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from jet_bridge_base import settings 5 | from jet_bridge_base.logger import logger 6 | from jet_bridge_base.utils.conf import get_connection_id, get_connection_schema, get_connection_name, get_metadata_file_path 7 | 8 | from .mongo_metadata import MongoMetadata 9 | 10 | 11 | def mongo_dump_metadata_file(conf, metadata): 12 | if not settings.CACHE_METADATA: 13 | return 14 | 15 | connection_id = get_connection_id(conf) 16 | schema = get_connection_schema(conf) 17 | connection_name = get_connection_name(conf, schema) 18 | id_short = connection_id[:4] 19 | 20 | file_path = get_metadata_file_path(conf) 21 | 22 | try: 23 | dir_path = os.path.dirname(file_path) 24 | 25 | if dir_path and not os.path.exists(dir_path): 26 | os.makedirs(dir_path) 27 | 28 | with open(file_path, 'w') as file: 29 | json.dump(metadata.serialize(), file, cls=MetadataJSONEncoder) 30 | 31 | logger.info('[{}] Saved schema cache for "{}"'.format(id_short, connection_name)) 32 | 33 | return file_path 34 | except Exception as e: 35 | logger.error('[{}] Failed dumping schema cache for "{}"'.format(id_short, connection_name), exc_info=e) 36 | 37 | 38 | class MetadataJSONEncoder(json.JSONEncoder): 39 | def default(self, obj): 40 | if isinstance(obj, set): 41 | return list(obj) 42 | return super(MetadataJSONEncoder, self).default(obj) 43 | 44 | 45 | def mongo_load_metadata_file(conf): 46 | if not settings.CACHE_METADATA: 47 | return 48 | 49 | connection_id = get_connection_id(conf) 50 | schema = get_connection_schema(conf) 51 | connection_name = get_connection_name(conf, schema) 52 | id_short = connection_id[:4] 53 | 54 | file_path = get_metadata_file_path(conf) 55 | 56 | if not os.path.exists(file_path): 57 | logger.info('[{}] Schema cache not found for "{}"'.format(id_short, connection_name)) 58 | return 59 | 60 | try: 61 | with open(file_path, 'r') as file: 62 | data = json.load(file) 63 | metadata = MongoMetadata.deserialize(data) 64 | 65 | return { 66 | 'file_path': file_path, 67 | 'metadata': metadata 68 | } 69 | except Exception as e: 70 | logger.error('[{}] Failed loading schema cache for "{}"'.format(id_short, connection_name), exc_info=e) 71 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_operator.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class MongoOperator(object): 5 | def __init__(self, operator, lhs=None, rhs=None): 6 | self.operator = operator 7 | self.lhs = lhs 8 | self.rhs = rhs 9 | 10 | def __invert__(self): 11 | return MongoOperator('not', self) 12 | 13 | def __repr__(self): 14 | return json.dumps({ 15 | 'operator': self.operator, 16 | 'lhs': repr(self.lhs), 17 | 'rhs': repr(self.rhs), 18 | }) 19 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_record.py: -------------------------------------------------------------------------------- 1 | class MongoRecordMeta(object): 2 | def __init__(self, table_name): 3 | self.table_name = table_name 4 | 5 | 6 | class MongoRecord(object): 7 | create_pending = False 8 | delete_pending = False 9 | 10 | def __init__(self, table_name, **kwargs): 11 | object.__setattr__(self, 'meta', MongoRecordMeta(table_name)) 12 | object.__setattr__(self, 'data', dict(kwargs)) 13 | object.__setattr__(self, 'update_pending', set()) 14 | 15 | def __getattr__(self, name): 16 | return self.get(name) 17 | 18 | def get(self, name, default=None): 19 | return self.data.get(name, default) 20 | 21 | def __setattr__(self, name, value): 22 | self.set(name, value) 23 | 24 | def set(self, name, value): 25 | self.data[name] = value 26 | self.mark_update(name) 27 | 28 | def get_data(self): 29 | return object.__getattribute__(self, 'data') 30 | 31 | def get_meta(self): 32 | return object.__getattribute__(self, 'meta') 33 | 34 | def is_create_pending(self): 35 | return object.__getattribute__(self, 'create_pending') 36 | 37 | def mark_create(self): 38 | object.__setattr__(self, 'create_pending', True) 39 | 40 | def get_update_pending(self): 41 | return object.__getattribute__(self, 'update_pending') 42 | 43 | def mark_update(self, name): 44 | object.__getattribute__(self, 'update_pending').add(name) 45 | 46 | def is_delete_pending(self): 47 | return object.__getattribute__(self, 'delete_pending') 48 | 49 | def mark_delete(self): 50 | object.__setattr__(self, 'delete_pending', True) 51 | 52 | def clear_pending(self): 53 | object.__setattr__(self, 'create_pending', False) 54 | object.__getattribute__(self, 'update_pending').clear() 55 | object.__setattr__(self, 'delete_pending', False) 56 | 57 | def __repr__(self): 58 | return '_id({})'.format(self.get('_id')) 59 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_session.py: -------------------------------------------------------------------------------- 1 | from .mongo_queryset import MongoQueryset 2 | from .mongo_column import MongoColumn 3 | from .mongo_declarative_meta import MongoDeclarativeMeta 4 | 5 | 6 | class MongoSession(object): 7 | info = {} 8 | records = [] 9 | 10 | def __init__(self, db): 11 | self.db = db 12 | 13 | def commit(self): 14 | for record in self.records: 15 | name = record.get_meta().table_name 16 | update_pending = record.get_update_pending() 17 | 18 | if record.is_delete_pending() and not record.is_create_pending(): 19 | self.db[name].delete_one({'_id': record._id}) 20 | elif record.is_create_pending(): 21 | record_data = record.get_data() 22 | data = {} 23 | 24 | for key, value in record_data.items(): 25 | if key == '_id': 26 | continue 27 | data[key] = value 28 | 29 | self.db[name].insert_one(data) 30 | elif len(update_pending): 31 | record_data = record.get_data() 32 | data = {} 33 | 34 | for key, value in record_data.items(): 35 | if key not in update_pending: 36 | continue 37 | data[key] = value 38 | 39 | self.db[name].update_one({'_id': record._id}, {'$set': data}) 40 | 41 | record.clear_pending() 42 | 43 | def rollback(self): 44 | pass 45 | 46 | def close(self): 47 | pass 48 | 49 | def query(self, *args): 50 | if isinstance(args[0], MongoDeclarativeMeta): 51 | name = args[0].get_name() 52 | select = None 53 | elif len(args) > 0 and all(map(lambda x: isinstance(x, MongoColumn), args)): 54 | name = args[0].table.name 55 | select = args 56 | else: 57 | raise Exception('Unsupported query args={}'.format(args)) 58 | 59 | return MongoQueryset(self, name, select=select) 60 | 61 | def add(self, record): 62 | self.bind_record(record) 63 | record.mark_create() 64 | 65 | def delete(self, record): 66 | record.mark_delete() 67 | 68 | def bind_record(self, record): 69 | self.records.append(record) 70 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/mongo/mongo_table.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.utils.common import CollectionDict 2 | 3 | from .mongo_column import MongoColumn 4 | 5 | 6 | class MongoTable(object): 7 | def __init__(self, name, columns=None, comment=None, schema=None): 8 | self.name = name 9 | self.columns = CollectionDict(map(lambda x: ( 10 | x['name'], 11 | MongoColumn.deserialize(self, x) 12 | ), columns)) if columns else CollectionDict() 13 | self.comment = comment 14 | self.schema = schema 15 | 16 | def append_column(self, column): 17 | self.columns[column.name] = column 18 | 19 | @staticmethod 20 | def deserialize(obj): 21 | name = obj.pop('name') 22 | return MongoTable(name, **obj) 23 | 24 | def serialize(self): 25 | return { 26 | 'name': self.name, 27 | 'columns': list(map(lambda x: x.serialize(), self.columns.values())), 28 | 'comment': self.comment, 29 | 'schema': self.schema 30 | } 31 | 32 | def __repr__(self): 33 | return self.name 34 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/sql/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import sql_inspect, sql_get_session_engine 2 | from .sql_reflect import sql_get_tables, sql_reflect 3 | from .sql_db import sql_init_database_connection, sql_build_engine_url, sql_create_connection_engine, sql_load_mapped_base, sql_load_database_table 4 | from .sql_metadata_file import sql_dump_metadata_file, sql_load_metadata_file 5 | from .timezones import sql_fetch_default_timezone 6 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/sql/common.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import inspect 2 | 3 | 4 | def sql_inspect(cls): 5 | return inspect(cls) 6 | 7 | 8 | def sql_get_session_engine(session): 9 | return session.bind.engine.name 10 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/sql/sql_metadata_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | 4 | from jet_bridge_base import settings 5 | from jet_bridge_base.logger import logger 6 | from jet_bridge_base.utils.conf import get_connection_id, get_connection_schema, get_connection_name, get_metadata_file_path 7 | 8 | 9 | def sql_dump_metadata_file(conf, metadata): 10 | if not settings.CACHE_METADATA: 11 | return 12 | 13 | connection_id = get_connection_id(conf) 14 | schema = get_connection_schema(conf) 15 | connection_name = get_connection_name(conf, schema) 16 | id_short = connection_id[:4] 17 | 18 | file_path = get_metadata_file_path(conf) 19 | 20 | try: 21 | dir_path = os.path.dirname(file_path) 22 | 23 | if dir_path and not os.path.exists(dir_path): 24 | os.makedirs(dir_path) 25 | 26 | with open(file_path, 'wb') as file: 27 | pickle.dump(metadata, file) 28 | 29 | logger.info('[{}] Saved schema cache for "{}"'.format(id_short, connection_name)) 30 | 31 | return file_path 32 | except Exception as e: 33 | logger.error('[{}] Failed dumping schema cache for "{}"'.format(id_short, connection_name), exc_info=e) 34 | 35 | 36 | def sql_load_metadata_file(conf, connection): 37 | if not settings.CACHE_METADATA: 38 | return 39 | 40 | connection_id = get_connection_id(conf) 41 | schema = get_connection_schema(conf) 42 | connection_name = get_connection_name(conf, schema) 43 | id_short = connection_id[:4] 44 | 45 | file_path = get_metadata_file_path(conf) 46 | 47 | if not os.path.exists(file_path): 48 | logger.info('[{}] Schema cache not found for "{}"'.format(id_short, connection_name)) 49 | return 50 | 51 | try: 52 | with open(file_path, 'rb') as file: 53 | metadata = pickle.load(file=file) 54 | 55 | metadata.bind = connection 56 | 57 | return { 58 | 'file_path': file_path, 59 | 'metadata': metadata 60 | } 61 | except Exception as e: 62 | logger.error('[{}] Failed loading schema cache for "{}"'.format(id_short, connection_name), exc_info=e) 63 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/sql/timezones.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import SQLAlchemyError 2 | from dateutil.tz import gettz 3 | 4 | from jet_bridge_base.logger import logger 5 | from jet_bridge_base.utils.conf import get_connection_id 6 | from jet_bridge_base.utils.timezones import FixedOffsetTimezone 7 | 8 | from .common import sql_get_session_engine 9 | 10 | 11 | def sql_fetch_postgresql_default_timezone(session): 12 | try: 13 | cursor = session.execute('SELECT NOW()') 14 | row = cursor.fetchone() 15 | session.commit() 16 | 17 | return row[0].tzinfo 18 | except SQLAlchemyError: 19 | session.rollback() 20 | 21 | 22 | def sql_fetch_mysql_default_timezone(session): 23 | try: 24 | cursor = session.execute('SELECT TIMEDIFF(NOW(), UTC_TIMESTAMP)') 25 | row = cursor.fetchone() 26 | session.commit() 27 | 28 | return FixedOffsetTimezone(row[0]) 29 | except SQLAlchemyError: 30 | session.rollback() 31 | 32 | 33 | def sql_fetch_mssql_default_timezone(session): 34 | try: 35 | cursor = session.execute('SELECT sysdatetimeoffset() as now') 36 | row = cursor.fetchone() 37 | session.commit() 38 | 39 | return row[0].tzinfo 40 | except SQLAlchemyError: 41 | session.rollback() 42 | 43 | 44 | def sql_fetch_default_timezone(conf, session): 45 | database_timezone = conf.get('timezone') 46 | if database_timezone: 47 | try: 48 | return gettz(database_timezone) 49 | except Exception as e: 50 | connection_id = get_connection_id(conf) 51 | id_short = connection_id[:4] 52 | logger.info('[{}] Failed to parse database timezone "{}": {}'.format(id_short, database_timezone, str(e))) 53 | 54 | try: 55 | if sql_get_session_engine(session) == 'postgresql': 56 | return sql_fetch_postgresql_default_timezone(session) 57 | elif sql_get_session_engine(session) == 'mssql': 58 | return sql_fetch_mssql_default_timezone(session) 59 | elif sql_get_session_engine(session) == 'mysql': 60 | return sql_fetch_mysql_default_timezone(session) 61 | except: 62 | pass 63 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/sql/type_codes.py: -------------------------------------------------------------------------------- 1 | import re 2 | from sqlalchemy.exc import SQLAlchemyError 3 | 4 | from .common import sql_get_session_engine 5 | 6 | 7 | def fetch_postgresql_type_code_to_sql_type(session): 8 | from sqlalchemy.dialects.postgresql.base import ischema_names 9 | 10 | result = {} 11 | 12 | try: 13 | types_cursor = session.execute(''' 14 | SELECT 15 | pg_catalog.format_type(oid, NULL), 16 | oid 17 | FROM 18 | pg_type 19 | ''') 20 | 21 | for pg_type in types_cursor: 22 | # Copied from: 23 | # def _get_column_info 24 | # site-packages/sqlalchemy/dialects/postgresql/base.py:3716 25 | 26 | type_code = pg_type['oid'] 27 | format_type = pg_type['format_type'] 28 | 29 | def _handle_array_type(attype): 30 | return ( 31 | # strip '[]' from integer[], etc. 32 | re.sub(r"\[\]$", "", attype), 33 | attype.endswith("[]"), 34 | ) 35 | 36 | # strip (*) from character varying(5), timestamp(5) 37 | # with time zone, geometry(POLYGON), etc. 38 | attype = re.sub(r"\(.*\)", "", format_type) 39 | 40 | # strip '[]' from integer[], etc. and check if an array 41 | attype, is_array = _handle_array_type(attype) 42 | 43 | if attype.startswith('interval'): 44 | attype = 'interval' 45 | 46 | sql_type = ischema_names.get(attype) 47 | 48 | if sql_type: 49 | result[type_code] = sql_type 50 | 51 | session.commit() 52 | except SQLAlchemyError: 53 | session.rollback() 54 | 55 | return result 56 | 57 | 58 | def fetch_type_code_to_sql_type(session): 59 | if sql_get_session_engine(session) == 'postgresql': 60 | return fetch_postgresql_type_code_to_sql_type(session) 61 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/db_types/timezones.py: -------------------------------------------------------------------------------- 1 | import dateparser 2 | from datetime import datetime 3 | 4 | from .common import get_session_engine 5 | from .mongo import MongoSession 6 | from .sql import sql_fetch_default_timezone 7 | 8 | 9 | def fetch_default_timezone(conf, session): 10 | if isinstance(session, MongoSession): 11 | pass 12 | else: 13 | return sql_fetch_default_timezone(conf, session) 14 | 15 | 16 | def apply_session_timezone(session, timezone): 17 | if isinstance(session, MongoSession): 18 | pass 19 | else: 20 | if get_session_engine(session) == 'mysql': 21 | session.execute('SET time_zone = :tz', {'tz': timezone}) 22 | session.info['_queries_timezone'] = timezone 23 | elif get_session_engine(session) in ['postgresql', 'mssql']: 24 | offset_hours = dateparser.parse(datetime.now().isoformat() + timezone).utcoffset().total_seconds() / 60 / 60 25 | offset_hours_str = '{:+}'.format(offset_hours).replace(".0", "") 26 | session.execute('SET TIME ZONE :tz', {'tz': offset_hours_str}) 27 | session.info['_queries_timezone'] = timezone 28 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/encoders.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import datetime 4 | import decimal 5 | import json # noqa 6 | import uuid 7 | import six 8 | from bson import ObjectId 9 | 10 | 11 | class JSONEncoder(json.JSONEncoder): 12 | 13 | def is_aware(self, value): 14 | return value.utcoffset() is not None 15 | 16 | def default(self, obj): 17 | if isinstance(obj, datetime.datetime): 18 | representation = obj.isoformat() 19 | if representation.endswith('+00:00'): 20 | representation = representation[:-6] + 'Z' 21 | return representation 22 | elif isinstance(obj, datetime.date): 23 | return obj.isoformat() 24 | elif isinstance(obj, datetime.time): 25 | if self.is_aware(obj): 26 | raise ValueError("JSON can't represent timezone-aware times.") 27 | representation = obj.isoformat() 28 | return representation 29 | elif isinstance(obj, datetime.timedelta): 30 | return six.text_type(obj.total_seconds()) 31 | elif isinstance(obj, decimal.Decimal): 32 | return float(obj) 33 | elif isinstance(obj, uuid.UUID): 34 | return six.text_type(obj) 35 | elif isinstance(obj, ObjectId) or type(obj) is ObjectId: 36 | return six.text_type(obj) 37 | elif isinstance(obj, six.binary_type): 38 | return obj.decode('utf-8') 39 | elif hasattr(obj, 'tolist'): 40 | return obj.tolist() 41 | elif hasattr(obj, '__getitem__'): 42 | try: 43 | return dict(obj) 44 | except Exception: 45 | pass 46 | elif hasattr(obj, '__iter__'): 47 | return tuple(item for item in obj) 48 | return super(JSONEncoder, self).default(obj) 49 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/exceptions/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/api.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from jet_bridge_base import status 4 | 5 | 6 | class APIException(Exception): 7 | default_detail = 'A server error occurred.' 8 | default_code = 'error' 9 | default_status_code = status.HTTP_500_INTERNAL_SERVER_ERROR 10 | 11 | def __init__(self, detail=None, code=None, status_code=None): 12 | if detail is None: 13 | detail = self.default_detail 14 | if code is None: 15 | code = self.default_code 16 | if status_code is None: 17 | status_code = self.default_status_code 18 | 19 | self.detail = detail 20 | self.code = code 21 | self.status_code = status_code 22 | 23 | def __str__(self): 24 | return six.text_type(self.detail) 25 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/missing_argument_error.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import status 2 | from jet_bridge_base.exceptions.api import APIException 3 | 4 | 5 | class MissingArgumentError(APIException): 6 | default_detail = 'Invalid input' 7 | default_code = 'invalid' 8 | default_status_code = status.HTTP_400_BAD_REQUEST 9 | 10 | def __init__(self, arg_name): 11 | super(MissingArgumentError, self).__init__(detail='Missing argument %s' % arg_name) 12 | self.arg_name = arg_name 13 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/not_found.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import status 2 | from jet_bridge_base.exceptions.api import APIException 3 | 4 | 5 | class NotFound(APIException): 6 | default_detail = 'Not Found' 7 | default_code = 'not_found' 8 | default_status_code = status.HTTP_404_NOT_FOUND 9 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/permission_denied.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import status 2 | from jet_bridge_base.exceptions.api import APIException 3 | 4 | 5 | class PermissionDenied(APIException): 6 | default_detail = 'Permission denied' 7 | default_code = 'permission_denied' 8 | default_status_code = status.HTTP_403_FORBIDDEN 9 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/request_error.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import status 2 | from jet_bridge_base.exceptions.validation_error import ValidationError 3 | 4 | 5 | class RequestError(ValidationError): 6 | default_detail = 'invalid request' 7 | default_code = 'invalid_request' 8 | default_status_code = status.HTTP_400_BAD_REQUEST 9 | 10 | def __init__(self, request, *args, **kwargs): 11 | self.request = request 12 | super(RequestError, self).__init__(*args, **kwargs) 13 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/sql.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import status 2 | from jet_bridge_base.exceptions.api import APIException 3 | 4 | 5 | class SqlError(APIException): 6 | status_code = status.HTTP_400_BAD_REQUEST 7 | default_detail = 'Query failed' 8 | default_code = 'query_failed' 9 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/exceptions/validation_error.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import status 2 | from jet_bridge_base.exceptions.api import APIException 3 | 4 | 5 | class ValidationError(APIException): 6 | default_detail = 'invalid input' 7 | default_code = 'invalid' 8 | default_status_code = status.HTTP_400_BAD_REQUEST 9 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/external_auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/external_auth/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/external_auth/mixin.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import settings 2 | from jet_bridge_base.configuration import configuration 3 | from jet_bridge_base.exceptions.not_found import NotFound 4 | from jet_bridge_base.external_auth.utils import load_strategy, load_backends_classes 5 | 6 | 7 | class ExternalAuthMixin(object): 8 | strategy = None 9 | backends = {} 10 | backend = None 11 | 12 | def __init__(self, *args, **kwargs): 13 | self.init_backends() 14 | super(ExternalAuthMixin, self).__init__(*args, **kwargs) 15 | 16 | def init_backends(self): 17 | backend_paths = list(map(lambda x: x.get('backend_path'), settings.SSO_APPLICATIONS.values())) 18 | self.backends = load_backends_classes(backend_paths) 19 | 20 | def init_auth(self, request, app): 21 | redirect_uri = self.redirect_uri(app) 22 | 23 | try: 24 | name = configuration.clean_sso_application_name(app) 25 | config = settings.SSO_APPLICATIONS[name] 26 | except KeyError: 27 | raise NotFound 28 | 29 | backend_path = config.get('backend_path') 30 | Backend = self.backends.get(backend_path) 31 | 32 | self.strategy = load_strategy(self, request, config) 33 | self.backend = Backend(self.strategy, redirect_uri) 34 | 35 | def redirect_uri(self, app): 36 | return '/api/external_auth/complete/{0}/'.format(app) 37 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/external_auth/pipeline.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | 4 | from jet_bridge_base import settings 5 | from jet_bridge_base.configuration import configuration 6 | from jet_bridge_base.external_auth.storage import User 7 | from jet_bridge_base.utils.compress import compress_data 8 | 9 | 10 | def save_extra_data(backend, user, response, details, strategy, *args, **kwargs): 11 | extra_data = backend.extra_data(user, 0, response, details, *args, **kwargs) 12 | sso = configuration.clean_sso_application_name(getattr(backend, 'sso')) 13 | extra_data_key = '_'.join(['extra_data', sso]) 14 | 15 | data = { 16 | 'expires_on': extra_data.get('expires_on'), 17 | 'access_token': extra_data.get('access_token'), 18 | 'expires': extra_data.get('expires'), 19 | 'auth_time': extra_data.get('auth_time'), 20 | 'refresh_token': extra_data.get('refresh_token'), 21 | 'token_updated': int(time.time()) 22 | } 23 | 24 | extra_data_str = json.dumps(data) 25 | 26 | if settings.COOKIE_COMPRESS: 27 | extra_data_str = compress_data(extra_data_str) 28 | 29 | strategy.session_set(extra_data_key, extra_data_str, secure=not settings.COOKIE_COMPRESS) 30 | 31 | return { 32 | 'extra_data': extra_data 33 | } 34 | 35 | 36 | def return_result(details, extra_data, *args, **kwargs): 37 | user = User() 38 | user.details = details 39 | user.extra_data = extra_data 40 | 41 | return { 42 | 'user': user 43 | } 44 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/external_auth/storage.py: -------------------------------------------------------------------------------- 1 | from social_core.storage import BaseStorage, UserMixin 2 | 3 | 4 | class User(object): 5 | social_user = None 6 | is_new = True 7 | details = None 8 | extra_data = None 9 | 10 | 11 | class UserController(UserMixin): 12 | 13 | @classmethod 14 | def user_model(cls): 15 | return User 16 | 17 | 18 | class JetBridgeStorage(BaseStorage): 19 | user = UserController 20 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/external_auth/strategy.py: -------------------------------------------------------------------------------- 1 | from social_core.utils import build_absolute_uri 2 | from social_core.strategy import BaseStrategy 3 | 4 | from jet_bridge_base import settings 5 | from jet_bridge_base.configuration import configuration 6 | from jet_bridge_base.responses.redirect import RedirectResponse 7 | from jet_bridge_base.utils.common import merge_two_dicts 8 | 9 | 10 | class JetBridgeStrategy(BaseStrategy): 11 | 12 | def __init__(self, storage, request_handler, request, config, tpl=None): 13 | self.request_handler = request_handler 14 | self.request = request 15 | self.config = config 16 | super(JetBridgeStrategy, self).__init__(storage, tpl) 17 | 18 | def authenticate(self, *args, **kwargs): 19 | result = super(JetBridgeStrategy, self).authenticate(*args, **kwargs) 20 | # Ignore redirect response by returning user of another class 21 | return { 22 | 'auth': True, 23 | 'details': result.details, 24 | 'extra_data': result.extra_data 25 | } if result else {} 26 | 27 | def get_setting(self, name): 28 | settings = merge_two_dicts(self.config, { 29 | 'pipeline': [ 30 | 'social_core.pipeline.social_auth.social_details', 31 | 'jet_bridge_base.external_auth.pipeline.save_extra_data', 32 | 'jet_bridge_base.external_auth.pipeline.return_result' 33 | ] 34 | }) 35 | key = name.lower() 36 | return settings[key] 37 | 38 | def request_data(self, merge=True): 39 | return merge_two_dicts(self.request.data, self.request.query_arguments) 40 | 41 | def request_host(self): 42 | return self.request.host 43 | 44 | def redirect(self, url): 45 | return RedirectResponse(url) 46 | 47 | def html(self, content): 48 | self.request_handler.write(content) 49 | 50 | def session_get(self, name, default=None, secure=True): 51 | value = configuration.session_get(self.request, name, default, secure=secure) 52 | if value: 53 | return value 54 | return default 55 | 56 | def session_set(self, name, value, secure=True): 57 | configuration.session_set(self.request, name, value, secure=secure) 58 | 59 | def session_pop(self, name): 60 | value = self.session_get(name) 61 | configuration.session_clear(self.request, name) 62 | return value 63 | 64 | def session_setdefault(self, name, value): 65 | pass 66 | 67 | def build_absolute_uri(self, path=None): 68 | if settings.BASE_URL is None: 69 | base_url = '{0}://{1}'.format(self.request.protocol, self.request.host) 70 | else: 71 | base_url = settings.BASE_URL 72 | 73 | return build_absolute_uri( 74 | base_url, 75 | path 76 | ) 77 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/external_auth/utils.py: -------------------------------------------------------------------------------- 1 | from social_core.backends.utils import load_backends 2 | from social_core.utils import get_strategy 3 | 4 | 5 | STRATEGY_PATH = 'jet_bridge_base.external_auth.strategy.JetBridgeStrategy' 6 | STORAGE_PATH = 'jet_bridge_base.external_auth.storage.JetBridgeStorage' 7 | 8 | 9 | def load_strategy(request_handler, request, config): 10 | return get_strategy(STRATEGY_PATH, STORAGE_PATH, request_handler, request, config) 11 | 12 | 13 | def load_backends_classes(backend_paths): 14 | backends = load_backends(backend_paths, force_load=True) 15 | 16 | return dict(map(lambda x: (backend_paths[x[0]], x[1]), enumerate(backends.values()))) 17 | 18 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/__init__.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields.raw import RawField 2 | from jet_bridge_base.fields.char import CharField 3 | from jet_bridge_base.fields.boolean import BooleanField 4 | from jet_bridge_base.fields.integer import IntegerField 5 | from jet_bridge_base.fields.float import FloatField 6 | from jet_bridge_base.fields.datetime import DateTimeField 7 | from jet_bridge_base.fields.json import JSONField 8 | from jet_bridge_base.fields.wkt import WKTField 9 | from jet_bridge_base.fields.array import ArrayField 10 | from jet_bridge_base.fields.binary import BinaryField 11 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/array.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import json 3 | from six import string_types 4 | 5 | from jet_bridge_base.fields.field import Field 6 | 7 | 8 | class ArrayField(Field): 9 | field_error_messages = { 10 | 'invalid': 'not a valid array' 11 | } 12 | 13 | def to_internal_value_item(self, value): 14 | internal_value = None 15 | 16 | if isinstance(value, string_types): 17 | try: 18 | internal_value = json.loads(value) 19 | except ValueError: 20 | self.error('invalid') 21 | else: 22 | internal_value = value 23 | 24 | if not isinstance(internal_value, list): 25 | raise ValueError 26 | 27 | return internal_value 28 | 29 | def to_representation_item(self, value): 30 | return value 31 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/binary.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | import binascii 3 | 4 | from jet_bridge_base.fields.field import Field 5 | 6 | 7 | class BinaryField(Field): 8 | def to_internal_value_item(self, value): 9 | column = self.context.get('model_field') 10 | if column is not None \ 11 | and hasattr(column, 'params') \ 12 | and hasattr(column.params, 'get') \ 13 | and column.params.get('type') == 'object_id': 14 | return ObjectId(value) 15 | else: 16 | return binascii.unhexlify(value) 17 | 18 | def to_representation_item(self, value): 19 | if isinstance(value, bytes): 20 | return binascii.hexlify(value).decode('ascii') 21 | elif isinstance(value, ObjectId): 22 | return binascii.hexlify(value.binary).decode('ascii') 23 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/boolean.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields.field import Field 2 | 3 | 4 | class BooleanField(Field): 5 | TRUE_VALUES = { 6 | 't', 'T', 7 | 'y', 'Y', 'yes', 'YES', 8 | 'true', 'True', 'TRUE', 9 | 'on', 'On', 'ON', 10 | '1', 1, 11 | True 12 | } 13 | FALSE_VALUES = { 14 | 'f', 'F', 15 | 'n', 'N', 'no', 'NO', 16 | 'false', 'False', 'FALSE', 17 | 'off', 'Off', 'OFF', 18 | '0', 0, 0.0, 19 | False 20 | } 21 | 22 | def to_internal_value_item(self, value): 23 | if value in self.TRUE_VALUES: 24 | return True 25 | elif value in self.FALSE_VALUES: 26 | return False 27 | return bool(value) 28 | 29 | def to_representation_item(self, value): 30 | return value 31 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/char.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from jet_bridge_base.fields.field import Field 4 | 5 | 6 | class CharField(Field): 7 | 8 | def __init__(self, *args, **kwargs): 9 | self.trim_whitespace = kwargs.pop('trim_whitespace', False) 10 | super(CharField, self).__init__(*args, **kwargs) 11 | 12 | def to_internal_value_item(self, value): 13 | if value is None: 14 | return 15 | value = six.text_type(value) 16 | return value.strip() if self.trim_whitespace else value 17 | 18 | def to_representation_item(self, value): 19 | if value is None: 20 | return 21 | return six.text_type(value) 22 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/datetime.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import dateparser 3 | import six 4 | 5 | from jet_bridge_base.db import get_default_timezone 6 | from jet_bridge_base.fields.field import Field 7 | 8 | 9 | def get_timezone_from_str(value): 10 | if not value: 11 | return 12 | 13 | return datetime.datetime.strptime(value.replace(':', ''), '%z').tzinfo 14 | 15 | 16 | def datetime_apply_default_timezone(value, request): 17 | if value.tzinfo is not None: 18 | return value 19 | 20 | default_timezone = get_default_timezone(request) if request else None 21 | if default_timezone: 22 | return value.replace(tzinfo=default_timezone) 23 | 24 | return value 25 | 26 | 27 | class DateTimeField(Field): 28 | field_error_messages = { 29 | 'invalid': 'date has wrong format' 30 | } 31 | 32 | def to_internal_value_item(self, value): 33 | if value is None: 34 | return 35 | value = six.text_type(value).strip() 36 | 37 | try: 38 | result = dateparser.parse(value) 39 | except ValueError: 40 | result = None 41 | 42 | if result is None: 43 | self.error('invalid') 44 | 45 | return result 46 | 47 | def to_representation_item(self, value): 48 | if value is None: 49 | return 50 | 51 | request = self.context.get('request') 52 | value = datetime_apply_default_timezone(value, request) 53 | 54 | return value.isoformat() 55 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/float.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from jet_bridge_base.fields.field import Field 4 | 5 | 6 | class FloatField(Field): 7 | field_error_messages = { 8 | 'invalid': 'not a valid float' 9 | } 10 | 11 | def to_internal_value_item(self, value): 12 | if value is None: 13 | return 14 | value = six.text_type(value).strip() 15 | 16 | if value.lower() == 'true': 17 | return 1 18 | elif value.lower() == 'false': 19 | return 0 20 | 21 | try: 22 | return float(value) 23 | except (ValueError, TypeError): 24 | self.error('invalid') 25 | 26 | def to_representation_item(self, value): 27 | if value is None: 28 | return 29 | return value 30 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/integer.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from jet_bridge_base.fields.field import Field 4 | 5 | 6 | class IntegerField(Field): 7 | field_error_messages = { 8 | 'invalid': 'not a valid integer' 9 | } 10 | 11 | def to_internal_value_item(self, value): 12 | if value is None: 13 | return 14 | 15 | if value is True: 16 | return 1 17 | elif value is False: 18 | return 0 19 | 20 | value = six.text_type(value).strip() 21 | 22 | try: 23 | return int(value) 24 | except (ValueError, TypeError): 25 | self.error('invalid') 26 | 27 | def to_representation_item(self, value): 28 | if value is None: 29 | return 30 | return value 31 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/json.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import json 3 | 4 | from six import string_types 5 | 6 | from jet_bridge_base.fields.field import Field 7 | 8 | 9 | class JSONField(Field): 10 | field_error_messages = { 11 | 'invalid': 'not a valid JSON' 12 | } 13 | 14 | def __init__(self, *args, **kwargs): 15 | if 'allow_many' not in kwargs: 16 | kwargs['allow_many'] = True 17 | super(JSONField, self).__init__(*args, **kwargs) 18 | 19 | def to_internal_value_item(self, value): 20 | if isinstance(value, string_types): 21 | try: 22 | return json.loads(value) 23 | except ValueError: 24 | return value 25 | else: 26 | return value 27 | 28 | def to_representation_item(self, value): 29 | return value 30 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/raw.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields.field import Field 2 | 3 | 4 | class RawField(Field): 5 | 6 | def to_internal_value_item(self, value): 7 | return value 8 | 9 | def to_representation_item(self, value): 10 | return value 11 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/sql_params.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import fields 2 | 3 | 4 | class SqlParamsSerializers(fields.CharField): 5 | 6 | def to_internal_value_item(self, value): 7 | value = super(SqlParamsSerializers, self).to_internal_value_item(value) 8 | if value is None: 9 | return [] 10 | # value = list(filter(lambda x: x != '', value.split(','))) 11 | value = value.split(',') 12 | return dict([['param_{}'.format(i), x] for i, x in enumerate(value)]) 13 | 14 | def to_representation_item(self, value): 15 | return list(value) 16 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/fields/wkt.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from jet_bridge_base.fields.field import Field 4 | 5 | 6 | class WKTField(Field): 7 | field_error_messages = { 8 | 'invalid': 'not a valid Geo object - {error}' 9 | } 10 | 11 | def to_internal_value_item(self, value): 12 | if value is None: 13 | return 14 | from geoalchemy2 import ArgumentError, WKTElement 15 | try: 16 | return WKTElement(value) 17 | except ArgumentError as e: 18 | self.error('invalid', error=six.text_type(e)) 19 | 20 | def to_representation_item(self, value): 21 | if value is None: 22 | return 23 | from geoalchemy2.shape import to_shape 24 | shape = to_shape(value) 25 | if hasattr(shape, 'wkt'): 26 | return shape.wkt 27 | else: 28 | # Backward compatibility Shapely <1.8.0 29 | return shape.to_wkt() 30 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/filters/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/binary_filter.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields import BinaryField 2 | from jet_bridge_base.filters.filter import Filter 3 | 4 | 5 | class BinaryFilter(Filter): 6 | field_class = BinaryField 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/boolean_filter.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields import BooleanField 2 | from jet_bridge_base.filters.filter import Filter 3 | 4 | 5 | class BooleanFilter(Filter): 6 | field_class = BooleanField 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/char_filter.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields import CharField 2 | from jet_bridge_base.filters.filter import Filter 3 | 4 | 5 | class CharFilter(Filter): 6 | field_class = CharField 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/datetime_filter.py: -------------------------------------------------------------------------------- 1 | 2 | from jet_bridge_base.fields.datetime import DateTimeField 3 | from jet_bridge_base.filters.filter import Filter 4 | 5 | 6 | class DateTimeFilter(Filter): 7 | field_class = DateTimeField 8 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/filter_class.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types import inspect_uniform 2 | from jet_bridge_base.filters import lookups 3 | from jet_bridge_base.filters.filter import Filter 4 | from jet_bridge_base.filters.filter_for_dbfield import filter_for_column 5 | 6 | 7 | class FilterClass(object): 8 | filters = [] 9 | 10 | def __init__(self, *args, **kwargs): 11 | self.meta = getattr(self, 'Meta', None) 12 | if 'context' in kwargs: 13 | self.handler = kwargs['context'].get('handler', None) 14 | self.update_filters() 15 | 16 | def update_filters(self): 17 | filters = [] 18 | 19 | if self.meta: 20 | if hasattr(self.meta, 'model'): 21 | Model = self.meta.model 22 | mapper = inspect_uniform(Model) 23 | columns = mapper.columns 24 | 25 | if hasattr(self.meta, 'fields'): 26 | columns = filter(lambda x: x.name in self.meta.fields, columns) 27 | 28 | for column in columns: 29 | item = filter_for_column(column) 30 | for lookup in item['lookups']: 31 | for exclude in [False, True]: 32 | instance = item['filter_class']( 33 | name=column.key, 34 | column=column, 35 | lookup=lookup, 36 | exclude=exclude 37 | ) 38 | filters.append(instance) 39 | 40 | declared_filters = filter(lambda x: isinstance(x[1], Filter), map(lambda x: (x, getattr(self, x)), dir(self))) 41 | 42 | for filter_name, filter_item in declared_filters: 43 | filter_item.name = filter_name 44 | filter_item.model = Model 45 | filter_item.handler = self.handler 46 | filters.append(filter_item) 47 | 48 | self.filters = filters 49 | 50 | def filter_queryset(self, request, queryset): 51 | def get_filter_value(name, filters_instance=None): 52 | return request.get_argument_safe(name, None) 53 | 54 | for item in self.filters: 55 | if item.name: 56 | argument_name = '{}__{}'.format(item.name, item.lookup) 57 | if item.exclude: 58 | argument_name = 'exclude__{}'.format(argument_name) 59 | value = get_filter_value(argument_name, item) 60 | 61 | if value is None and item.lookup == lookups.DEFAULT_LOOKUP: 62 | argument_name = item.name 63 | if item.exclude: 64 | argument_name = 'exclude__{}'.format(argument_name) 65 | value = get_filter_value(argument_name, item) 66 | else: 67 | value = None 68 | 69 | queryset = item.filter(queryset, value) 70 | return queryset 71 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/float_filter.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields import FloatField 2 | from jet_bridge_base.filters.filter import Filter 3 | 4 | 5 | class FloatFilter(Filter): 6 | field_class = FloatField 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/integer_filter.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import fields 2 | from jet_bridge_base.fields import FloatField 3 | from jet_bridge_base.filters.filter import Filter 4 | 5 | 6 | class IntegerFilter(Filter): 7 | field_class = FloatField 8 | 9 | def get_default_lookup_field_class(self): 10 | return fields.FloatField 11 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/lookups.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from jet_bridge_base.utils.gql import RawScalar 4 | 5 | EXACT = 'exact' 6 | GT = 'gt' 7 | GTE = 'gte' 8 | LT = 'lt' 9 | LTE = 'lte' 10 | ICONTAINS = 'icontains' 11 | IN = 'in' 12 | STARTS_WITH = 'istartswith' 13 | ENDS_WITH = 'iendswith' 14 | IS_NULL = 'isnull' 15 | IS_EMPTY = 'isempty' 16 | JSON_ICONTAINS = 'json_icontains' 17 | COVEREDBY = 'coveredby' 18 | DEFAULT_LOOKUP = EXACT 19 | 20 | by_gql = { 21 | 'eq': EXACT, 22 | 'gt': GT, 23 | 'gte': GTE, 24 | 'lt': LT, 25 | 'lte': LTE, 26 | 'containsI': ICONTAINS, 27 | 'in': IN, 28 | 'startsWithI': STARTS_WITH, 29 | 'endsWithI': ENDS_WITH, 30 | 'isNull': IS_NULL, 31 | 'isEmpty': IS_EMPTY, 32 | 'jsonContainsI': JSON_ICONTAINS, 33 | 'coveredBy': COVEREDBY 34 | } 35 | 36 | gql_scalar = { 37 | IN: graphene.List(RawScalar) 38 | } 39 | 40 | gql = dict(map(lambda x: (x[1], x[0]), by_gql.items())) 41 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/model.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.filters.filter_class import FilterClass 2 | from jet_bridge_base.filters.model_m2m import get_model_m2m_filter 3 | from jet_bridge_base.filters.model_relation import get_model_relation_filter 4 | from jet_bridge_base.filters.model_search import get_model_search_filter 5 | from jet_bridge_base.filters.model_segment import get_model_segment_filter 6 | from jet_bridge_base.filters.order_by import OrderFilter 7 | 8 | 9 | def get_model_filter_class(request, Model): 10 | search_filter = get_model_search_filter(Model) 11 | model_m2m_filter = get_model_m2m_filter(Model) 12 | model_segment_filter = get_model_segment_filter(request, Model) 13 | model_relation_filter = get_model_relation_filter(request, Model) 14 | 15 | class ModelFilterClass(FilterClass): 16 | _order_by = OrderFilter() 17 | _search = search_filter() 18 | _m2m = model_m2m_filter() 19 | _segment = model_segment_filter() 20 | _relation = model_relation_filter() 21 | 22 | class Meta: 23 | model = Model 24 | 25 | return ModelFilterClass 26 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/model_aggregate.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types import queryset_aggregate 2 | from jet_bridge_base.filters.char_filter import CharFilter 3 | from jet_bridge_base.filters.filter import EMPTY_VALUES 4 | 5 | 6 | class ModelAggregateFilter(CharFilter): 7 | 8 | def filter(self, qs, value): 9 | if value in EMPTY_VALUES: 10 | return qs 11 | 12 | return queryset_aggregate(self.model, qs, value) 13 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/model_group.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types.queryset import queryset_group 2 | from jet_bridge_base.filters.char_filter import CharFilter 3 | from jet_bridge_base.filters.filter import EMPTY_VALUES 4 | 5 | 6 | class ModelGroupFilter(CharFilter): 7 | 8 | def filter(self, qs, value): 9 | if value in EMPTY_VALUES: 10 | return qs 11 | 12 | return queryset_group(self.model, qs, value) 13 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/model_m2m.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types import empty_filter, inspect_uniform 2 | from jet_bridge_base.filters.char_filter import CharFilter 3 | from jet_bridge_base.filters.filter import EMPTY_VALUES 4 | 5 | 6 | def get_model_m2m_filter(Model): 7 | mapper = inspect_uniform(Model) 8 | 9 | class ModelM2MFilter(CharFilter): 10 | 11 | def filter(self, qs, value): 12 | if value in EMPTY_VALUES: 13 | return qs 14 | 15 | params = value.split(',', 2) 16 | 17 | if len(params) < 2: 18 | return qs.filter(empty_filter(Model)) 19 | 20 | relation_name, value = params 21 | 22 | relations = [] 23 | 24 | for relationship in mapper.relationships: 25 | for sub_relationship in relationship.mapper.relationships: 26 | if sub_relationship.table.name != relation_name: 27 | continue 28 | 29 | relations.append({ 30 | 'relationship': relationship, 31 | 'sub_relationship': sub_relationship 32 | }) 33 | 34 | if len(relations) == 0: 35 | return qs.filter(empty_filter(Model)) 36 | 37 | relation = relations[0] 38 | relationship_entity = relation['relationship'].mapper.entity 39 | id_column_name = relation['sub_relationship'].primaryjoin.right.name 40 | relationship_entity_id_key = getattr(relationship_entity, id_column_name) 41 | 42 | qs = qs.join(relationship_entity) 43 | qs = qs.filter(relationship_entity_id_key == value) 44 | 45 | return qs 46 | 47 | return ModelM2MFilter 48 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/model_relation.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy 2 | 3 | from jet_bridge_base.db import get_mapped_base 4 | from jet_bridge_base.db_types import inspect_uniform 5 | from jet_bridge_base.filters.char_filter import CharFilter 6 | from jet_bridge_base.filters.filter import EMPTY_VALUES 7 | 8 | 9 | def filter_search_field(field): 10 | allowed_fields = [ 11 | sqlalchemy.String, 12 | sqlalchemy.JSON, 13 | ] 14 | 15 | return isinstance(field.type, tuple(allowed_fields)) 16 | 17 | 18 | def get_model_relation_filter(request, Model): 19 | mapper = inspect_uniform(Model) 20 | MappedBase = get_mapped_base(request) 21 | 22 | class ModelRelationFilter(CharFilter): 23 | def filter(self, qs, value): 24 | if value in EMPTY_VALUES: 25 | return qs 26 | 27 | current_table = mapper.tables[0] 28 | path = list(map(lambda x: x.split('.'), value.split('|'))) 29 | path_len = len(path) 30 | 31 | for i in range(path_len): 32 | item = path[i] 33 | last = i == path_len - 1 34 | 35 | if not last: 36 | current_table_column = current_table.columns[item[0]] 37 | 38 | related_table = MappedBase.metadata.tables[item[1]] 39 | related_table_column = related_table.columns[item[2]] 40 | 41 | qs = qs.join(related_table, current_table_column == related_table_column) 42 | current_table = related_table 43 | else: 44 | current_table_column = current_table.columns[item[0]] 45 | value = item[1].split(',') 46 | qs = qs.filter(current_table_column.in_(value)) 47 | 48 | return qs 49 | 50 | return ModelRelationFilter 51 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/model_search.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types import inspect_uniform, queryset_search 2 | from jet_bridge_base.filters.char_filter import CharFilter 3 | from jet_bridge_base.filters.filter import EMPTY_VALUES 4 | 5 | 6 | def get_model_search_filter(Model): 7 | mapper = inspect_uniform(Model) 8 | 9 | class ModelSearchFilter(CharFilter): 10 | def filter(self, qs, value): 11 | if value in EMPTY_VALUES: 12 | return qs 13 | 14 | value = self.clean_value(value) 15 | return queryset_search(qs, mapper, value) 16 | 17 | return ModelSearchFilter 18 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/model_segment.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types import empty_filter, inspect_uniform 2 | from jet_bridge_base.filters.char_filter import CharFilter 3 | from jet_bridge_base.filters.filter import EMPTY_VALUES 4 | from jet_bridge_base.serializers.sql import SqlSerializer 5 | 6 | 7 | def get_model_segment_filter(request, Model): 8 | mapper = inspect_uniform(Model) 9 | primary_key = mapper.primary_key[0].name 10 | 11 | class ModelSegmentFilter(CharFilter): 12 | def filter(self, qs, value): 13 | value = self.clean_value(value) 14 | if value in EMPTY_VALUES: 15 | return qs 16 | 17 | body = self.handler.data 18 | 19 | if not isinstance(body, dict): 20 | return qs.filter(empty_filter(Model)) 21 | 22 | items = list(filter(lambda x: x.get('name') == value, body.get('segments', []))) 23 | 24 | if len(items) == 0: 25 | return qs.filter(empty_filter(Model)) 26 | 27 | query = items[0].get('query') 28 | 29 | serializer = SqlSerializer(data={'query': query}, context={'request': request}) 30 | serializer.is_valid(raise_exception=True) 31 | result = serializer.execute() 32 | columns = list(result['columns']) 33 | rows = result['data'] 34 | 35 | if len(columns) == 0 or len(rows) == 0: 36 | return qs.filter(empty_filter(Model)) 37 | 38 | ids = list(map(lambda x: list(x)[0], rows)) 39 | 40 | return qs.filter(getattr(Model, primary_key).in_(ids)) 41 | 42 | return ModelSegmentFilter 43 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/order_by.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types import desc_uniform, empty_filter 2 | from jet_bridge_base.filters.char_filter import CharFilter 3 | from jet_bridge_base.filters.filter import EMPTY_VALUES 4 | 5 | 6 | class OrderFilter(CharFilter): 7 | 8 | def filter(self, qs, value): 9 | if value in EMPTY_VALUES: 10 | return qs 11 | 12 | if len(value) < 2: 13 | return qs.filter(empty_filter(self.model)) 14 | 15 | ordering = value.split(',') 16 | 17 | def map_field(name): 18 | descending = False 19 | if name.startswith('-'): 20 | name = name[1:] 21 | descending = True 22 | field = getattr(self.model, name) 23 | if descending: 24 | field = desc_uniform(field) 25 | return field 26 | 27 | if ordering: 28 | qs = qs.order_by(*map(lambda x: map_field(x), ordering)) 29 | 30 | return qs 31 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/filters/wkt_filter.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields import WKTField 2 | from jet_bridge_base.filters.filter import Filter 3 | 4 | 5 | class WKTFilter(Filter): 6 | field_class = WKTField 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger('jet_bridge') 4 | ch = logging.StreamHandler() 5 | 6 | 7 | class Formatter(logging.Formatter): 8 | 9 | formats = { 10 | logging.INFO: '%(message)s' 11 | } 12 | default_format = '%(levelname)s - %(asctime)s: %(message)s' 13 | 14 | def formatMessage(self, record): 15 | return self.formats.get(record.levelno, self.default_format) % record.__dict__ 16 | 17 | formatter = Formatter('%(asctime)s %(levelname)s %(message)s', '%Y-%m-%d %H:%M:%S') 18 | 19 | ch.setFormatter(formatter) 20 | 21 | logger.propagate = False 22 | logger.addHandler(ch) 23 | 24 | def set_logger_level(level): 25 | ch.setLevel(level) 26 | logger.setLevel(level) 27 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/media_cache.py: -------------------------------------------------------------------------------- 1 | import os 2 | import hashlib 3 | 4 | from jet_bridge_base.configuration import configuration 5 | 6 | 7 | class MediaCache(object): 8 | max_cache_size = 1024 * 1024 * 50 9 | files = [] 10 | size = 0 11 | dir = '_jet_cache' 12 | 13 | def __init__(self): 14 | self.update_files() 15 | 16 | def get_files(self): 17 | result = [] 18 | if configuration.media_exists(self.dir): 19 | directories, files = configuration.media_listdir(self.dir) 20 | for f in files: 21 | fp = os.path.join(self.dir, f) 22 | result.append({ 23 | 'path': fp, 24 | 'size': configuration.media_size(fp) 25 | }) 26 | self.sort_files(result) 27 | return result 28 | 29 | def sort_files(self, files): 30 | files.sort(key=lambda x: configuration.media_get_modified_time(x['path'])) 31 | 32 | def get_files_size(self, files): 33 | total_size = 0 34 | for file in files: 35 | total_size += file['size'] 36 | return total_size 37 | 38 | def update_files(self): 39 | self.files = self.get_files() 40 | self.size = self.get_files_size(self.files) 41 | 42 | def add_file(self, path): 43 | absolute_path = cache.full_path(path) 44 | size = configuration.media_size(absolute_path) 45 | 46 | self.files.append({ 47 | 'path': absolute_path, 48 | 'size': size 49 | }) 50 | self.size += size 51 | self.sort_files(self.files) 52 | 53 | def clear_cache_if_needed(self): 54 | while self.size > self.max_cache_size: 55 | configuration.media_delete(self.files[0]['path']) 56 | self.size -= self.files[0]['size'] 57 | self.files.remove(self.files[0]) 58 | 59 | def filename(self, path): 60 | extension = os.path.splitext(path)[1] 61 | return '{}{}'.format(hashlib.sha256(path.encode('utf8')).hexdigest(), extension) 62 | 63 | def full_path(self, path): 64 | return os.path.join(self.dir, self.filename(path)) 65 | 66 | def exists(self, path): 67 | thumbnail_full_path = self.full_path(path) 68 | return configuration.media_exists(thumbnail_full_path) 69 | 70 | def url(self, path, request): 71 | return configuration.media_url(os.path.join(self.dir, self.filename(path)), request) 72 | 73 | cache = MediaCache() 74 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/messages.py: -------------------------------------------------------------------------------- 1 | 2 | GET_ACTION_LIST = 'get_action_list' 3 | EXECUTE_ACTION = 'execute_action' 4 | GET_FIELD_OPTIONS = 'get_field_options' 5 | GET_ELEMENT_STATUS = 'get_element_status' 6 | 7 | message_handlers = {} 8 | 9 | 10 | def add_handler(message_name, func): 11 | message_handlers[message_name] = func 12 | 13 | 14 | def get_handler(message_name): 15 | return message_handlers.get(message_name) 16 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import model_relation_override 2 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/models/base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.declarative import declarative_base 2 | 3 | Base = declarative_base() 4 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/models/column.py: -------------------------------------------------------------------------------- 1 | 2 | class Column(object): 3 | 4 | def __init__(self, name, data_type): 5 | self.name = name 6 | self.data_type = data_type 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/models/data_types.py: -------------------------------------------------------------------------------- 1 | CHAR = 'CharField' 2 | FIXED_CHAR = 'FixedCharField' 3 | TEXT = 'TextField' 4 | UUID = 'UUIDField' 5 | SELECT = 'SelectField' 6 | BOOLEAN = 'BooleanField' 7 | INTEGER = 'IntegerField' 8 | BIG_INTEGER = 'BigIntegerField' 9 | SMALL_INTEGER = 'SmallIntegerField' 10 | FLOAT = 'FloatField' 11 | NUMBER = 'NumberField' 12 | DECIMAL = 'DecimalField' 13 | DOUBLE_PRECISION = 'DoublePrecisionField' 14 | MONEY = 'MoneyField' 15 | DATE = 'DateField' 16 | DATE_TIME = 'DateTimeField' 17 | TIMESTAMP = 'TimestampField' 18 | FOREIGN_KEY = 'ForeignKey' 19 | JSON = 'JSONField' 20 | GEOMETRY = 'GeometryField' 21 | GEOGRAPHY = 'GeographyField' 22 | BINARY = 'BinaryField' 23 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/models/model_relation_override.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String, Boolean, Integer, Sequence, Index 2 | 3 | from jet_bridge_base.models.base import Base 4 | 5 | 6 | class ModelRelationOverrideModel(Base): 7 | __tablename__ = 'model_relation_override' 8 | 9 | id = Column(Integer, Sequence('id_seq', start=1), primary_key=True) 10 | 11 | connection_id = Column(String) 12 | model = Column(String) 13 | draft = Column(Boolean) 14 | name = Column(String) 15 | direction = Column(String) 16 | local_field = Column(String) 17 | related_model = Column(String) 18 | related_field = Column(String) 19 | 20 | __table_args__ = ( 21 | Index('model_draft', connection_id, model, draft), 22 | ) 23 | 24 | def __repr__(self): 25 | return '{} {} {}'.format(self.connection_id, self.model, self.draft) 26 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/models/table.py: -------------------------------------------------------------------------------- 1 | 2 | class Table(object): 3 | 4 | def __init__(self, name, columns): 5 | self.name = name 6 | self.columns = columns 7 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/paginators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/paginators/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/paginators/pagination.py: -------------------------------------------------------------------------------- 1 | 2 | class Pagination(object): 3 | count = None 4 | 5 | def paginate_queryset(self, request, queryset, handler): 6 | raise NotImplementedError 7 | 8 | def get_paginated_response(self, request, data): 9 | raise NotImplementedError 10 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/responses/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/responses/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/responses/base.py: -------------------------------------------------------------------------------- 1 | 2 | class Response(object): 3 | def __init__(self, data=None, status=None, headers=None, exception=False, content_type=None): 4 | self.data = data 5 | self.status = status 6 | self.exception = exception 7 | self.content_type = content_type 8 | self.headers = self.default_headers() 9 | 10 | if headers: 11 | self.headers.update(headers) 12 | 13 | def default_headers(self): 14 | return {'Content-Type': 'text/html'} 15 | 16 | def header_items(self): 17 | if not self.headers: 18 | return [] 19 | return self.headers.items() 20 | 21 | def render(self): 22 | return self.data 23 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/responses/json.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import json 3 | 4 | from jet_bridge_base import encoders 5 | from jet_bridge_base.responses.base import Response 6 | 7 | 8 | class JSONResponse(Response): 9 | encoder_class = encoders.JSONEncoder 10 | 11 | def __init__(self, *args, **kwargs): 12 | self.rendered_data = kwargs.pop('rendered_data', None) 13 | super(JSONResponse, self).__init__(*args, **kwargs) 14 | 15 | def default_headers(self): 16 | return {'Content-Type': 'application/json'} 17 | 18 | def render(self): 19 | if self.rendered_data is not None: 20 | return self.rendered_data 21 | 22 | if self.data is None: 23 | return 24 | 25 | self.rendered_data = json.dumps( 26 | self.data, 27 | cls=self.encoder_class 28 | ) 29 | return self.rendered_data 30 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/responses/not_found.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.responses.base import Response 2 | from jet_bridge_base.status import HTTP_404_NOT_FOUND 3 | 4 | 5 | class NotFoundResponse(Response): 6 | 7 | def __init__(self): 8 | super(NotFoundResponse, self).__init__( 9 | status=HTTP_404_NOT_FOUND, 10 | data='

Not Found

The requested URL {} was not found on this server.

' 11 | ) 12 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/responses/optional_json.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.responses.json import JSONResponse 2 | 3 | 4 | class OptionalJSONResponse(JSONResponse): 5 | pass 6 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/responses/redirect.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.responses.base import Response 2 | from jet_bridge_base.status import HTTP_302_FOUND 3 | 4 | 5 | class RedirectResponse(Response): 6 | 7 | def __init__(self, url, status=HTTP_302_FOUND): 8 | self.url = url 9 | super(RedirectResponse, self).__init__(status=status) 10 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/responses/template.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.responses.base import Response 2 | 3 | 4 | class TemplateResponse(Response): 5 | headers = {'Content-Type': 'text/html'} 6 | 7 | def __init__(self, template, data=None, status=None, headers=None, exception=False, content_type=None): 8 | self.template = template 9 | super(TemplateResponse, self).__init__(data, status, headers, exception, content_type) 10 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/router.py: -------------------------------------------------------------------------------- 1 | 2 | def action(methods=None, detail=False): 3 | if methods is None: 4 | methods = ['get'] 5 | else: 6 | methods = [method.lower() for method in methods] 7 | 8 | def decorator(func): 9 | func.bind_to_methods = methods 10 | func.detail = detail 11 | return func 12 | return decorator 13 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/sentry.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.exceptions.api import APIException 2 | from jet_bridge_base.logger import logger 3 | 4 | 5 | class SentryController(object): 6 | sentry_sdk = None 7 | 8 | def enable(self, dsn, release, tornado=False): 9 | try: 10 | import sentry_sdk 11 | 12 | integrations = [] 13 | 14 | if tornado: 15 | try: 16 | from sentry_sdk.integrations.tornado import TornadoIntegration 17 | integrations.append(TornadoIntegration()) 18 | except ImportError: 19 | pass 20 | 21 | sentry_sdk.init( 22 | dsn=dsn, 23 | integrations=integrations, 24 | release=release, 25 | before_send=lambda event, hint: self.before_send(event, hint) 26 | ) 27 | 28 | self.sentry_sdk = sentry_sdk 29 | except ImportError: 30 | self.sentry_sdk = None 31 | 32 | def before_send(self, event, hint): 33 | if 'exc_info' in hint: 34 | exc_type, exc_value, tb = hint['exc_info'] 35 | if isinstance(exc_value, (APIException, KeyboardInterrupt)): 36 | return None 37 | if event.get('logger') == 'jet_bridge': 38 | return None 39 | return event 40 | 41 | def set_user(self, user): 42 | if not self.sentry_sdk: 43 | return 44 | 45 | self.sentry_sdk.set_user(user) 46 | 47 | def set_context(self, name, value): 48 | if not self.sentry_sdk: 49 | return 50 | 51 | self.sentry_sdk.set_context(name, value) 52 | 53 | def capture_exception(self, exc): 54 | logger.exception(exc) 55 | 56 | if not self.sentry_sdk: 57 | return 58 | 59 | self.sentry_sdk.capture_exception(exc) 60 | 61 | def capture_message(self, message): 62 | logger.error(message) 63 | 64 | if not self.sentry_sdk: 65 | return 66 | 67 | self.sentry_sdk.capture_message(message) 68 | 69 | 70 | sentry_controller = SentryController() 71 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/serializers/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/message.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import fields 2 | from jet_bridge_base.messages import get_handler 3 | from jet_bridge_base.serializers.serializer import Serializer 4 | 5 | 6 | class MessageSerializer(Serializer): 7 | name = fields.CharField() 8 | params = fields.JSONField(required=False) 9 | 10 | def save(self): 11 | handler = get_handler(self.validated_data['name']) 12 | if not handler: 13 | return 14 | return handler(self.validated_data['name'], self.validated_data.get('params', {})) 15 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/model.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db_types import inspect_uniform 2 | from jet_bridge_base.serializers.model_serializer import ModelSerializer 3 | 4 | 5 | def get_model_serializer(Model): 6 | mapper = inspect_uniform(Model) 7 | 8 | class CustomModelSerializer(ModelSerializer): 9 | class Meta: 10 | model = Model 11 | model_fields = list(map(lambda x: x.key, mapper.columns)) 12 | 13 | return CustomModelSerializer 14 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/model_description.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import fields as fields_ 2 | from jet_bridge_base.serializers.serializer import Serializer 3 | 4 | 5 | class ModelDescriptionFieldSerializer(Serializer): 6 | name = fields_.CharField() 7 | db_column = fields_.CharField() 8 | field = fields_.CharField() 9 | db_field = fields_.CharField() 10 | filterable = fields_.BooleanField() 11 | null = fields_.BooleanField() 12 | editable = fields_.BooleanField() 13 | params = fields_.JSONField() 14 | data_source_field = fields_.CharField(required=False) 15 | data_source_name = fields_.CharField(required=False) 16 | data_source_params = fields_.JSONField(required=False) 17 | data_source_order_after = fields_.CharField(required=False) 18 | data_source_hidden = fields_.BooleanField(required=False) 19 | verbose_name = fields_.CharField(required=False) 20 | required = fields_.BooleanField(required=False) 21 | default_type = fields_.CharField(required=False) 22 | default_value = fields_.RawField(required=False) 23 | length = fields_.IntegerField(required=False) 24 | related_model = fields_.JSONField(required=False) 25 | custom_primary_key = fields_.CharField(required=False) 26 | 27 | 28 | class ModelDescriptionRelationSerializer(Serializer): 29 | name = fields_.CharField() 30 | direction = fields_.CharField() 31 | local_field = fields_.CharField() 32 | related_model = fields_.CharField() 33 | related_field = fields_.CharField() 34 | 35 | 36 | class ModelDescriptionSerializer(Serializer): 37 | model = fields_.CharField() 38 | db_table = fields_.CharField() 39 | hidden = fields_.BooleanField() 40 | primary_key_field = fields_.CharField() 41 | primary_key_auto = fields_.BooleanField() 42 | is_view = fields_.BooleanField() 43 | fields = ModelDescriptionFieldSerializer(many=True) 44 | relations = ModelDescriptionRelationSerializer(many=True) 45 | relation_overrides = ModelDescriptionRelationSerializer(many=True, required=False) 46 | verbose_name = fields_.CharField(required=False) 47 | verbose_name_plural = fields_.CharField(required=False) 48 | display_field = fields_.CharField(required=False) 49 | default_order_by = fields_.CharField(required=False) 50 | data_source_name = fields_.CharField(required=False) 51 | data_source_name_plural = fields_.CharField(required=False) 52 | data_source_order_after = fields_.CharField(required=False) 53 | data_source_hidden = fields_.BooleanField(required=False) 54 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/model_group.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.fields import CharField 2 | from jet_bridge_base.serializers.serializer import Serializer 3 | 4 | 5 | class ModelGroupSerializer(Serializer): 6 | group = CharField() 7 | y_func = CharField() # TODO: change to integer default 8 | 9 | def __init__(self, *args, **kwargs): 10 | if 'group_serializer' in kwargs: 11 | self.fields['group'] = kwargs.pop('group_serializer') 12 | 13 | if 'y_func_serializer' in kwargs: 14 | self.fields['y_func'] = kwargs.pop('y_func_serializer') 15 | 16 | super(ModelGroupSerializer, self).__init__(*args, **kwargs) 17 | 18 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/reorder.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import inspect 2 | from sqlalchemy.exc import SQLAlchemyError 3 | 4 | from jet_bridge_base import fields 5 | from jet_bridge_base.serializers.serializer import Serializer 6 | 7 | 8 | def get_reorder_serializer(Model, queryset, session): 9 | class ReorderSerializer(Serializer): 10 | ordering_field = fields.CharField() 11 | forward = fields.BooleanField() 12 | segment_from = fields.IntegerField() 13 | segment_to = fields.IntegerField() 14 | item = fields.IntegerField() 15 | segment_by_ordering_field = fields.BooleanField(default=False) 16 | 17 | def save(self): 18 | mapper = inspect(Model) 19 | 20 | primary_key_field = mapper.primary_key[0].name 21 | ordering_field = self.validated_data['ordering_field'] 22 | primary_key = getattr(Model, primary_key_field) 23 | ordering = getattr(Model, ordering_field) 24 | 25 | if self.validated_data.get('segment_by_ordering_field'): 26 | segment_from = self.validated_data['segment_from'] 27 | segment_to = self.validated_data['segment_to'] 28 | else: 29 | segment_from_instance = queryset.filter(primary_key == self.validated_data['segment_from']).first() 30 | segment_to_instance = queryset.filter(primary_key == self.validated_data['segment_to']).first() 31 | 32 | segment_from = getattr(segment_from_instance, ordering_field) 33 | segment_to = getattr(segment_to_instance, ordering_field) 34 | 35 | if self.validated_data['forward']: 36 | queryset.filter( 37 | ordering >= segment_from, 38 | ordering <= segment_to 39 | ).update( 40 | {ordering_field: ordering - 1} 41 | ) 42 | queryset.filter( 43 | primary_key == self.validated_data['item'] 44 | ).update( 45 | {ordering_field: segment_to} 46 | ) 47 | else: 48 | queryset.filter( 49 | ordering >= segment_from, 50 | ordering <= segment_to 51 | ).update( 52 | {ordering_field: ordering + 1} 53 | ) 54 | queryset.filter( 55 | primary_key == self.validated_data['item'] 56 | ).update( 57 | {ordering_field: segment_to} 58 | ) 59 | 60 | try: 61 | session.commit() 62 | except SQLAlchemyError as e: 63 | session.rollback() 64 | raise e 65 | 66 | return ReorderSerializer 67 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/reset_order.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import case 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from sqlalchemy.orm import load_only 4 | 5 | from jet_bridge_base import fields 6 | from jet_bridge_base.db_types import desc_uniform 7 | from jet_bridge_base.serializers.serializer import Serializer 8 | 9 | 10 | def get_reset_order_serializer(Model, queryset, session): 11 | class ResetOrderSerializer(Serializer): 12 | ordering_field = fields.CharField() 13 | ordering = fields.CharField(required=False, allow_null=True) 14 | value_ordering = fields.CharField(required=False, allow_null=True) 15 | 16 | def save(self): 17 | ordering_field = self.validated_data['ordering_field'] 18 | ordering = self.validated_data.get('ordering') 19 | value_ordering = self.validated_data.get('value_ordering') 20 | 21 | qs = queryset 22 | order_by = [] 23 | 24 | if value_ordering: 25 | field, values_str = value_ordering.split('-', 2) 26 | values = values_str.split(',') 27 | order_by.append(case( 28 | [(getattr(Model, field) == x, i) for i, x in enumerate(values)], 29 | else_=len(values) 30 | )) 31 | 32 | if ordering: 33 | def map_field(name): 34 | descending = False 35 | if name.startswith('-'): 36 | name = name[1:] 37 | descending = True 38 | field = getattr(Model, name) 39 | if descending: 40 | field = desc_uniform(field) 41 | return field 42 | 43 | order_by.extend(map(lambda x: map_field(x), ordering.split(','))) 44 | 45 | if order_by: 46 | qs = qs.order_by(*order_by) 47 | 48 | i = 1 49 | 50 | try: 51 | items = qs.options(load_only(ordering_field)).all() 52 | except SQLAlchemyError: 53 | queryset.session.rollback() 54 | raise 55 | 56 | for instance in items: 57 | setattr(instance, ordering_field, i) 58 | i += 1 59 | 60 | try: 61 | session.commit() 62 | except SQLAlchemyError: 63 | session.rollback() 64 | raise 65 | 66 | return ResetOrderSerializer 67 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/serializers/table.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import fields 2 | from jet_bridge_base.exceptions.validation_error import ValidationError 3 | from jet_bridge_base.serializers.serializer import Serializer 4 | 5 | 6 | class TableColumnParamsRelatedModelSerializer(Serializer): 7 | model = fields.CharField() 8 | 9 | 10 | class TableColumnParamsSerializer(Serializer): 11 | length = fields.IntegerField(required=False) 12 | related_model = TableColumnParamsRelatedModelSerializer(required=False) 13 | custom_primary_key = fields.CharField(required=False) 14 | 15 | 16 | class TableColumnSerializer(Serializer): 17 | name = fields.CharField() 18 | field = fields.CharField() 19 | db_field = fields.CharField(required=False) 20 | primary_key = fields.BooleanField(required=False) 21 | null = fields.BooleanField(required=False) 22 | default_type = fields.CharField(required=False) 23 | default_value = fields.RawField(required=False) 24 | params = TableColumnParamsSerializer(required=False) 25 | data_source_field = fields.CharField(required=False) 26 | data_source_name = fields.CharField(required=False) 27 | data_source_params = fields.JSONField(required=False) 28 | 29 | 30 | class TableSerializer(Serializer): 31 | name = fields.CharField() 32 | columns = TableColumnSerializer(many=True) 33 | data_source_name = fields.CharField(required=False) 34 | data_source_name_plural = fields.CharField(required=False) 35 | 36 | def validate(self, attrs): 37 | if not any(map(lambda x: x.get('primary_key'), attrs['columns'])): 38 | raise ValidationError('No primary key specified') 39 | return attrs 40 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | 5 | from jet_bridge_base.logger import set_logger_level 6 | 7 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 8 | CURRENT_MODULE = sys.modules[__name__] 9 | DEFAULT_CONFIG_PATH = os.path.join('jet.conf') 10 | 11 | DEBUG = False 12 | READ_ONLY = False 13 | AUTO_OPEN_REGISTER = True 14 | CONFIG = DEFAULT_CONFIG_PATH 15 | PROJECT = None 16 | TOKEN = None 17 | ENVIRONMENT = None 18 | CORS_HEADERS = True 19 | BASE_URL = None 20 | JWT_VERIFY_KEY = '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyfJablscZmsh7eHisWHi\n/x2gJUjc9juL4VLkEpp6PH9Ah+wBtGmPF9nfoVlQsgH9hra63POiaLUzIwW/ewOl\njyPF0FJngsxreCxNs8lmd/WNXrcnazknFItkFFeXJuMTfoBPQGiZOmVYb14jmvkc\n9vMmeXYjrbT+95SVayN3E6DzLoHDhny4Mka1OsxvIP5s77s0dOo68TzoEfBVeuto\nI/dopG86DVu4wYVtYPITzJ4z47OFVPKCyYVyy5aR3+DUnmdK7xTRVr+iWmHpcr7e\nhoeVcL4CqAILZ0gd54kQmnHbg7Bu6x8JtQkiLU5TQvWzjiN00io4eydvIAkQTAaR\nmdd32O1vJbSHmLyCR2tEW/uV7P25naPUlkApxuLzh5C21S0XJxNJ/P07KSMymt5U\n1lWqt4CInpjAwMI8qs9MkEwJev5+yumxqIrDKcQLMR3TBLJZIb+rL1teCLOW28qB\nL6VSKhfKRIaXUdLpRwAcSuXraTzwa9oCCZa19tw3uizMeMFrCrv43YbyOsS9h7JQ\n8ixj/a1R/ud0fCrhXWUl7nKlz0b15koILLG1Ts+MUTmIaEnHTVEY74CfJVq7waw9\nx2kyzSzbsmMXvFkrVzTmyImTN631+gatU+npJ3vtcD9SooEZLOCLa4pb+DIsv9P1\nEeIEAh1VZC7s2qsQZsiYTG0CAwEAAQ==\n-----END PUBLIC KEY-----\n' 21 | BEARER_AUTH_KEY = None 22 | ENVIRONMENT_TYPE = None 23 | BLACKLIST_HOSTS = None 24 | 25 | WEB_BASE_URL = None 26 | API_BASE_URL = None 27 | 28 | DATABASE_ENGINE = None 29 | DATABASE_URL = None 30 | DATABASE_HOST = None 31 | DATABASE_PORT = None 32 | DATABASE_USER = None 33 | DATABASE_PASSWORD = None 34 | DATABASE_NAME = None 35 | DATABASE_EXTRA = None 36 | DATABASE_CONNECTIONS = None 37 | DATABASE_CONNECTIONS_OVERFLOW = None 38 | DATABASE_ONLY = None 39 | DATABASE_EXCEPT = None 40 | DATABASE_MAX_TABLES = None 41 | DATABASE_SCHEMA = None 42 | DATABASE_TIMEZONE = None 43 | DATABASE_RLS_TYPE = None 44 | DATABASE_RLS_SSO = None 45 | DATABASE_RLS_USER_PROPERTY = None 46 | DATABASE_REFLECT_MAX_RECORDS = None 47 | 48 | DATABASE_SSL_CA = None 49 | DATABASE_SSL_CERT = None 50 | DATABASE_SSL_KEY = None 51 | 52 | DATABASE_SSH_HOST = None 53 | DATABASE_SSH_PORT = None 54 | DATABASE_SSH_USER = None 55 | DATABASE_SSH_PRIVATE_KEY = None 56 | 57 | COOKIE_SAMESITE = None 58 | COOKIE_SECURE = None 59 | COOKIE_DOMAIN = None 60 | COOKIE_COMPRESS = None 61 | 62 | STORE_PATH = None 63 | 64 | CACHE_METADATA = False 65 | CACHE_METADATA_PATH = None 66 | CACHE_MODEL_DESCRIPTIONS = False 67 | 68 | SSO_APPLICATIONS = {} 69 | 70 | ALLOW_ORIGIN = '*' 71 | 72 | TRACK_DATABASES = '' 73 | TRACK_DATABASES_ENDPOINT = '' 74 | TRACK_DATABASES_AUTH = '' 75 | 76 | TRACK_MODELS_ENDPOINT = '' 77 | TRACK_MODELS_AUTH = '' 78 | 79 | TRACK_QUERY_SLOW_TIME = None 80 | TRACK_QUERY_HIGH_MEMORY = None 81 | 82 | RELEASE_INACTIVE_GRAPHQL_SCHEMAS_TIMEOUT = None 83 | 84 | DISABLE_AUTH = None 85 | 86 | 87 | def set_settings(settings): 88 | for key, value in settings.items(): 89 | if value is None: 90 | continue 91 | setattr(CURRENT_MODULE, key, value) 92 | 93 | level = logging.DEBUG if DEBUG else logging.INFO 94 | set_logger_level(level) 95 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/status.py: -------------------------------------------------------------------------------- 1 | 2 | def is_informational(code): 3 | return 100 <= code <= 199 4 | 5 | 6 | def is_success(code): 7 | return 200 <= code <= 299 8 | 9 | 10 | def is_redirect(code): 11 | return 300 <= code <= 399 12 | 13 | 14 | def is_client_error(code): 15 | return 400 <= code <= 499 16 | 17 | 18 | def is_server_error(code): 19 | return 500 <= code <= 599 20 | 21 | 22 | HTTP_100_CONTINUE = 100 23 | HTTP_101_SWITCHING_PROTOCOLS = 101 24 | HTTP_200_OK = 200 25 | HTTP_201_CREATED = 201 26 | HTTP_202_ACCEPTED = 202 27 | HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 28 | HTTP_204_NO_CONTENT = 204 29 | HTTP_205_RESET_CONTENT = 205 30 | HTTP_206_PARTIAL_CONTENT = 206 31 | HTTP_207_MULTI_STATUS = 207 32 | HTTP_300_MULTIPLE_CHOICES = 300 33 | HTTP_301_MOVED_PERMANENTLY = 301 34 | HTTP_302_FOUND = 302 35 | HTTP_303_SEE_OTHER = 303 36 | HTTP_304_NOT_MODIFIED = 304 37 | HTTP_305_USE_PROXY = 305 38 | HTTP_306_RESERVED = 306 39 | HTTP_307_TEMPORARY_REDIRECT = 307 40 | HTTP_400_BAD_REQUEST = 400 41 | HTTP_401_UNAUTHORIZED = 401 42 | HTTP_402_PAYMENT_REQUIRED = 402 43 | HTTP_403_FORBIDDEN = 403 44 | HTTP_404_NOT_FOUND = 404 45 | HTTP_405_METHOD_NOT_ALLOWED = 405 46 | HTTP_406_NOT_ACCEPTABLE = 406 47 | HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 48 | HTTP_408_REQUEST_TIMEOUT = 408 49 | HTTP_409_CONFLICT = 409 50 | HTTP_410_GONE = 410 51 | HTTP_411_LENGTH_REQUIRED = 411 52 | HTTP_412_PRECONDITION_FAILED = 412 53 | HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 54 | HTTP_414_REQUEST_URI_TOO_LONG = 414 55 | HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 56 | HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 57 | HTTP_417_EXPECTATION_FAILED = 417 58 | HTTP_422_UNPROCESSABLE_ENTITY = 422 59 | HTTP_423_LOCKED = 423 60 | HTTP_424_FAILED_DEPENDENCY = 424 61 | HTTP_428_PRECONDITION_REQUIRED = 428 62 | HTTP_429_TOO_MANY_REQUESTS = 429 63 | HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 64 | HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451 65 | HTTP_500_INTERNAL_SERVER_ERROR = 500 66 | HTTP_501_NOT_IMPLEMENTED = 501 67 | HTTP_502_BAD_GATEWAY = 502 68 | HTTP_503_SERVICE_UNAVAILABLE = 503 69 | HTTP_504_GATEWAY_TIMEOUT = 504 70 | HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 71 | HTTP_507_INSUFFICIENT_STORAGE = 507 72 | HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 73 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/store.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import scoped_session, sessionmaker 3 | 4 | from jet_bridge_base import settings 5 | from jet_bridge_base.logger import logger 6 | from jet_bridge_base.models.base import Base 7 | 8 | 9 | class Store(object): 10 | engine = None 11 | sessions = None 12 | 13 | def __init__(self, path, base): 14 | self.path = path 15 | self.base = base 16 | self.open() 17 | 18 | def open(self): 19 | try: 20 | self.engine = create_engine('sqlite:///{}'.format(self.path)) 21 | self.sessions = scoped_session(sessionmaker(self.engine)) 22 | 23 | import jet_bridge_base.models 24 | Base.metadata.create_all(self.engine) 25 | except Exception as e: 26 | logger.error('Store initialize failed', exc_info=e) 27 | 28 | def close(self): 29 | if self.engine: 30 | self.engine.dispose() 31 | 32 | def is_ok(self): 33 | return self.engine is not None and self.sessions is not None 34 | 35 | def session(self): 36 | return self.sessions() 37 | 38 | 39 | store = Store(settings.STORE_PATH, Base) 40 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/templates/403.html: -------------------------------------------------------------------------------- 1 |

Forbidden

You don't have permission to access {{ path }} on this server

-------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/templates/404.html: -------------------------------------------------------------------------------- 1 |

Not Found

The requested URL {{ path }} was not found on this server.

-------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/templates/500.html: -------------------------------------------------------------------------------- 1 |

Server Error (500)

-------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/templates/external_auth_complete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/utils/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/base62.py: -------------------------------------------------------------------------------- 1 | BASE62_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 2 | 3 | 4 | def base62_encode(byte_data): 5 | num = int.from_bytes(byte_data, 'big') 6 | if num == 0: 7 | return BASE62_ALPHABET[0] 8 | encoded = "" 9 | while num > 0: 10 | num, rem = divmod(num, 62) 11 | encoded = BASE62_ALPHABET[rem] + encoded 12 | return encoded 13 | 14 | 15 | def base62_decode(encoded_str): 16 | num = 0 17 | for char in encoded_str: 18 | num = num * 62 + BASE62_ALPHABET.index(char) 19 | # Calculate the number of bytes needed 20 | byte_length = (num.bit_length() + 7) // 8 21 | return num.to_bytes(byte_length, 'big') 22 | 23 | 24 | def utf8_to_base62(s): 25 | return base62_encode(s.encode('utf-8')) 26 | 27 | 28 | def base62_to_utf8(encoded_str): 29 | return base62_decode(encoded_str).decode('utf-8') 30 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/classes.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | def issubclass_safe(x, A_tuple): 5 | return inspect.isclass(x) and issubclass(x, A_tuple) 6 | 7 | 8 | def is_instance_or_subclass(x, A_tuple): 9 | return isinstance(x, A_tuple) or issubclass_safe(x, A_tuple) 10 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/common.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import random 3 | import time 4 | 5 | try: 6 | random = random.SystemRandom() 7 | using_sysrandom = True 8 | except NotImplementedError: 9 | import warnings 10 | warnings.warn('A secure pseudo-random number generator is not available ' 11 | 'on your system. Falling back to Mersenne Twister.') 12 | using_sysrandom = False 13 | 14 | 15 | def get_random_string(length, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', salt=''): 16 | """ 17 | Returns a securely generated random string. 18 | 19 | The default length of 12 with the a-z, A-Z, 0-9 character set returns 20 | a 71-bit value. log_2((26+26+10)^12) =~ 71 bits 21 | """ 22 | if not using_sysrandom: 23 | # This is ugly, and a hack, but it makes things better than 24 | # the alternative of predictability. This re-seeds the PRNG 25 | # using a value that is hard for an attacker to predict, every 26 | # time a random string is required. This may change the 27 | # properties of the chosen random sequence slightly, but this 28 | # is better than absolute predictability. 29 | random.seed( 30 | hashlib.sha256( 31 | ("%s%s%s" % ( 32 | random.getstate(), 33 | time.time(), 34 | salt)).encode('utf-8') 35 | ).digest()) 36 | return ''.join(random.choice(allowed_chars) for i in range(length)) 37 | 38 | 39 | def find_index(list, predicate): 40 | for i, value in enumerate(list): 41 | if predicate(value, i): 42 | return i 43 | return None 44 | 45 | 46 | # TODO: Fix non dict list items 47 | # TODO: List merge is not universal 48 | def merge(destination, source): 49 | for key, value in source.items(): 50 | if key == 'params': 51 | destination[key] = value 52 | elif isinstance(value, dict): 53 | node = destination.setdefault(key, {}) 54 | merge(node, value) 55 | elif isinstance(value, list): 56 | node = destination.setdefault(key, []) 57 | 58 | for item in value: 59 | index = find_index(node, lambda x, i: x['db_column'] == item['db_column']) 60 | if index is None: 61 | continue 62 | node[index] 63 | merge(node[index], item) 64 | else: 65 | destination[key] = value 66 | 67 | return destination 68 | 69 | 70 | def merge_two_dicts(x, y): 71 | z = x.copy() 72 | z.update(y) 73 | return z 74 | 75 | 76 | def get_set_first(value): 77 | return next(iter(value)) 78 | 79 | 80 | def any_type_sorter(value): 81 | if value is None: 82 | return '' 83 | return str(value) 84 | 85 | 86 | def unique(arr): 87 | result = [] 88 | for item in arr: 89 | if item not in result: 90 | result.append(item) 91 | return result 92 | 93 | 94 | def flatten(arr): 95 | result = [] 96 | for item in arr: 97 | if isinstance(item, list): 98 | result.extend(item) 99 | else: 100 | result.append(item) 101 | return result 102 | 103 | 104 | def format_size(num, suffix='B'): 105 | for unit in ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi'): 106 | if abs(num) < 1024.0: 107 | return f'{num:3.1f}{unit}{suffix}' 108 | num /= 1024.0 109 | return f'{num:.1f}Yi{suffix}' 110 | 111 | 112 | class CollectionDict(dict): 113 | def __iter__(self): 114 | for column in self.values(): 115 | yield column 116 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/compress.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import gzip 3 | 4 | 5 | def decompress_data(value): 6 | try: 7 | bytes = base64.b64decode(value) 8 | data = gzip.decompress(bytes) 9 | return data.decode('utf-8') 10 | except AttributeError: 11 | return value.decode('zlib') 12 | 13 | 14 | def compress_data(data): 15 | try: 16 | encoded = data.encode('utf-8') 17 | bytes = gzip.compress(encoded) 18 | return str(base64.b64encode(bytes), 'utf-8') 19 | except AttributeError: 20 | return data.encode('zlib') 21 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/crypt.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from cryptography.fernet import Fernet 3 | from cryptography.hazmat.backends import default_backend 4 | from cryptography.hazmat.primitives import hashes 5 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC 6 | from hashlib import sha256 7 | 8 | backend = default_backend() 9 | 10 | 11 | def decrypt(message_encrypted, secret_key): 12 | message_salt = message_encrypted[-24:] 13 | message_payload = message_encrypted[:-24] 14 | salt = base64.b64decode(message_salt) 15 | kdf = PBKDF2HMAC( 16 | algorithm=hashes.SHA256(), 17 | length=32, 18 | salt=salt, 19 | iterations=100000, 20 | backend=backend 21 | ) 22 | key = base64.urlsafe_b64encode(kdf.derive(bytes(secret_key, encoding='utf8'))) 23 | f = Fernet(key) 24 | return f.decrypt(bytes(message_payload, encoding='latin1')).decode('utf8') 25 | 26 | 27 | def get_sha256_hash(value): 28 | return sha256(value.encode('utf-8')).hexdigest() 29 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/datetime.py: -------------------------------------------------------------------------------- 1 | def date_trunc_minutes(date): 2 | return date.replace(minute=0, second=0, microsecond=0) 3 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | import re 2 | import six 3 | 4 | from jet_bridge_base.db_types import inspect_uniform 5 | from jet_bridge_base.exceptions.validation_error import ValidationError 6 | 7 | 8 | def serialize_validation_error(exc): 9 | def process(e, root=False): 10 | if isinstance(e.detail, dict): 11 | return dict(map(lambda x: (x[0], process(x[1])), e.detail.items())) 12 | elif isinstance(e.detail, list): 13 | return list(map(lambda x: process(x), e.detail)) 14 | elif root: 15 | return {'non_field_errors': [e.detail]} 16 | else: 17 | return e.detail 18 | 19 | return process(exc, root=True) 20 | 21 | 22 | def validation_error_from_database_error(e, model): 23 | if hasattr(e, 'orig'): 24 | if hasattr(e.orig, 'args') and hasattr(e.orig.args, '__getitem__'): 25 | if len(e.orig.args) == 1: 26 | message = e.orig.args[0] 27 | elif len(e.orig.args) == 2: 28 | message = e.orig.args[1] 29 | else: 30 | message = e.orig.args 31 | 32 | message = six.text_type(message) 33 | 34 | regex = [ 35 | [r'Key\s\((.+)\)=\((.+)\)\salready\sexists', 1, 2], # PostgreSQL 36 | [r'Duplicate\sentry\s\'(.+)\'\sfor key\s\'(.+)\'', 2, 1], # MySQL 37 | [r'UNIQUE\sconstraint\sfailed\:\s(.+)\.(.+)', 2, None] # SQLite 38 | ] 39 | 40 | for (r, field_index, value_index) in regex: 41 | match = re.search(r, message, re.IGNORECASE | re.MULTILINE) 42 | 43 | if match: 44 | mapper = inspect_uniform(model) 45 | columns = dict(map(lambda x: (x.key, x), mapper.columns)) 46 | column_name = match.group(field_index) 47 | 48 | if column_name in columns: 49 | error = dict() 50 | error[column_name] = ValidationError('record with the same value already exists') 51 | return ValidationError(error) 52 | 53 | return ValidationError(message) 54 | return ValidationError('Query failed') 55 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/gql.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class RawScalar(graphene.Scalar): 5 | @staticmethod 6 | def serialize(value): 7 | return value 8 | 9 | @staticmethod 10 | def parse_literal(node, _variables=None): 11 | return node.value 12 | 13 | @staticmethod 14 | def parse_value(value): 15 | return value 16 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/http.py: -------------------------------------------------------------------------------- 1 | import six 2 | from six.moves.urllib import parse 3 | 4 | 5 | def replace_query_param(url, key, val): 6 | (scheme, netloc, path, query, fragment) = parse.urlsplit(six.text_type(url)) 7 | query_dict = parse.parse_qs(query, keep_blank_values=True) 8 | query_dict[six.text_type(key)] = [six.text_type(val)] 9 | query = parse.urlencode(sorted(list(query_dict.items())), doseq=True) 10 | return parse.urlunsplit((scheme, netloc, path, query, fragment)) 11 | 12 | 13 | def remove_query_param(url, key): 14 | (scheme, netloc, path, query, fragment) = parse.urlsplit(six.text_type(url)) 15 | query_dict = parse.parse_qs(query, keep_blank_values=True) 16 | query_dict.pop(key, None) 17 | query = parse.urlencode(sorted(list(query_dict.items())), doseq=True) 18 | return parse.urlunsplit((scheme, netloc, path, query, fragment)) 19 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/process.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | 3 | from jet_bridge_base.utils.common import format_size 4 | 5 | 6 | def get_memory_usage(): 7 | process = psutil.Process() 8 | return process.memory_info().rss 9 | 10 | 11 | def get_memory_usage_human(): 12 | memory_used = get_memory_usage() 13 | return format_size(memory_used) 14 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/relations.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import MANYTOONE, ONETOMANY 2 | 3 | 4 | def relationship_direction_to_str(direction): 5 | if direction == MANYTOONE: 6 | return 'MANYTOONE' 7 | elif direction == ONETOMANY: 8 | return 'ONETOMANY' 9 | else: 10 | return str(direction) 11 | 12 | 13 | def parse_relationship_direction(direction): 14 | if direction == 'MANYTOONE': 15 | return MANYTOONE 16 | elif direction == 'ONETOMANY': 17 | return ONETOMANY 18 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/siblings.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import inspect, func 2 | from sqlalchemy.exc import SQLAlchemyError 3 | from sqlalchemy.orm import load_only 4 | 5 | from jet_bridge_base.db_types import apply_default_ordering, queryset_count_optimized, get_queryset_order_by 6 | 7 | 8 | def get_row_number(Model, queryset, instance): 9 | mapper = inspect(Model) 10 | pk = mapper.primary_key[0].name 11 | ordering = get_queryset_order_by(queryset) 12 | 13 | subqquery = queryset.with_entities( 14 | mapper.primary_key[0].label('__inner__pk'), 15 | func.row_number().over(order_by=ordering).label('__inner__row') 16 | ).subquery() 17 | 18 | try: 19 | rest = queryset.session.query(subqquery.c.__inner__row).filter(subqquery.c.__inner__pk == getattr(instance, pk)) 20 | return rest.scalar() 21 | except SQLAlchemyError: 22 | queryset.session.rollback() 23 | raise 24 | 25 | 26 | def get_row_siblings(Model, queryset, row_number): 27 | mapper = inspect(Model) 28 | pk = mapper.primary_key[0].name 29 | 30 | has_prev = row_number > 1 31 | offset = row_number - 2 if has_prev else row_number - 1 32 | limit = 3 if has_prev else 2 33 | 34 | try: 35 | rows = queryset.options(load_only(pk)).limit(limit).offset(offset).all() 36 | except SQLAlchemyError: 37 | queryset.session.rollback() 38 | raise 39 | 40 | if has_prev: 41 | next_index = 2 42 | else: 43 | next_index = 1 44 | 45 | if next_index >= len(rows): 46 | next_index = None 47 | 48 | if has_prev: 49 | prev_index = 0 50 | else: 51 | prev_index = None 52 | 53 | def map_row(row): 54 | return dict(((pk, getattr(row, pk)),)) 55 | 56 | return { 57 | 'prev': map_row(rows[prev_index]) if prev_index is not None else None, 58 | 'next': map_row(rows[next_index]) if next_index is not None else None 59 | } 60 | 61 | 62 | def get_model_siblings(request, Model, instance, queryset): 63 | count = queryset_count_optimized(request.session, queryset) 64 | 65 | if count > 10000: 66 | return {} 67 | 68 | queryset = apply_default_ordering(Model, queryset) 69 | row_number = get_row_number(Model, queryset, instance) 70 | 71 | if not row_number: 72 | return {} 73 | 74 | return get_row_siblings(Model, queryset, row_number) 75 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/tables.py: -------------------------------------------------------------------------------- 1 | def get_table_name(metadata, table): 2 | if table.schema and table.schema != metadata.schema: 3 | return '{}.{}'.format(table.schema, table.name) 4 | else: 5 | return str(table.name) 6 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/text.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def clean_alphanumeric(str): 5 | return re.sub('[^0-9a-zA-Z.]+', '-', str) 6 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/timezones.py: -------------------------------------------------------------------------------- 1 | from datetime import tzinfo, timedelta 2 | 3 | 4 | class FixedOffsetTimezone(tzinfo): 5 | def __init__(self, offset): 6 | self.offset = offset 7 | self.name = 'Etc/GMT%+d' % (offset.total_seconds() / 60 / 60) 8 | 9 | def tzname(self, dt): 10 | return self.name 11 | 12 | def utcoffset(self, dt): 13 | return self.offset 14 | 15 | def dst(self, dt): 16 | return timedelta(0) 17 | 18 | def __repr__(self): 19 | return 'FixedOffsetTimezone(name={}, offset={})'.format(self.name, self.offset) 20 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/token.py: -------------------------------------------------------------------------------- 1 | import json 2 | import jwt 3 | from jwt import PyJWTError 4 | 5 | from jet_bridge_base import settings 6 | from jet_bridge_base.utils.compress import decompress_data 7 | 8 | USER_TOKEN_PREFIX = 'Token' 9 | PROJECT_TOKEN_PREFIX = 'ProjectToken' 10 | JWT_TOKEN_PREFIX = 'JWT' 11 | BEARER_TOKEN_PREFIX = 'Bearer' 12 | 13 | 14 | def parse_token(value): 15 | tokens = value.split(',') if value else [] 16 | result = {} 17 | 18 | for token in tokens: 19 | try: 20 | type, data = token.split(' ', 2) 21 | items = data.split(';') 22 | 23 | if len(items) == 0: 24 | continue 25 | 26 | try: 27 | params = dict(map(lambda x: x.split('=', 2), items[1:])) 28 | except ValueError: 29 | params = {} 30 | 31 | result[type] = { 32 | 'type': type, 33 | 'value': items[0], 34 | 'params': params 35 | } 36 | except (ValueError, AttributeError): 37 | pass 38 | 39 | if JWT_TOKEN_PREFIX in result: 40 | return result[JWT_TOKEN_PREFIX] 41 | elif len(result): 42 | return list(result.values())[0] 43 | 44 | 45 | def decode_jwt_token(token, verify_exp=True): 46 | JWT_VERIFY_KEY = '\n'.join([line.lstrip() for line in settings.JWT_VERIFY_KEY.split('\\n')]) 47 | 48 | try: 49 | return jwt.decode(token, key=JWT_VERIFY_KEY, algorithms=['RS256'], options={'verify_exp': verify_exp}) 50 | except PyJWTError: 51 | return None 52 | 53 | 54 | def decompress_permissions(permissions): 55 | decoded = decompress_data(permissions) 56 | return json.loads(decoded) 57 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/track_database.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from fnmatch import fnmatch 3 | import requests 4 | 5 | from jet_bridge_base import settings 6 | from jet_bridge_base.configuration import configuration 7 | from jet_bridge_base.db import get_request_connection 8 | from jet_bridge_base.sentry import sentry_controller 9 | from jet_bridge_base.utils.conf import get_conf 10 | 11 | TRACK_DATABASES_THROTTLE = 60 * 15 12 | 13 | 14 | def track_database(conf, connection): 15 | if not settings.TRACK_DATABASES_ENDPOINT: 16 | return 17 | 18 | current_track_date = datetime.now() 19 | latest_track_date = connection.get('tracked') 20 | 21 | if latest_track_date and (current_track_date - latest_track_date).total_seconds() < TRACK_DATABASES_THROTTLE: 22 | return 23 | 24 | track_databases = list(filter(lambda x: x != '', map(lambda x: x.lower().strip(), settings.TRACK_DATABASES.split(',')))) 25 | hostname = '{}:{}'.format(conf.get('host', ''), conf.get('port', '')).lower() 26 | 27 | if not any(map(lambda x: fnmatch(hostname, x), track_databases)): 28 | return 29 | 30 | headers = {} 31 | data = { 32 | 'databaseName': conf.get('name', ''), 33 | 'databaseSchema': conf.get('schema', ''), 34 | 'databaseHost': conf.get('host', ''), 35 | 'databasePort': conf.get('port', '') 36 | } 37 | 38 | if settings.TRACK_DATABASES_AUTH: 39 | headers['Authorization'] = settings.TRACK_DATABASES_AUTH 40 | 41 | try: 42 | r = requests.post(settings.TRACK_DATABASES_ENDPOINT, headers=headers, json=data) 43 | success = 200 <= r.status_code < 300 44 | 45 | if success: 46 | connection['tracked'] = current_track_date 47 | else: 48 | error = 'TRACK_DATABASE request error: {} {} {}'.format(r.status_code, r.reason, r.text) 49 | sentry_controller.capture_message(error) 50 | except Exception as e: 51 | sentry_controller.capture_exception(e) 52 | 53 | 54 | def track_database_async(request): 55 | if not settings.TRACK_DATABASES_ENDPOINT: 56 | return 57 | 58 | conf = get_conf(request) 59 | connection = get_request_connection(request) 60 | configuration.run_async(track_database, conf, connection) 61 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/track_model.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from jet_bridge_base import settings 6 | from jet_bridge_base.configuration import configuration 7 | from jet_bridge_base.encoders import JSONEncoder 8 | from jet_bridge_base.sentry import sentry_controller 9 | 10 | 11 | def track_model(project, environment, resource_token, model, action, uid, model_data): 12 | if not settings.TRACK_MODELS_ENDPOINT: 13 | return 14 | 15 | if project is None or environment is None or resource_token is None: 16 | error = 'MODEL_CHANGE incorrect request: {} {} {}'.format(project, environment, resource_token) 17 | sentry_controller.capture_message(error) 18 | return 19 | 20 | url = '{}/model_change'.format(settings.TRACK_MODELS_ENDPOINT) 21 | headers = { 22 | 'Content-Type': 'application/json' 23 | } 24 | data = { 25 | 'project': project, 26 | 'environment': environment, 27 | 'resource_token': resource_token, 28 | 'model': model, 29 | 'action': action, 30 | 'data': model_data 31 | } 32 | 33 | if uid is not None: 34 | data['id'] = uid 35 | 36 | if settings.TRACK_MODELS_AUTH: 37 | headers['Authorization'] = settings.TRACK_MODELS_AUTH 38 | 39 | data_str = json.dumps(data, cls=JSONEncoder) 40 | 41 | try: 42 | r = requests.post(url, data=data_str, headers=headers) 43 | success = 200 <= r.status_code < 300 44 | 45 | if not success: 46 | error = 'MODEL_CHANGE request error: {} {} {}'.format(r.status_code, r.reason, r.text) 47 | sentry_controller.capture_message(error) 48 | except Exception as e: 49 | sentry_controller.capture_exception(e) 50 | 51 | 52 | def track_model_async(request, model, action, uid, data): 53 | if not settings.TRACK_MODELS_ENDPOINT: 54 | return 55 | 56 | configuration.run_async(track_model, request.project, request.environment, request.resource_token, model, action, uid, data) 57 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/utils/utc.py: -------------------------------------------------------------------------------- 1 | from datetime import tzinfo, timedelta 2 | 3 | 4 | try: 5 | from datetime import timezone 6 | utc = timezone.utc 7 | except ImportError: 8 | # Python 2 9 | class UTC(tzinfo): 10 | def utcoffset(self, dt): 11 | return timedelta(0) 12 | 13 | def tzname(self, dt): 14 | return "UTC" 15 | 16 | def dst(self, dt): 17 | return timedelta(0) 18 | 19 | utc = UTC() 20 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/views/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/api.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.configuration import configuration 2 | from jet_bridge_base.responses.json import JSONResponse 3 | from jet_bridge_base.store import store 4 | from jet_bridge_base.views.base.api import BaseAPIView 5 | 6 | 7 | class ApiView(BaseAPIView): 8 | 9 | def get(self, request, *args, **kwargs): 10 | return JSONResponse({ 11 | 'version': configuration.get_version(), 12 | 'type': configuration.get_type(), 13 | 'store_available': store.is_ok(), 14 | 'media_url_template': configuration.media_url('{}', request) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/views/base/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/discover_connection.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db import get_connection_tunnel 2 | from jet_bridge_base.db_types import discover_connection 3 | from jet_bridge_base.exceptions.validation_error import ValidationError 4 | from jet_bridge_base.permissions import HasProjectPermissions 5 | from jet_bridge_base.responses.json import JSONResponse 6 | from jet_bridge_base.utils.conf import get_conf 7 | from jet_bridge_base.views.base.api import BaseAPIView 8 | 9 | 10 | class DiscoverConnectionView(BaseAPIView): 11 | permission_classes = (HasProjectPermissions,) 12 | 13 | def required_project_permission(self, request): 14 | return { 15 | 'permission_type': 'project', 16 | 'permission_object': 'project_customization', 17 | 'permission_actions': '' 18 | } 19 | 20 | def get(self, request, *args, **kwargs): 21 | conf = get_conf(request) 22 | 23 | try: 24 | tunnel = get_connection_tunnel(conf) 25 | status = discover_connection(conf, tunnel) 26 | 27 | return JSONResponse({ 28 | 'status': status 29 | }) 30 | except Exception as e: 31 | raise ValidationError(str(e)) 32 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/discover_table.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import settings 2 | from jet_bridge_base.db import get_connection_tunnel 3 | from jet_bridge_base.db_types import discover_tables 4 | from jet_bridge_base.exceptions.validation_error import ValidationError 5 | from jet_bridge_base.permissions import HasProjectPermissions 6 | from jet_bridge_base.responses.json import JSONResponse 7 | from jet_bridge_base.utils.conf import get_conf 8 | from jet_bridge_base.views.base.api import BaseAPIView 9 | 10 | 11 | class DiscoverTableView(BaseAPIView): 12 | permission_classes = (HasProjectPermissions,) 13 | 14 | def required_project_permission(self, request): 15 | return { 16 | 'permission_type': 'project', 17 | 'permission_object': 'project_customization', 18 | 'permission_actions': '' 19 | } 20 | 21 | def get(self, request, *args, **kwargs): 22 | conf = get_conf(request) 23 | 24 | try: 25 | tunnel = get_connection_tunnel(conf) 26 | tables = discover_tables(conf, tunnel) 27 | 28 | return JSONResponse({ 29 | 'tables': tables, 30 | 'max_tables': settings.DATABASE_MAX_TABLES 31 | }) 32 | except Exception as e: 33 | raise ValidationError(str(e)) 34 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/external_auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/views/external_auth/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/external_auth/complete.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from social_core.actions import do_complete 4 | 5 | from jet_bridge_base import settings 6 | from jet_bridge_base.configuration import configuration 7 | from jet_bridge_base.external_auth.mixin import ExternalAuthMixin 8 | from jet_bridge_base.responses.template import TemplateResponse 9 | from jet_bridge_base.views.base.api import BaseAPIView 10 | from jet_bridge_base.views.external_auth.login import REDIRECT_URI_KEY, AUTH_URI_KEY, PROJECT_KEY 11 | 12 | 13 | class ExternalAuthCompleteView(ExternalAuthMixin, BaseAPIView): 14 | 15 | def get(self, request, *args, **kwargs): 16 | backend = kwargs.get('app') 17 | return self._complete(request, backend) 18 | 19 | def post(self, request, *args, **kwargs): 20 | backend = kwargs.get('app') 21 | return self._complete(request, backend) 22 | 23 | def _complete(self, request, app): 24 | self.init_auth(request, app) 25 | 26 | # Hack for passing SSO 27 | setattr(self.backend, 'sso', app) 28 | result = do_complete( 29 | self.backend, 30 | login=lambda: None 31 | ) 32 | 33 | success = result and result.get('auth') 34 | auth_uri = configuration.session_get(request, AUTH_URI_KEY, '/api/') 35 | project = configuration.session_get(request, PROJECT_KEY) 36 | redirect_uri = configuration.session_get(request, REDIRECT_URI_KEY) 37 | 38 | data = { 39 | 'sso': app, 40 | 'token': settings.TOKEN, 41 | 'project': project, 42 | 'redirect_uri': redirect_uri 43 | } 44 | 45 | if success: 46 | data['result'] = True 47 | data['username'] = result['details'].get('username') 48 | data['email'] = result['details'].get('email') 49 | data['first_name'] = result['details'].get('first_name') 50 | data['last_name'] = result['details'].get('last_name') 51 | data['full_name'] = result['details'].get('fullname') 52 | else: 53 | data['result'] = False 54 | data['error'] = 'Authentication failed' 55 | 56 | return TemplateResponse('external_auth_complete.html', status=200, data={ 57 | 'context': json.dumps({ 58 | 'url': auth_uri, 59 | 'data': data 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/external_auth/login.py: -------------------------------------------------------------------------------- 1 | from social_core.actions import do_auth 2 | 3 | from jet_bridge_base.configuration import configuration 4 | from jet_bridge_base.external_auth.mixin import ExternalAuthMixin 5 | from jet_bridge_base.views.base.api import BaseAPIView 6 | 7 | AUTH_URI_KEY = 'auth' 8 | PROJECT_KEY = 'project' 9 | REDIRECT_URI_KEY = 'redirect_uri' 10 | 11 | 12 | class ExternalAuthLoginView(ExternalAuthMixin, BaseAPIView): 13 | 14 | def get(self, request, *args, **kwargs): 15 | app = kwargs.get('app') 16 | return self._auth(request, app) 17 | 18 | def post(self, request, *args, **kwargs): 19 | app = kwargs.get('app') 20 | return self._auth(request, app) 21 | 22 | def _auth(self, request, app): 23 | auth_uri = request.get_argument('auth_uri', None) 24 | project = request.get_argument('project', None) 25 | redirect_uri = request.get_argument('redirect_uri', None) 26 | 27 | configuration.session_set(request, AUTH_URI_KEY, auth_uri) 28 | configuration.session_set(request, PROJECT_KEY, project) 29 | configuration.session_set(request, REDIRECT_URI_KEY, redirect_uri) 30 | 31 | self.init_auth(request, app) 32 | return do_auth(self.backend) 33 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/file_upload.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jet_bridge_base.configuration import configuration 4 | from jet_bridge_base.permissions import HasProjectPermissions 5 | from jet_bridge_base.responses.json import JSONResponse 6 | from jet_bridge_base.views.base.api import APIView 7 | 8 | 9 | class FileUploadView(APIView): 10 | permission_classes = (HasProjectPermissions,) 11 | 12 | def post(self, request, *args, **kwargs): 13 | # TODO: Move to serializer 14 | original_filename, file = request.files.get('file', None) 15 | path = request.get_body_argument('path') 16 | filename = request.get_body_argument('filename', original_filename) 17 | 18 | upload_path = os.path.join(path, filename) 19 | upload_path = configuration.media_get_available_name(upload_path) 20 | 21 | # TODO: Add validations 22 | 23 | configuration.media_save(upload_path, file) 24 | 25 | return JSONResponse({ 26 | 'uploaded_path': upload_path, 27 | 'uploaded_url': configuration.media_url(upload_path, request) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/image_resize.py: -------------------------------------------------------------------------------- 1 | import io 2 | from six.moves import urllib 3 | from PIL import Image 4 | 5 | from jet_bridge_base.configuration import configuration 6 | from jet_bridge_base.exceptions.not_found import NotFound 7 | from jet_bridge_base.media_cache import cache 8 | from jet_bridge_base.responses.redirect import RedirectResponse 9 | from jet_bridge_base.views.base.api import APIView 10 | 11 | 12 | class ImageResizeView(APIView): 13 | 14 | def create_thumbnail(self, file, thumbnail_path, max_width, max_height): 15 | img = Image.open(file) 16 | img.thumbnail((max_width, max_height), Image.ANTIALIAS) 17 | 18 | with io.BytesIO() as memory_file: 19 | img.save(memory_file, format=img.format, quality=85) # TODO: determine real extension from format 20 | memory_file.seek(0) 21 | configuration.media_save(thumbnail_path, memory_file.read()) 22 | 23 | def get(self, request, *args, **kwargs): 24 | # TODO: Move to serializer 25 | # TODO: Add options dependant cache name 26 | 27 | path = request.get_argument('path') 28 | max_width = request.get_argument('max_width', 320) 29 | max_height = request.get_argument('max_height', 240) 30 | external_path = path.startswith('http://') or path.startswith('https://') 31 | 32 | try: 33 | if not cache.exists(path): 34 | thumbnail_full_path = cache.full_path(path) 35 | 36 | if not external_path: 37 | if not configuration.media_exists(path): 38 | raise NotFound 39 | 40 | file = configuration.media_open(path) 41 | else: 42 | fd = urllib.request.urlopen(path) 43 | file = io.BytesIO(fd.read()) 44 | 45 | with file: 46 | cache.clear_cache_if_needed() 47 | self.create_thumbnail(file, thumbnail_full_path, max_width, max_height) 48 | cache.add_file(path) 49 | 50 | # self.set_header('Content-Type', 'image/jpg') 51 | # 52 | # with open(thumbnail_full_path, 'rb') as f: 53 | # data = f.read() 54 | # self.write(data) 55 | # self.finish() 56 | 57 | # self.redirect(cache.url(path)) 58 | return RedirectResponse(cache.url(path, request)) 59 | except IOError as e: 60 | raise e 61 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/inspect_token.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime 3 | 4 | from jet_bridge_base import settings 5 | from jet_bridge_base.responses.json import JSONResponse 6 | from jet_bridge_base.status import HTTP_400_BAD_REQUEST 7 | from jet_bridge_base.utils.crypt import decrypt, get_sha256_hash 8 | from jet_bridge_base.utils.token import parse_token, JWT_TOKEN_PREFIX, decode_jwt_token, decompress_permissions 9 | from jet_bridge_base.views.base.api import BaseAPIView 10 | 11 | 12 | class TokenInspectView(BaseAPIView): 13 | 14 | def get(self, request, *args, **kwargs): 15 | token_str = request.get_argument('authorization', default=None) or request.headers.get('AUTHORIZATION') 16 | if not token_str: 17 | return JSONResponse({'error': 'Token not specified'}, status=HTTP_400_BAD_REQUEST) 18 | 19 | token = parse_token(token_str) 20 | 21 | if not token: 22 | return JSONResponse({'error': 'Token parse failed'}, status=HTTP_400_BAD_REQUEST) 23 | 24 | response = { 25 | 'token_type': token['type'], 26 | 'token_value': token['value'], 27 | 'token_params': token['params'] 28 | } 29 | 30 | if token['type'] == JWT_TOKEN_PREFIX: 31 | jwt_value = decode_jwt_token(token['value'], verify_exp=False) 32 | 33 | response['jwt_data'] = {**jwt_value} 34 | 35 | if jwt_value: 36 | expire = datetime.utcfromtimestamp(jwt_value['exp']) 37 | 38 | response['jwt_data']['exp'] = expire.isoformat() 39 | response['jwt_data']['expired'] = datetime.utcnow() >= expire 40 | response['jwt_data']['exp_raw'] = jwt_value['exp'] 41 | 42 | projects_extended = {} 43 | 44 | for project, user_permissions in jwt_value.get('projects', {}).items(): 45 | if 'permissions' in user_permissions: 46 | permissions = decompress_permissions(user_permissions['permissions']) 47 | else: 48 | permissions = [] 49 | 50 | projects_extended[project] = { 51 | **user_permissions, 52 | 'permissions': permissions, 53 | 'permissions_raw': user_permissions.get('permissions') 54 | } 55 | 56 | response['jwt_data']['projects'] = projects_extended 57 | 58 | bridge_settings_str = request.get_argument('bridge_settings', default=None) or request.headers.get('X_BRIDGE_SETTINGS') 59 | if bridge_settings_str: 60 | response['bridge_settings_str'] = bridge_settings_str 61 | 62 | try: 63 | secret_key = settings.TOKEN.replace('-', '').lower() 64 | decrypted = decrypt(bridge_settings_str, secret_key) 65 | 66 | bridge_settings = json.loads(decrypted) 67 | bridge_settings_token = bridge_settings.get('token') 68 | 69 | response['bridge_settings'] = { 70 | 'token': bridge_settings_token, 71 | 'token_hash': get_sha256_hash(bridge_settings_token.replace('-', '').lower()), 72 | 'project': bridge_settings.get('project') 73 | } 74 | except Exception as e: 75 | response['bridge_settings_error'] = repr(e) 76 | 77 | return JSONResponse(response) 78 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/message.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.permissions import HasProjectPermissions 2 | from jet_bridge_base.responses.optional_json import OptionalJSONResponse 3 | from jet_bridge_base.serializers.message import MessageSerializer 4 | from jet_bridge_base.views.base.api import APIView 5 | 6 | 7 | class MessageView(APIView): 8 | permission_classes = (HasProjectPermissions,) 9 | 10 | def post(self, request, *args, **kwargs): 11 | serializer = MessageSerializer(data=request.data) 12 | serializer.is_valid(raise_exception=True) 13 | 14 | result = serializer.save() 15 | 16 | return OptionalJSONResponse(result) 17 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_bridge_base/jet_bridge_base/views/mixins/__init__.py -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/mixins/create.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base import status 2 | from jet_bridge_base.configuration import configuration 3 | from jet_bridge_base.responses.json import JSONResponse 4 | from jet_bridge_base.utils.track_database import track_database_async 5 | from jet_bridge_base.utils.track_model import track_model_async 6 | 7 | 8 | class CreateAPIViewMixin(object): 9 | 10 | def create(self, request, *args, **kwargs): 11 | track_database_async(request) 12 | 13 | self.apply_timezone(request) 14 | request.apply_rls_if_enabled() 15 | 16 | serializer = self.get_serializer(request, data=request.data) 17 | serializer.is_valid(raise_exception=True) 18 | self.perform_create(request, serializer) 19 | 20 | representation_data = serializer.representation_data 21 | track_model_async(request, kwargs.get('model'), 'create', None, representation_data) 22 | 23 | return JSONResponse(representation_data, status=status.HTTP_201_CREATED) 24 | 25 | def perform_create(self, request, serializer): 26 | serializer_instance = serializer.create_instance(serializer.validated_data) 27 | configuration.on_model_pre_create(request.path_kwargs['model'], serializer_instance) 28 | instance = serializer.save() 29 | configuration.on_model_post_create(request.path_kwargs['model'], instance) 30 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/mixins/destroy.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.utils.track_database import track_database_async 2 | from jet_bridge_base.utils.track_model import track_model_async 3 | from sqlalchemy.exc import SQLAlchemyError 4 | 5 | from jet_bridge_base import status 6 | from jet_bridge_base.configuration import configuration 7 | from jet_bridge_base.responses.json import JSONResponse 8 | from jet_bridge_base.utils.exceptions import validation_error_from_database_error 9 | 10 | 11 | class DestroyAPIViewMixin(object): 12 | 13 | def destroy(self, request, *args, **kwargs): 14 | track_database_async(request) 15 | 16 | self.apply_timezone(request) 17 | request.apply_rls_if_enabled() 18 | 19 | instance = self.get_object(request) 20 | self.perform_destroy(request, instance) 21 | 22 | serializer = self.get_serializer(request, instance=instance) 23 | representation_data = serializer.representation_data 24 | track_model_async(request, kwargs.get('model'), 'delete', kwargs.get('pk'), representation_data) 25 | 26 | return JSONResponse(status=status.HTTP_204_NO_CONTENT) 27 | 28 | def perform_destroy(self, request, instance): 29 | configuration.on_model_pre_delete(request.path_kwargs['model'], instance) 30 | Model = self.get_model(request) 31 | request.session.delete(instance) 32 | 33 | try: 34 | request.session.commit() 35 | except SQLAlchemyError as e: 36 | request.session.rollback() 37 | raise validation_error_from_database_error(e, Model) 38 | 39 | configuration.on_model_post_delete(request.path_kwargs['model'], instance) 40 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/mixins/list.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import SQLAlchemyError 2 | 3 | from jet_bridge_base.db_types import get_queryset_limit 4 | from jet_bridge_base.responses.json import JSONResponse 5 | from jet_bridge_base.utils.track_database import track_database_async 6 | 7 | 8 | class ListAPIViewMixin(object): 9 | 10 | def list(self, request, *args, **kwargs): 11 | track_database_async(request) 12 | 13 | self.apply_timezone(request) 14 | request.apply_rls_if_enabled() 15 | 16 | queryset = self.filter_queryset(request, self.get_queryset(request)) 17 | 18 | paginate = not request.get_argument('_no_pagination', False) 19 | 20 | try: 21 | page = self.paginate_queryset(request, queryset) if paginate else None 22 | if page is not None: 23 | instance = list(page) 24 | serializer = self.get_serializer(request, instance=instance, many=True) 25 | return self.get_paginated_response(request, serializer.representation_data) 26 | except SQLAlchemyError: 27 | request.session.rollback() 28 | raise 29 | 30 | if get_queryset_limit(queryset) is None: 31 | queryset = queryset.limit(10000) 32 | 33 | try: 34 | instance = list(queryset) 35 | serializer = self.get_serializer(request, instance=instance, many=True) 36 | data = serializer.representation_data 37 | return JSONResponse(data) 38 | except SQLAlchemyError: 39 | request.session.rollback() 40 | raise 41 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/mixins/model.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.views.base.generic_api import GenericAPIView 2 | from jet_bridge_base.views.mixins.create import CreateAPIViewMixin 3 | from jet_bridge_base.views.mixins.destroy import DestroyAPIViewMixin 4 | from jet_bridge_base.views.mixins.list import ListAPIViewMixin 5 | from jet_bridge_base.views.mixins.retrieve import RetrieveAPIViewMixin 6 | from jet_bridge_base.views.mixins.update import UpdateAPIViewMixin 7 | 8 | 9 | class ModelAPIViewMixin(ListAPIViewMixin, 10 | RetrieveAPIViewMixin, 11 | DestroyAPIViewMixin, 12 | CreateAPIViewMixin, 13 | UpdateAPIViewMixin, 14 | GenericAPIView): 15 | pass 16 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/mixins/retrieve.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.responses.json import JSONResponse 2 | from jet_bridge_base.utils.track_database import track_database_async 3 | 4 | 5 | class RetrieveAPIViewMixin(object): 6 | 7 | def retrieve(self, request, *args, **kwargs): 8 | track_database_async(request) 9 | 10 | self.apply_timezone(request) 11 | request.apply_rls_if_enabled() 12 | instance = self.get_object(request) 13 | serializer = self.get_serializer(request, instance=instance) 14 | return JSONResponse(serializer.representation_data) 15 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/mixins/update.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.configuration import configuration 2 | from jet_bridge_base.responses.json import JSONResponse 3 | from jet_bridge_base.utils.track_database import track_database_async 4 | from jet_bridge_base.utils.track_model import track_model_async 5 | 6 | 7 | class UpdateAPIViewMixin(object): 8 | 9 | def update(self, request, *args, **kwargs): 10 | track_database_async(request) 11 | 12 | partial = kwargs.pop('partial', False) 13 | self.apply_timezone(request) 14 | request.apply_rls_if_enabled() 15 | instance = self.get_object(request) 16 | serializer = self.get_serializer(request, instance=instance, data=request.data, partial=partial) 17 | serializer.is_valid(raise_exception=True) 18 | self.perform_update(request, serializer) 19 | 20 | representation_data = serializer.representation_data 21 | track_model_async(request, kwargs.get('model'), 'update', kwargs.get('pk'), representation_data) 22 | 23 | return JSONResponse(representation_data) 24 | 25 | def put(self, *args, **kwargs): 26 | self.update(*args, **kwargs) 27 | 28 | def patch(self, *args, **kwargs): 29 | self.update(partial=True, *args, **kwargs) 30 | 31 | def perform_update(self, request, serializer): 32 | configuration.on_model_pre_update(request.path_kwargs['model'], serializer.instance) 33 | instance = serializer.save() 34 | configuration.on_model_post_update(request.path_kwargs['model'], instance) 35 | 36 | def partial_update(self, *args, **kwargs): 37 | kwargs['partial'] = True 38 | return self.update(*args, **kwargs) 39 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/model_description_relationship_override.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.serializers.relationship_override import ModelDescriptionRelationOverridesSerializer 2 | 3 | from jet_bridge_base.permissions import HasProjectPermissions 4 | from jet_bridge_base.responses.json import JSONResponse 5 | from jet_bridge_base.views.base.api import APIView 6 | 7 | 8 | class ModelDescriptionRelationshipOverrideView(APIView): 9 | permission_classes = (HasProjectPermissions,) 10 | 11 | def post(self, request, *args, **kwargs): 12 | serializer_context = {'request': request} 13 | serializer = ModelDescriptionRelationOverridesSerializer(data=request.data, many=True, context=serializer_context) 14 | serializer.is_valid(raise_exception=True) 15 | serializer.save() 16 | 17 | return JSONResponse(serializer.representation_data) 18 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/proxy_request.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.serializers.proxy_request import ProxyRequestSerializer 2 | from jet_bridge_base.views.base.api import BaseAPIView 3 | 4 | 5 | class ProxyRequestView(BaseAPIView): 6 | serializer_class = ProxyRequestSerializer 7 | 8 | def get(self, request, *args, **kwargs): 9 | return self.make_request(request, request.query_arguments, *args, **kwargs) 10 | 11 | def post(self, request, *args, **kwargs): 12 | return self.make_request(request, request.data, *args, **kwargs) 13 | 14 | def make_request(self, request, data, *args, **kwargs): 15 | serializer = ProxyRequestSerializer(data=data, context={'request': request, 'handler': self}) 16 | serializer.is_valid(raise_exception=True) 17 | result = serializer.submit() 18 | return result 19 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/register.py: -------------------------------------------------------------------------------- 1 | from six.moves.urllib_parse import quote 2 | 3 | from jet_bridge_base import settings 4 | from jet_bridge_base.responses.base import Response 5 | from jet_bridge_base.responses.redirect import RedirectResponse 6 | from jet_bridge_base.status import HTTP_400_BAD_REQUEST 7 | from jet_bridge_base.views.base.api import BaseAPIView 8 | 9 | 10 | class RegisterView(BaseAPIView): 11 | 12 | def get(self, request, *args, **kwargs): 13 | if not settings.PROJECT: 14 | return Response('Project name is not set', status=HTTP_400_BAD_REQUEST) 15 | 16 | if not settings.TOKEN: 17 | return Response('Project token is not set', status=HTTP_400_BAD_REQUEST) 18 | 19 | token = request.get_argument('token', '') 20 | environment_type = settings.ENVIRONMENT_TYPE 21 | 22 | if settings.WEB_BASE_URL.startswith('https') and not request.full_url().startswith('https'): 23 | web_base_url = 'http{}'.format(settings.WEB_BASE_URL[5:]) 24 | else: 25 | web_base_url = settings.WEB_BASE_URL 26 | 27 | if settings.ENVIRONMENT: 28 | url = '{}/builder/{}/{}/resources/database/create/'.format(web_base_url, settings.PROJECT, settings.ENVIRONMENT) 29 | else: 30 | url = '{}/builder/{}/resources/database/create/'.format(web_base_url, settings.PROJECT) 31 | 32 | parameters = [ 33 | ['engine', settings.DATABASE_ENGINE], 34 | ['referrer', request.full_url().encode('utf8')], 35 | ] 36 | 37 | if token: 38 | parameters.append(['token', token]) 39 | 40 | if environment_type: 41 | parameters.append(['environment_type', environment_type]) 42 | 43 | query_string = '&'.join(map(lambda x: '{}={}'.format(x[0], quote(x[1])), parameters)) 44 | 45 | return RedirectResponse('%s#%s' % (url, query_string)) 46 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/reload.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.db import dispose_request_connection, get_request_connection 2 | from jet_bridge_base.db_types import remove_metadata_file 3 | from jet_bridge_base.permissions import HasProjectPermissions 4 | from jet_bridge_base.responses.json import JSONResponse 5 | from jet_bridge_base.utils.conf import get_conf 6 | from jet_bridge_base.views.base.api import APIView 7 | 8 | 9 | class ReloadView(APIView): 10 | permission_classes = (HasProjectPermissions,) 11 | 12 | def required_project_permission(self, request): 13 | return { 14 | 'permission_type': 'project', 15 | 'permission_object': 'project_settings', 16 | 'permission_actions': '' 17 | } 18 | 19 | def post(self, request, *args, **kwargs): 20 | conf = get_conf(request) 21 | remove_metadata_file(conf) 22 | 23 | result = dispose_request_connection(request) 24 | get_request_connection(request) 25 | 26 | return JSONResponse({ 27 | 'success': result 28 | }) 29 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/sql.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.exceptions.sql import SqlError 2 | from jet_bridge_base.exceptions.validation_error import ValidationError 3 | from jet_bridge_base.permissions import HasProjectPermissions 4 | from jet_bridge_base.responses.json import JSONResponse 5 | from jet_bridge_base.serializers.sql import SqlSerializer, SqlsSerializer 6 | from jet_bridge_base.utils.track_database import track_database_async 7 | from jet_bridge_base.views.base.api import APIView 8 | from jet_bridge_base.status import HTTP_400_BAD_REQUEST 9 | 10 | 11 | class SqlView(APIView): 12 | permission_classes = (HasProjectPermissions,) 13 | track_queries = True 14 | 15 | def post(self, request, *args, **kwargs): 16 | track_database_async(request) 17 | 18 | if 'queries' in request.data: 19 | serializer = SqlsSerializer(data=request.data, context={'request': request}) 20 | else: 21 | serializer = SqlSerializer(data=request.data, context={'request': request}) 22 | 23 | try: 24 | serializer.is_valid(raise_exception=True) 25 | except ValidationError as e: 26 | return JSONResponse({'error': str(e)}, status=HTTP_400_BAD_REQUEST) 27 | 28 | try: 29 | result = serializer.execute(serializer.validated_data) 30 | return JSONResponse(result) 31 | except SqlError as e: 32 | return JSONResponse({'error': str(e.detail)}, status=HTTP_400_BAD_REQUEST) 33 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/table.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from jet_bridge_base import status 4 | from jet_bridge_base.db import get_mapped_base, get_engine, reload_request_mapped_base 5 | from jet_bridge_base.encoders import JSONEncoder 6 | from jet_bridge_base.exceptions.not_found import NotFound 7 | from jet_bridge_base.exceptions.validation_error import ValidationError 8 | from jet_bridge_base.permissions import HasProjectPermissions 9 | from jet_bridge_base.responses.json import JSONResponse 10 | from jet_bridge_base.serializers.table import TableSerializer 11 | from jet_bridge_base.views.base.api import APIView 12 | from jet_bridge_base.views.table_column import map_dto_column 13 | from sqlalchemy import Table 14 | from sqlalchemy.exc import InternalError 15 | 16 | 17 | class TableView(APIView): 18 | permission_classes = (HasProjectPermissions,) 19 | 20 | def get_db(self, request): 21 | MappedBase = get_mapped_base(request) 22 | engine = get_engine(request) 23 | return MappedBase.metadata, engine 24 | 25 | def update_base(self, request): 26 | reload_request_mapped_base(request) 27 | 28 | def get_object(self, request): 29 | metadata, engine = self.get_db(request) 30 | pk = request.path_kwargs['pk'] 31 | 32 | try: 33 | obj = list(filter(lambda x: x.name == pk, metadata.tables.values()))[0] 34 | except IndexError: 35 | raise NotFound 36 | 37 | self.check_object_permissions(request, obj) 38 | 39 | return obj 40 | 41 | def create(self, request, *args, **kwargs): 42 | serializer = TableSerializer(data=request.data) 43 | serializer.is_valid(raise_exception=True) 44 | 45 | try: 46 | self.perform_create(request, serializer) 47 | except Exception as e: 48 | raise ValidationError(str(e)) 49 | 50 | return JSONResponse(serializer.representation_data, status=status.HTTP_201_CREATED) 51 | 52 | def perform_create(self, request, serializer): 53 | data = serializer.validated_data 54 | metadata, engine = self.get_db(request) 55 | 56 | data_source_meta = {} 57 | data_source_name = data.get('data_source_name') 58 | data_source_name_plural = data.get('data_source_name_plural') 59 | 60 | if data_source_name: 61 | data_source_meta['name'] = data_source_name 62 | 63 | if data_source_name_plural: 64 | data_source_meta['name_plural'] = data_source_name_plural 65 | 66 | if len(data_source_meta.keys()): 67 | comment = json.dumps(data_source_meta, cls=JSONEncoder) 68 | else: 69 | comment = None 70 | 71 | columns = list(map(lambda x: map_dto_column(data['name'], x, metadata), data['columns'])) 72 | table = Table( 73 | data['name'], 74 | metadata, 75 | *columns, 76 | comment=comment 77 | ) 78 | 79 | try: 80 | table.create(bind=engine) 81 | 82 | metadata._set_parent(table) 83 | self.update_base(request) 84 | except Exception as e: 85 | metadata.remove(table) 86 | raise e 87 | 88 | def destroy(self, request, *args, **kwargs): 89 | instance = self.get_object(request) 90 | self.perform_destroy(request, instance) 91 | return JSONResponse(status=status.HTTP_204_NO_CONTENT) 92 | 93 | def perform_destroy(self, request, table): 94 | metadata, engine = self.get_db(request) 95 | 96 | try: 97 | table.drop(bind=engine) 98 | except InternalError as e: 99 | raise ValidationError(str(e)) 100 | 101 | metadata.remove(table) 102 | self.update_base(request) 103 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/jet_bridge_base/views/trigger_exception.py: -------------------------------------------------------------------------------- 1 | from jet_bridge_base.permissions import AdministratorPermissions 2 | from jet_bridge_base.responses.json import JSONResponse 3 | from jet_bridge_base.views.base.api import BaseAPIView 4 | 5 | 6 | class TriggerExceptionView(BaseAPIView): 7 | permission_classes = (AdministratorPermissions,) 8 | 9 | def get(self, request, *args, **kwargs): 10 | division_by_zero = 1 / 0 11 | return JSONResponse({ 12 | 'result': True 13 | }) 14 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(fname): 6 | path = os.path.join(os.path.dirname(__file__), fname) 7 | try: 8 | file = open(path, encoding='utf-8') 9 | except TypeError: 10 | file = open(path) 11 | return file.read() 12 | 13 | 14 | def get_install_requires(): 15 | install_requires = [ 16 | 'sqlalchemy<2.0', 17 | 'pymongo[srv]==4.1.1', 18 | 'six', 19 | 'requests', 20 | 'Pillow', 21 | 'dateparser', 22 | 'python-dateutil', 23 | 'psutil', 24 | 'social-auth-core', 25 | 'prompt_toolkit==2.0.9', 26 | 'graphene<3.0', 27 | ] 28 | 29 | try: 30 | from collections import OrderedDict 31 | except ImportError: 32 | install_requires.append('ordereddict') 33 | 34 | return install_requires 35 | 36 | setup( 37 | name='jet-bridge-base', 38 | version=__import__('jet_bridge_base').VERSION, 39 | description='', 40 | long_description=read('README.md'), 41 | long_description_content_type='text/markdown', 42 | author='Denis Kildishev', 43 | author_email='support@jetadmin.io', 44 | url='https://github.com/jet-admin/jet-bridge-base', 45 | packages=find_packages(), 46 | license='MIT', 47 | classifiers=[ 48 | 49 | ], 50 | zip_safe=False, 51 | include_package_data=True, 52 | install_requires=get_install_requires() 53 | ) 54 | -------------------------------------------------------------------------------- /packages/jet_bridge_base/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | 4 | rm -rf dist/* 5 | python setup.py sdist bdist_wheel 6 | twine check dist/* 7 | twine upload dist/* 8 | rm -rf build/* 9 | -------------------------------------------------------------------------------- /packages/jet_django/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_django/LICENSE -------------------------------------------------------------------------------- /packages/jet_django/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | recursive-include jet_django * 4 | -------------------------------------------------------------------------------- /packages/jet_django/jet_django/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = '1.12.0' 2 | default_app_config = 'jet_django.apps.JetDjangoConfig' 3 | -------------------------------------------------------------------------------- /packages/jet_django/jet_django/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | from jet_bridge_base.logger import logger 4 | 5 | 6 | class JetDjangoConfig(AppConfig): 7 | name = 'jet_django' 8 | 9 | def ready(self): 10 | from jet_bridge_base import configuration 11 | from jet_django.configuration import JetDjangoConfiguration 12 | conf = JetDjangoConfiguration() 13 | configuration.set_configuration(conf) 14 | from jet_bridge_base.commands.check_token import check_token_command 15 | check_token_command('/jet_api/') 16 | from jet_bridge_base.db import connect_database_from_settings 17 | 18 | try: 19 | connect_database_from_settings() 20 | except Exception: 21 | logger.exception('Database from settings connection error') 22 | -------------------------------------------------------------------------------- /packages/jet_django/jet_django/migrations/0002_auto_20181014_2002.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11 on 2018-10-14 17:02 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('jet_django', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.DeleteModel( 16 | name='MenuSettings', 17 | ), 18 | migrations.DeleteModel( 19 | name='ModelDescription', 20 | ), 21 | migrations.DeleteModel( 22 | name='ViewSettings', 23 | ), 24 | migrations.RemoveField( 25 | model_name='widget', 26 | name='dashboard', 27 | ), 28 | migrations.DeleteModel( 29 | name='Dashboard', 30 | ), 31 | migrations.DeleteModel( 32 | name='Widget', 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /packages/jet_django/jet_django/migrations/0003_auto_20191007_2005.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.8 on 2019-10-07 17:05 2 | 3 | from django.db import connection, migrations 4 | 5 | 6 | def load(apps, schema_editor): 7 | with connection.cursor() as cursor: 8 | cursor.execute('SELECT token, date_add FROM jet_django_token') 9 | old_token = cursor.fetchone() 10 | 11 | if not old_token: 12 | return 13 | 14 | cursor.execute('SELECT id FROM __jet__token') 15 | new_token = cursor.fetchone() 16 | 17 | if new_token is None: 18 | cursor.execute('INSERT INTO __jet__token (token, date_add) VALUES (%s, %s)', [ 19 | old_token[0].hex, 20 | old_token[1] 21 | ]) 22 | else: 23 | cursor.execute('UPDATE __jet__token SET token = %s, date_add = %s WHERE id = %s', [ 24 | old_token[0].hex, 25 | old_token[1], 26 | new_token[0] 27 | ]) 28 | 29 | 30 | class Migration(migrations.Migration): 31 | 32 | dependencies = [ 33 | ('jet_django', '0002_auto_20181014_2002'), 34 | ] 35 | 36 | operations = [ 37 | migrations.AlterModelOptions( 38 | name='token', 39 | options={'managed': False, 'verbose_name': 'token', 'verbose_name_plural': 'tokens'}, 40 | ), 41 | migrations.RunPython(load, reverse_code=lambda a, b: ()), 42 | ] 43 | -------------------------------------------------------------------------------- /packages/jet_django/jet_django/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/packages/jet_django/jet_django/migrations/__init__.py -------------------------------------------------------------------------------- /packages/jet_django/jet_django/router.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | class Router(object): 5 | routes = [ 6 | { 7 | 'path': '/', 8 | 'regex': '(?P[^/]+)/', 9 | 'method_mapping': { 10 | 'get': 'retrieve', 11 | 'put': 'update', 12 | 'patch': 'partial_update', 13 | 'delete': 'destroy' 14 | }, 15 | 'detail': True 16 | }, 17 | { 18 | 'path': '', 19 | 'regex': '', 20 | 'method_mapping': { 21 | 'get': 'list', 22 | 'post': 'create' 23 | }, 24 | 'detail': False 25 | } 26 | ] 27 | urls = [] 28 | 29 | def add_handler(self, view, path_, regex, actions): 30 | class ActionHandler(view): 31 | pass 32 | 33 | for method, method_action in actions.items(): 34 | def create_action_method(action): 35 | def action_method(inner_self, *args, **kwargs): 36 | request = inner_self.get_request() 37 | request.action = action 38 | 39 | try: 40 | inner_self.view.action = action 41 | inner_self.before_dispatch(request) 42 | response = inner_self.view.dispatch(action, request, *args, **kwargs) 43 | return inner_self.write_response(response) 44 | except Exception: 45 | exc_type, exc, traceback = sys.exc_info() 46 | response = inner_self.view.error_response(request, exc_type, exc, traceback) 47 | return inner_self.write_response(response) 48 | finally: 49 | inner_self.on_finish(request) 50 | 51 | return action_method 52 | 53 | func = create_action_method(method_action) 54 | setattr(ActionHandler, method, func) 55 | 56 | try: 57 | from django.urls import path 58 | self.urls.append(path(path_, ActionHandler.as_view())) 59 | except ImportError: 60 | from django.conf.urls import url 61 | self.urls.append(url(regex, ActionHandler.as_view())) 62 | 63 | def add_route_actions(self, view, route, prefix_path, prefix_regex): 64 | viewset = view.view_cls 65 | actions = route['method_mapping'] 66 | actions = dict(filter(lambda x: hasattr(viewset, x[1]), actions.items())) 67 | 68 | if len(actions) == 0: 69 | return 70 | 71 | path = '{}{}'.format(prefix_path, route['path']) 72 | regex = '{}{}'.format(prefix_regex, route['regex']) 73 | self.add_handler(view, path, regex, actions) 74 | 75 | def add_route_extra_actions(self, view, route, prefix_path, prefix_regex): 76 | viewset = view.view_cls 77 | for attr in dir(viewset): 78 | method = getattr(viewset, attr) 79 | bind_to_methods = getattr(method, 'bind_to_methods', None) 80 | 81 | if bind_to_methods is None: 82 | continue 83 | 84 | detail = getattr(method, 'detail', None) 85 | 86 | if detail != route['detail']: 87 | continue 88 | 89 | extra_actions = dict(map(lambda x: (x, attr), bind_to_methods)) 90 | 91 | path = '{}{}{}/'.format(prefix_path, route['path'], attr) 92 | regex = '{}{}{}/'.format(prefix_regex, route['regex'], attr) 93 | self.add_handler(view, path, regex, extra_actions) 94 | 95 | def register(self, prefix_path, prefix_regex, view): 96 | for route in self.routes: 97 | self.add_route_extra_actions(view, route, prefix_path, prefix_regex) 98 | 99 | for route in self.routes: 100 | self.add_route_actions(view, route, prefix_path, prefix_regex) 101 | -------------------------------------------------------------------------------- /packages/jet_django/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "application.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /packages/jet_django/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(fname): 6 | path = os.path.join(os.path.dirname(__file__), fname) 7 | try: 8 | file = open(path, encoding='utf-8') 9 | except TypeError: 10 | file = open(path) 11 | return file.read() 12 | 13 | 14 | def get_install_requires(): 15 | install_requires = [ 16 | 'Django', 17 | 'requests', 18 | 'jet-bridge-base==1.12.0', 19 | ] 20 | 21 | try: 22 | from collections import OrderedDict 23 | except ImportError: 24 | install_requires.append('ordereddict') 25 | 26 | return install_requires 27 | 28 | setup( 29 | name='jet-django', 30 | version=__import__('jet_django').VERSION, 31 | description='', 32 | long_description=read('README.rst'), 33 | author='Denis Kildishev', 34 | author_email='hello@geex-arts.com', 35 | url='https://github.com/jet-admin/jet-django', 36 | packages=find_packages(), 37 | license='MIT', 38 | classifiers=[ 39 | 40 | ], 41 | zip_safe=False, 42 | include_package_data=True, 43 | install_requires=get_install_requires() 44 | ) 45 | -------------------------------------------------------------------------------- /packages/jet_django/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | 4 | rm -rf dist/* 5 | python setup.py sdist bdist_wheel 6 | twine check dist/* 7 | twine upload dist/* 8 | rm -rf build/* 9 | -------------------------------------------------------------------------------- /promotion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/promotion.png -------------------------------------------------------------------------------- /release-tasks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # for PostgreSQL 4 | pip install psycopg2-binary==2.8.3 5 | # for MySQL 6 | #apt update 7 | #apt install libmysqlclient-dev python-dev 8 | #pip install mysqlclient -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2-binary==2.8.3 2 | 3 | -e packages/jet_bridge_base 4 | -e packages/jet_bridge 5 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.5 2 | -------------------------------------------------------------------------------- /scripts/set_tags.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | 4 | JET_BRIDGE_BASE_VERSION="$(grep -e "VERSION" ../packages/jet_bridge_base/jet_bridge_base/__init__.py | awk '{ print substr($3, 2, length($3) - 2) }')" 5 | JET_BRIDGE_BASE_TAG="jet_bridge_base/${JET_BRIDGE_BASE_VERSION}" 6 | JET_BRIDGE_VERSION="$(grep -e "VERSION" ../packages/jet_bridge/jet_bridge/__init__.py | awk '{ print substr($3, 2, length($3) - 2) }')" 7 | JET_BRIDGE_TAG="jet_bridge/${JET_BRIDGE_VERSION}" 8 | JET_DJANGO_VERSION="$(grep -e "VERSION" ../packages/jet_django/jet_django/__init__.py | awk '{ print substr($3, 2, length($3) - 2) }')" 9 | JET_DJANGO_TAG="jet_django/${JET_DJANGO_VERSION}" 10 | 11 | GIT_DIR=../.git 12 | 13 | JET_BRIDGE_BASE_TAG_ADDED=false 14 | JET_BRIDGE_TAG_ADDED=false 15 | JET_DJANGO_TAG_ADDED=false 16 | 17 | if git rev-parse $JET_BRIDGE_BASE_TAG >/dev/null 2>&1 ; then 18 | echo "Tag is already added: ${JET_BRIDGE_BASE_TAG}" 19 | else 20 | git checkout --quiet master && git tag ${JET_BRIDGE_BASE_TAG} 21 | echo "Added tag: ${JET_BRIDGE_BASE_TAG}" 22 | JET_BRIDGE_BASE_TAG_ADDED=true 23 | fi 24 | 25 | if git rev-parse $JET_BRIDGE_TAG >/dev/null 2>&1 ; then 26 | echo "Tag is already added: ${JET_BRIDGE_TAG}" 27 | else 28 | git checkout --quiet master && git tag ${JET_BRIDGE_TAG} 29 | echo "Added tag: ${JET_BRIDGE_TAG}" 30 | JET_BRIDGE_TAG_ADDED=true 31 | fi 32 | 33 | if git rev-parse $JET_DJANGO_TAG >/dev/null 2>&1 ; then 34 | echo "Tag is already added: ${JET_DJANGO_TAG}" 35 | else 36 | git checkout --quiet master && git tag ${JET_DJANGO_TAG} 37 | echo "Added tag: ${JET_DJANGO_TAG}" 38 | JET_DJANGO_TAG_ADDED=true 39 | fi 40 | 41 | if [ "$JET_BRIDGE_BASE_TAG_ADDED" = false ] && [ "$JET_BRIDGE_TAG_ADDED" = false ] && [ "$JET_DJANGO_TAG_ADDED" = false ] ; then 42 | exit 0 43 | fi 44 | 45 | read -p "Do you want to push tags? [Y/n]" -n 1 -r 46 | echo 47 | 48 | if [[ $REPLY =~ ^[Yy]$ ]] ; then 49 | if [ "$JET_BRIDGE_BASE_TAG_ADDED" = true ] ; then 50 | git push origin ${JET_BRIDGE_BASE_TAG} 51 | fi 52 | 53 | if [ "$JET_BRIDGE_TAG_ADDED" = true ] ; then 54 | git push origin ${JET_BRIDGE_TAG} 55 | fi 56 | 57 | if [ "$JET_DJANGO_TAG_ADDED" = true ] ; then 58 | git push origin ${JET_DJANGO_TAG} 59 | fi 60 | fi 61 | -------------------------------------------------------------------------------- /scripts/upload_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname "$0")" 3 | 4 | ../packages/jet_bridge_base/upload.sh 5 | ../packages/jet_bridge/upload.sh 6 | ../packages/jet_django/upload.sh 7 | -------------------------------------------------------------------------------- /static/customize.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/customize.jpg -------------------------------------------------------------------------------- /static/customize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/customize.png -------------------------------------------------------------------------------- /static/dashboard.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/dashboard.jpeg -------------------------------------------------------------------------------- /static/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/dashboard.png -------------------------------------------------------------------------------- /static/export.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/export.jpeg -------------------------------------------------------------------------------- /static/filters.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/filters.jpeg -------------------------------------------------------------------------------- /static/filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/filters.png -------------------------------------------------------------------------------- /static/kanban.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/kanban.jpeg -------------------------------------------------------------------------------- /static/kanban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/kanban.png -------------------------------------------------------------------------------- /static/list.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/list.jpeg -------------------------------------------------------------------------------- /static/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/list.png -------------------------------------------------------------------------------- /static/overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/overview.gif -------------------------------------------------------------------------------- /static/promotion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/promotion.png -------------------------------------------------------------------------------- /static/promotion2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/promotion2.png -------------------------------------------------------------------------------- /static/segment.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/segment.jpeg -------------------------------------------------------------------------------- /static/segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/segment.png -------------------------------------------------------------------------------- /static/users.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/users.jpeg -------------------------------------------------------------------------------- /static/users.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jet-admin/jet-bridge/91999ae4c5feb5abe00466becfc53d37a32b5425/static/users.png --------------------------------------------------------------------------------