def init_alembic_config() -> None: """Create Alembic config""" try: ini_path = ax_misc.path("alembic.ini") this.alembic_cfg = Config(ini_path) alembic_folder = ax_misc.path("backend/alembic") this.alembic_cfg.set_main_option('script_location', str(alembic_folder)) except Exception: logger.exception('Error initating alembic config.') raise
def index(request, path=None): # pylint: disable=unused-variable """ This is MAIN ROUTE. (except other routes listed in this module). All requests are directed to Vue single page app. After that Vue handles routing.""" del request, path absolute_path = ax_misc.path('dist/ax/index.html') return response.html(open(absolute_path).read())
def sync_field_types(): """ Read field_types.yaml and check if any fields need to be created or modified """ field_types_yaml = ax_misc.path('field_types.yaml') with ax_model.scoped_session() as db_session: ax_field_types = db_session.query(AxFieldType).all() existing_tags = [] for field_type in ax_field_types: existing_tags.append(field_type.tag) if not os.path.isfile(field_types_yaml): raise FileNotFoundError( 'Configuration failed, field_types.yaml not found') with open(field_types_yaml, 'r') as stream: yaml_vars = yaml.safe_load(stream) for item in yaml_vars: if isinstance(item, dict) and 'tag' in item: tag = item['tag'] if tag in existing_tags: update_field_type(field_types=ax_field_types, item=item) else: create_field_type(db_session=db_session, item=item) db_session.commit()
async def draw_ax1(request): # pylint: disable=unused-variable """ Outputs bundle.js. Used when Ax web-components are inputed somewhere. Users can use this url for <script> tag """ del request absolute_path = ax_misc.path('dist/ax/static/js/ax-bundle.js') return await response.file( absolute_path, headers={ 'Content-Type': 'application/javascript; charset=utf-8' })
def init_logger(logs_filename: str, logs_absolute_path: str, logs_level: str): """Initiate loguru Args: logs_filename (str): Desired name of log file. If None, only console is used. logs_absolute_path (str): Path where log file must be created. If None - the /backend/logs folder will be used logs_level (str): What log level to use INFO/ERROR/DEBUG. Default ERROR """ # sys.stdout.reconfigure(encoding='utf-8') # sys.stdout.reconfigure(errors='backslashreplace') config = { "handlers": [{ 'sink': sys.stdout, 'colorize': True, 'format': 'Ax | {level} | <level>{message}</level>', 'backtrace': False, 'level': logs_level }], "extra": { "user": "******" } } # ⛏️ try: # If log_filename set in app.yaml, then we must write log file if logs_filename is not None: log_path = ax_misc.path('backend/logs/' + logs_filename) if logs_absolute_path is not None: log_path = str(Path(logs_absolute_path) / logs_filename) # TODO add rotation and retention, compression from app.yaml file_handler = { 'sink': log_path, 'serialize': True, 'rotation': '100 MB', 'enqueue': True, 'backtrace': False, 'level': logs_level } config['handlers'].append(file_handler) logger.configure(**config) except Exception: logger.exception('Error initiating logger.') raise
import backend.model as ax_model import main as ax_app def include_object(object, name, type_, reflected, compare_to): if (type_ == "table" and not str(name).startswith('_')): return False else: return True print('----------------------------------------------------') # Sets alembic configuration. migration script distanation and database url # Database contains alebnic _ax_alembic_version table with current version alembic_folder = ax_misc.path("backend/alembic") config = context.config config.set_main_option('script_location', str(alembic_folder)) fileConfig(config.config_file_name) ax_misc.load_configuration() ax_app.init_model() config.set_main_option('sqlalchemy.url', str(ax_model.db_url)) # target_metadata = ax_model.Base.metadata def run_migrations_offline(): """Run migrations in 'offline' mode. This configures the context with just a URL
def init_model(dialect: str, host: str, port: str, login: str, password: str, database: str, sqlite_filename: str, sqlite_absolute_path: str) -> None: """Initiate database model Args: dialect (str): Supported dialects: sqlite | mysql host (str): Database host port (str): Database port login (str): Database user password (str): Database user password database (str): Database schema name sqlite_filename (str): Sqlite database file name. (ax_sqlite.db) sqlite_absolute_path (str): Path to Sqlite file. If file does not exist it will be created """ try: if dialect == 'sqlite': if sqlite_absolute_path is None: db_path = ax_misc.path(sqlite_filename) this.db_url = 'sqlite:///' + str(db_path) else: db_path = str(Path(sqlite_absolute_path) / sqlite_filename) this.db_url = 'sqlite:///' + db_path logger.debug('DB url = {url}', url=this.db_url) # print(f'DB url = {this.db_url}') this.engine = create_engine(this.db_url, pool_threadlocal=True, connect_args={'timeout': 10}) # TODO take sql timeout from app.yaml elif dialect == 'postgre': this.db_url = ( f'postgresql+pypostgresql://{login}:{password}@{host}:{port}/{database}' ) logger.debug('DB url = {url}', url=this.db_url) this.engine = create_engine(this.db_url, convert_unicode=True, pool_size=30, pool_use_lifo=True, max_overflow=0, pool_recycle=3600, pool_pre_ping=True, connect_args={'connect_timeout': 10}) else: msg = 'This database dialect is not supported' logger.error(msg) raise Exception(msg) this.Session = sessionmaker(bind=this.engine) # poolclass=SingletonThreadPool, # this.db_session = scoped_session(sessionmaker(autocommit=False, # autoflush=False, # bind=this.engine)) # this.Base.query = db_session.query_property() except Exception: logger.exception('Error initating SqlAlchemy model') raise
import sys import base64 import uuid import shutil import json from pathlib import Path from sanic import response from sanic import Blueprint from sanic_jwt.decorators import inject_user from loguru import logger import backend.misc as ax_misc from backend.auth import ax_protected this = sys.modules[__name__] tus_bp = Blueprint('sanic_tus') upload_folder = ax_misc.path('tmp') if ax_misc.server_is_app_engine(): upload_folder = str(Path('/tmp')) upload_url = 'upload' tus_api_version = '1.0.0' tus_api_version_supported = '1.0.0' tus_api_extensions = ['creation', 'termination', 'file-check'] # tus_max_file_size = int(os.environ.get('AX_UPLOADS_MAX_FILESIZE', 4294967)) # 4294967296 # 4GByte def write_info(data): """ write <guid>.info file with upload info""" file_guid = data['guid'] info_path = os.path.join( this.upload_folder, file_guid, file_guid + '.info')
def init_routes(sanic_app, pages_path=None, ssl_enabled=False): # pylint: disable=unused-variable """Innitiate all Ax routes""" del ssl_enabled try: sanic_app.static('/uploads', str(ax_misc.path('uploads'))) sanic_app.static('/static', str(ax_misc.path('dist/ax/static'))) sanic_app.static('/stats', str(ax_misc.path('dist/ax/stats.html'))) sanic_app.static('/test_webpack', str(ax_misc.path('dist/ax/test.html'))) sanic_app.static('/editor.worker.js', str(ax_misc.path('dist/ax/editor.worker.js'))) sanic_app.static('/html.worker.js', str(ax_misc.path('dist/ax/html.worker.js'))) sanic_app.static('json.worker.js', str(ax_misc.path('dist/ax/json.worker.js'))) # Pages routes { pages_dist_path = str(ax_misc.path('dist/pages')) if pages_path: pages_dist_path = pages_path index_path = str(os.path.join(pages_dist_path, "index.html")) sanic_app.static('/pages/static', str(ax_misc.path('dist/pages/static'))) # TODO WTF this guid? maybe must be part of build? pre_cache_file_name = None pages_dist_dir = ax_misc.path('dist/pages') file_list = os.listdir(pages_dist_dir) for name in file_list: if "precache-manifest." in name: pre_cache_file_name = name sanic_app.static( f'/pages/{pre_cache_file_name}', str(ax_misc.path(f'dist/pages/{pre_cache_file_name}'))) # pylint: disable=line-too-long sanic_app.static('/pages/service-worker.js', str(ax_misc.path('dist/pages/service-worker.js'))) sanic_app.static('/pages/manifest.json', str(ax_misc.path('dist/pages/manifest.json'))) sanic_app.static('/pages/robots.txt', str(ax_misc.path('dist/pages/robots.txt'))) sanic_app.static('/pages', index_path) @sanic_app.route('/pages/<path:path>') def pages_index(request, path=None): # pylint: disable=unused-variable """ """ del request, path return response.html(open(index_path).read()) @sanic_app.route('/signin') def pages_index1(request, path=None): # pylint: disable=unused-variable """ """ del request, path return response.html(open(index_path).read()) # Pages routes } # Add tus upload blueprint sanic_app.blueprint(tus_bp) # Add web-socket subscription server subscription_server = WsLibSubscriptionServer(ax_schema.schema) @sanic_app.route('api/signout', methods=['GET']) @inject_user() @ax_protected() async def signout(request, user=None): # pylint: disable=unused-variable """ Delete all auth cookies and redirect to signin """ user_guid = user.get('user_id', None) if user else None if user_guid: key = f'refresh_token_{user_guid}' await ax_cache.cache.delete(key) to_url = '/signin' if request.args.get('to_admin', False): to_url = '/admin/signin' resp = response.redirect(to_url) del resp.cookies['ax_auth'] del resp.cookies['access_token'] del resp.cookies['refresh_token'] return resp @sanic_app.route( '/api/file/<form_guid>/<row_guid>/<field_guid>/<file_name>', methods=['GET']) @inject_user() @ax_protected() async def db_file_viewer( # pylint: disable=unused-variable request, form_guid, row_guid, field_guid, file_name, user): # pylint: disable=unused-variable """ Used to display files that are stored in database. Used in fields like AxImageCropDb""" current_user = user del request, form_guid, file_name with ax_model.scoped_session( "routes.db_file_viewer") as db_session: safe_row_guid = str(uuid.UUID(str(row_guid))) ax_field = db_session.query(AxField).filter( AxField.guid == uuid.UUID(field_guid)).first() # first we select only guid and axState to know, if user have # access row_result = await ax_dialects.dialect.select_one( db_session=db_session, form=ax_field.form, fields_list=[], row_guid=safe_row_guid) state_name = row_result[0]['axState'] state_guid = await ax_auth.get_state_guid( ax_form=ax_field.form, state_name=state_name) user_guid = current_user.get('user_id', None) if current_user else None user_is_admin = current_user.get( 'is_admin', False) if current_user else False allowed_field_dict = await ax_auth.get_allowed_fields_dict( ax_form=ax_field.form, user_guid=user_guid, state_guid=state_guid) field_guid = str(ax_field.guid) if not user_is_admin: if (field_guid not in allowed_field_dict or allowed_field_dict[field_guid] == 0 or allowed_field_dict[field_guid] is None): email = current_user.get('email', None) msg = (f'Error in db_file_viewer. ', f'not allowed for user [{email}]') logger.error(msg) return response.text("", status=403) field_value = await ax_dialects.dialect.select_field( db_session=db_session, form_db_name=ax_field.form.db_name, field_db_name=ax_field.db_name, row_guid=safe_row_guid) return response.raw(field_value, content_type='application/octet-stream') @sanic_app.route( '/api/file/<form_guid>/<row_guid>/<field_guid>/<file_guid>/<file_name>', # pylint: disable=line-too-long methods=['GET']) @inject_user() @ax_protected() async def file_viewer( # pylint: disable=unused-variable request, form_guid, row_guid, field_guid, file_guid, file_name,\ user=None): """ Used to display files uploaded and stored on disk. Displays temp files too. Used in all fields with upload""" del request current_user = user with ax_model.scoped_session( "routes -> file_viewer") as db_session: # if row_guid is null -> display from /tmp without permissions if not row_guid or row_guid == 'null': tmp_dir = os.path.join(ax_misc.tmp_root_dir, file_guid) file_name = os.listdir(tmp_dir)[0] temp_path = os.path.join(tmp_dir, file_name) return await response.file(temp_path) # get AxForm with row values ax_form = db_session.query(AxForm).filter( AxForm.guid == uuid.UUID(form_guid)).first() ax_form = await form_schema.set_form_values( db_session=db_session, ax_form=ax_form, row_guid=row_guid, current_user=current_user) # Get values from row, field field_values = None for field in ax_form.fields: if field.guid == uuid.UUID(field_guid): if field.value: field_values = json.loads(field.value) if type(field_values) is str: try: field_values = json.loads(field_values) except: pass # Find requested file in value the_file = None for file in field_values: if file['guid'] == file_guid: the_file = file if not the_file: return response.text("", status=404) state_guid = await ax_auth.get_state_guid( ax_form=ax_form, state_name=ax_form.current_state_name) user_guid = current_user.get('user_id', None) if current_user else None user_is_admin = current_user.get( 'is_admin', False) if current_user else False allowed_field_dict = await ax_auth.get_allowed_fields_dict( ax_form=ax_form, user_guid=user_guid, state_guid=state_guid) if not user_is_admin: if (field_guid not in allowed_field_dict or allowed_field_dict[field_guid] == 0 or allowed_field_dict[field_guid] is None): email = current_user['email'] msg = (f'Error in file_viewer. ', f'not allowed for user [{email}]') logger.error(msg) return response.text("", status=403) # if file exists -> return file row_guid_str = str(uuid.UUID(row_guid)) file_path = os.path.join(ax_misc.uploads_root_dir, 'form_row_field_file', form_guid, row_guid_str, field_guid, the_file['guid'], the_file['name']) if not os.path.lexists(file_path): return response.text("", status=404) return await response.file(file_path) @sanic_app.route('/admin/<path:path>') def index(request, path=None): # pylint: disable=unused-variable """ This is MAIN ROUTE. (except other routes listed in this module). All requests are directed to Vue single page app. After that Vue handles routing.""" del request, path absolute_path = ax_misc.path('dist/ax/index.html') return response.html(open(absolute_path).read()) @sanic_app.route('/form/<path:path>') def index1(request, path=None): # pylint: disable=unused-variable """ Copy of index. Sanic bug - https://github.com/huge-success/sanic/pull/1779""" del request, path absolute_path = ax_misc.path('dist/ax/index.html') return response.html(open(absolute_path).read()) @sanic_app.route('/grid/<path:path>') def index2(request, path=None): # pylint: disable=unused-variable """ Copy of index. Sanic bug - https://github.com/huge-success/sanic/pull/1779""" del request, path absolute_path = ax_misc.path('dist/ax/index.html') return response.html(open(absolute_path).read()) @sanic_app.route('/admin/signin') def index3(request, path=None): # pylint: disable=unused-variable """ Copy of index. Sanic bug - https://github.com/huge-success/sanic/pull/1779""" del request, path absolute_path = ax_misc.path('dist/ax/index.html') return response.html(open(absolute_path).read()) @sanic_app.route('/') def handle_request(request): # pylint: disable=unused-variable del request return response.redirect('/pages') @sanic_app.route('/api/draw_ax') async def draw_ax(request): # pylint: disable=unused-variable """ Outputs bundle.js. Used when Ax web-components are inputed somewhere. Users can use this url for <script> tag """ del request absolute_path = ax_misc.path('dist/ax/static/js/ax-bundle.js') return await response.file( absolute_path, headers={ 'Content-Type': 'application/javascript; charset=utf-8' }) @sanic_app.route('/draw_ax') async def draw_ax1(request): # pylint: disable=unused-variable """ Outputs bundle.js. Used when Ax web-components are inputed somewhere. Users can use this url for <script> tag """ del request absolute_path = ax_misc.path('dist/ax/static/js/ax-bundle.js') return await response.file( absolute_path, headers={ 'Content-Type': 'application/javascript; charset=utf-8' }) @sanic_app.websocket('/api/subscriptions', subprotocols=['graphql-ws']) async def subscriptions(request, web_socket): # pylint: disable=unused-variable """Web socket route for graphql subscriptions""" del request try: # TODO: Why socket error exception occurs without internet await subscription_server.handle(web_socket) return web_socket except asyncio.CancelledError: pass # logger.exception('Socket error') @sanic_app.route('/api/ping', methods=['GET', 'HEAD']) async def ping(request): # pylint: disable=unused-variable """ Ping function checks if Ax is running. Used with monit """ del request from backend.schema import schema result = schema.execute("query { ping }") test_str = json.dumps(result.data, sort_keys=True, indent=4) return response.text(test_str) @sanic_app.route('/api/blog_rss') async def blog_rss(request): # pylint: disable=unused-variable """ Fetches medium blog rss """ del request feed = await ax_misc.fetch('https://medium.com/feed/@enf644') # pylint: disable=line-too-long return response.text(feed) @sanic_app.route('/api/stackoverflow_rss') async def stackoverflow_rss(request): # pylint: disable=unused-variable """ Fetches stackoverflow tag rss """ del request feed = await ax_misc.fetch('https://stackexchange.com/feeds/tagsets/381786/ax-workflow?sort=active') # pylint: disable=line-too-long return response.text(feed) @sanic_app.route('/api/home_welcome') async def home_welcome(request): # pylint: disable=unused-variable """ Fetches welcome_free.md from ax-info """ del request the_url = 'https://raw.githubusercontent.com/enf644/ax-info/master/welcome_free.md' if ax_auth.lise_is_active(): the_url = 'https://raw.githubusercontent.com/enf644/ax-info/master/welcome.md' feed = await ax_misc.fetch(the_url) # pylint: disable=line-too-long html = markdown2.markdown(feed) return response.text(html) @sanic_app.route('/api/marketplace_featured') async def marketplace_featured(request): # pylint: disable=unused-variable """ Fetches featured apps json """ del request feed = await ax_misc.fetch('https://raw.githubusercontent.com/enf644/ax-info/master/featured_apps.json') # pylint: disable=line-too-long return response.text(feed) @sanic_app.route('/api/marketplace_all') async def marketplace_apps(request): # pylint: disable=unused-variable """ Fetches all apps json """ del request feed = await ax_misc.fetch('https://raw.githubusercontent.com/enf644/ax-info/master/apps.json') # pylint: disable=line-too-long return response.text(feed) @sanic_app.route('/api/test') async def test(request): # pylint: disable=unused-variable """Test function""" del request ret_str = await ax_migration.send_stats() # data = { # "host": "hostHost", # "usersNum": 10 # } # value_str = json.dumps(data).replace('"', '\\"') # query_str = ( # " mutation{" # " doAction(" # " formDbName: \"AxUsageStats\"" # " actionDbName: \"newStats\"" # f" values: \"{value_str}\"" # " ) {" # " ok" # " }" # " }" # ) # json_data = {'query': query_str} # ret_str = await ax_misc.post_json( # 'http://127.0.0.1:8080/api/graphql', json_data) # ret_str = ax_model.engine.pool.status() # this.test_schema = 'IT WORKS' # ax_pubsub.publisher.publish( # aiopubsub.Key('dummy_test'), this.test_schema) return response.text(ret_str) @sanic_app.route('/api/set') async def cache_set(request): # pylint: disable=unused-variable """Cache Test function""" del request obj = ['one', 'two', 'three'] await ax_cache.cache.set('user_list', obj) return response.text('Cache SET' + str(obj)) @sanic_app.route('/api/get') async def cache_get(request): # pylint: disable=unused-variable """Cache Test function""" del request obj = await ax_cache.cache.get('user_list') ret_str = 'READ cache == ' + \ str(obj[0].username + ' - ' + os.environ['AX_VERSION']) return response.text(ret_str) except Exception: logger.exception('Error initiating routes.') raise
def index3(request, path=None): # pylint: disable=unused-variable """ Copy of index. Sanic bug - https://github.com/huge-success/sanic/pull/1779""" del request, path absolute_path = ax_misc.path('dist/ax/index.html') return response.html(open(absolute_path).read())
async def mutate(self, info, **args): # pylint: disable=missing-docstring err = "home_schema -> DeleteForm" with ax_model.try_catch(info.context['session'], err) as db_session: guid = args.get('guid') ax_form = db_session.query(AxForm).filter( AxForm.guid == uuid.UUID(guid)).first() if not ax_form: return DeleteForm(forms=None, ok=False) # Remove objects connected to form - Ax1tomReference, AxRole2Users, # AxState2Role, AxAction2Role, AxRoleFieldPermission, AxRole, # AxAction, AxState, AxColumn, AxGrid, AxField db_session.query(Ax1tomReference).filter( Ax1tomReference.form_guid == ax_form.guid).delete() for role in ax_form.roles: db_session.query(AxRole2Users).filter( AxRole2Users.role_guid == role.guid).delete() db_session.query(AxState2Role).filter( AxState2Role.role_guid == role.guid).delete() db_session.query(AxAction2Role).filter( AxAction2Role.role_guid == role.guid).delete() db_session.query(AxRoleFieldPermission).filter( AxRoleFieldPermission.role_guid == role.guid).delete() db_session.delete(role) for action in ax_form.actions: db_session.delete(action) for state in ax_form.states: db_session.delete(state) for grid in ax_form.grids: for column in grid.columns: db_session.delete(column) db_session.delete(grid) for field in ax_form.fields: db_session.delete(field) await ax_dialects.dialect.drop_table(db_session=db_session, db_name=ax_form.db_name) db_session.delete(ax_form) db_session.flush() # s.execute("SET FOREIGN_KEY_CHECKS=1;") query = Form.get_query(info) # SQLAlchemy query form_list = query.all() # Remove all uploaded files for this form uploads_path = ax_misc.path('uploads/form_row_field_file') form_folder = os.path.join(uploads_path, str(ax_form.guid)) if os.path.exists(form_folder) is True: shutil.rmtree(form_folder) return DeleteForm(forms=form_list, ok=True)