def proper_name_validator(value, default): """ Validate proper_name user input. """ # Check for default if value == default: return True, value # Validate and sanitize user input proper_name_error_regex = re.compile(r'^[a-zA-Z0-9\s]+$') proper_name_warn_regex = re.compile(r'^[a-zA-Z0-9-\s_\"\']+$') if not proper_name_error_regex.match(value): # If offending characters are dashes, underscores or quotes, replace and notify user if proper_name_warn_regex.match(value): before = value value = value.replace('_', ' ') value = value.replace('-', ' ') value = value.replace('"', '') value = value.replace("'", "") with pretty_output(FG_YELLOW) as p: p.write('Warning: Illegal characters were detected in proper name "{0}". They have been replaced or ' 'removed with valid characters: "{1}"'.format(before, value)) # Otherwise, throw error else: with pretty_output(FG_RED) as p: p.write('Error: Proper name can only contain letters and numbers and spaces.') return False, value return True, value
def proper_name_validator(value, default): """ Validate proper_name user input. """ # Check for default if value == default: return True, value # Validate and sanitize user input proper_name_error_regex = re.compile(r'^[a-zA-Z0-9\s]+$') proper_name_warn_regex = re.compile(r'^[a-zA-Z0-9-\s_\"\']+$') if not proper_name_error_regex.match(value): # If offending characters are dashes, underscores or quotes, replace and notify user if proper_name_warn_regex.match(value): before = value value = value.replace('_', ' ') value = value.replace('-', ' ') value = value.replace('"', '') value = value.replace("'", "") with pretty_output(FG_YELLOW) as p: p.write( 'Warning: Illegal characters were detected in proper name "{0}". They have been replaced or ' 'removed with valid characters: "{1}"'.format( before, value)) # Otherwise, throw error else: with pretty_output(FG_RED) as p: p.write( 'Error: Proper name can only contain letters and numbers and spaces.' ) return False, value return True, value
def create_ps_database_setting(app_package, name, description='', required=False, initializer='', initialized=False, spatial=False, dynamic=False): from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_GREEN from tethys_apps.models import PersistentStoreDatabaseSetting from tethys_apps.models import TethysApp try: app = TethysApp.objects.get(package=app_package) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A Tethys App with the name "{}" does not exist. Aborted.'. format(app_package)) return False try: setting = PersistentStoreDatabaseSetting.objects.get(name=name) if setting: with pretty_output(FG_RED) as p: p.write( 'A PersistentStoreDatabaseSetting with name "{}" already exists. Aborted.' .format(name)) return False except ObjectDoesNotExist: pass try: ps_database_setting = PersistentStoreDatabaseSetting( tethys_app=app, name=name, description=description, required=required, initializer=initializer, initialized=initialized, spatial=spatial, dynamic=dynamic) ps_database_setting.save() with pretty_output(FG_GREEN) as p: p.write( 'PersistentStoreDatabaseSetting named "{}" for app "{}" created successfully!' .format(name, app_package)) return True except Exception as e: print(e) with pretty_output(FG_RED) as p: p.write('The above error was encountered. Aborted.'.format( app_package)) return False
def process_exception(self, request, exception): if hasattr(social_exceptions, exception.__class__.__name__): if isinstance(exception, social_exceptions.AuthCanceled): if request.user.is_anonymous: return redirect('accounts:login') else: return redirect('user:settings', username=request.user.username) elif isinstance(exception, social_exceptions.AuthAlreadyAssociated): blurb = 'The {0} account you tried to connect to has already been associated with another account.' with pretty_output(FG_WHITE) as p: p.write(exception.backend.name) if 'google' in exception.backend.name: blurb = blurb.format('Google') elif 'linkedin' in exception.backend.name: blurb = blurb.format('LinkedIn') elif 'hydroshare' in exception.backend.name: blurb = blurb.format('HydroShare') elif 'facebook' in exception.backend.name: blurb = blurb.format('Facebook') else: blurb = blurb.format('social') messages.success(request, blurb) if request.user.is_anonymous: return redirect('accounts:login') else: return redirect('user:settings', username=request.user.username) elif isinstance(exception, social_exceptions.NotAllowedToDisconnect): blurb = 'Unable to disconnect from this social account.' messages.success(request, blurb) if request.user.is_anonymous: return redirect('accounts:login') else: return redirect('user:settings', username=request.user.username)
def __init__(self, name, type, endpoint, apikey=None, username=None, password=None): """ Constructor """ self.name = name # Validate the types if type in VALID_SPATIAL_ENGINES: self.type = type self.engine = VALID_SPATIAL_ENGINES[type] else: spatial_engine_key_list = list(VALID_SPATIAL_ENGINES) if len(VALID_SPATIAL_ENGINES) > 2: comma_separated_types = ', '.join('"{0}"'.format(t) for t in spatial_engine_key_list[:-1]) last_type = '"{0}"'.format(spatial_engine_key_list[-1]) valid_types_string = '{0}, and {1}'.format(comma_separated_types, last_type) elif len(VALID_SPATIAL_ENGINES) == 2: valid_types_string = '"{0}" and "{1}"'.format(spatial_engine_key_list[0], spatial_engine_key_list[1]) else: valid_types_string = '"{0}"'.format(spatial_engine_key_list[0]) raise ValueError('The value "{0}" is not a valid for argument "type" of SpatialDatasetService.' ' Valid values for "type" argument include {1}.'.format(type, valid_types_string)) self.endpoint = endpoint self.apikey = apikey self.username = username self.password = password with pretty_output(FG_WHITE) as p: p.write('DEPRECATION WARNING: Storing connection credentials for Spatial Dataset Services ' 'in the app.py is a security leak. App configuration for Spatial Dataset Services ' 'will be deprecated in version 1.2.')
def test_pretty_output_fg_red(self, mock_print): act_msg = 'This is a test in RED' expected_string = '\x1b[31mThis is a test in RED\x1b[0m' with pretty_output(FG_RED) as p: p.write(act_msg) mock_print.assert_called_with(expected_string)
def test_pretty_output_fg_blue(self, mock_print): act_msg = 'This is a test in BLUE' expected_string = '\x1b[34mThis is a test in BLUE\x1b[0m' with pretty_output(FG_BLUE) as p: p.write(act_msg) mock_print.assert_called_with(expected_string)
def test_pretty_output_bold_fg_green(self, mock_print): act_msg = 'This is a bold text in green' expected_string = '\x1b[1m\x1b[32mThis is a bold text in green\x1b[0m' with pretty_output(BOLD, FG_GREEN) as p: p.write(act_msg) mock_print.assert_called_with(expected_string)
def test_pretty_output_bold_bg_green(self, mock_print): act_msg = 'This is a text with green background' expected_string = '\x1b[1m\x1b[42mThis is a text with green background\x1b[0m' with pretty_output(BOLD, BG_GREEN) as p: p.write(act_msg) mock_print.assert_called_with(expected_string)
def test_pretty_output_fg_green(self, mock_print): act_msg = 'This is a green text with ' + BOLD + 'bold' + END + ' text included' expected_string = '\x1b[32mThis is a green text with \x1b[1mbold\x1b[0m\x1b[32m text included\x1b[0m' with pretty_output(FG_GREEN) as p: p.write(act_msg) mock_print.assert_called_with(expected_string)
def _run_install(self): """ The definition of the "run" method for the CustomInstallCommand metaclass. """ # Get paths tethysapp_dir = get_tethysapp_directory() destination_dir = os.path.join(tethysapp_dir, self.app_package) # Notify user with pretty_output(FG_BLACK) as p: p.write('Copying App Package: {0} to {1}'.format(self.app_package_dir, destination_dir)) # Copy files try: shutil.copytree(self.app_package_dir, destination_dir) except Exception: try: shutil.rmtree(destination_dir) except Exception: os.remove(destination_dir) shutil.copytree(self.app_package_dir, destination_dir) # Install dependencies for dependency in self.dependencies: subprocess.call(['pip', 'install', dependency]) # Run the original install command install.run(self)
def _run_install(self): """ The definition of the "run" method for the CustomInstallCommand metaclass. """ # Get paths tethysapp_dir = get_tethysapp_directory() destination_dir = os.path.join(tethysapp_dir, self.app_package) # Notify user with pretty_output(FG_BLACK) as p: p.write('Copying App Package: {0} to {1}'.format( self.app_package_dir, destination_dir)) # Copy files try: shutil.copytree(self.app_package_dir, destination_dir) except Exception: try: shutil.rmtree(destination_dir) except Exception: os.remove(destination_dir) shutil.copytree(self.app_package_dir, destination_dir) # Install dependencies for dependency in self.dependencies: subprocess.call(['pip', 'install', dependency]) # Run the original install command install.run(self)
def remove_ps_database_setting(app_package, name, force=False): from tethys_apps.models import TethysApp from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_GREEN from tethys_apps.models import PersistentStoreDatabaseSetting try: app = TethysApp.objects.get(package=app_package) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A Tethys App with the name "{}" does not exist. Aborted.'. format(app_package)) return False try: setting = PersistentStoreDatabaseSetting.objects.get(tethys_app=app, name=name) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write( 'An PersistentStoreDatabaseSetting with the name "{}" for app "{}" does not exist. Aborted.' .format(name, app_package)) return False if not force: proceed = input( 'Are you sure you want to delete the ' 'PersistentStoreDatabaseSetting named "{}"? [y/n]: '.format(name)) while proceed not in ['y', 'n', 'Y', 'N']: proceed = input('Please enter either "y" or "n": ') if proceed in ['y', 'Y']: setting.delete() with pretty_output(FG_GREEN) as p: p.write( 'Successfully removed PersistentStoreDatabaseSetting with name "{0}"!' .format(name)) return True else: with pretty_output(FG_RED) as p: p.write('Aborted. PersistentStoreDatabaseSetting not removed.') else: setting.delete() with pretty_output(FG_GREEN) as p: p.write( 'Successfully removed PersistentStoreDatabaseSetting with name "{0}"!' .format(name)) return True
def _run_develop(self): """ The definition of the "run" method for the CustomDevelopCommand metaclass. """ # Get paths tethysapp_dir = get_tethysapp_directory() destination_dir = os.path.join(tethysapp_dir, self.app_package) # Notify user with pretty_output(FG_BLACK) as p: p.write('Creating Symbolic Link to App Package: {0} to {1}'.format( self.app_package_dir, destination_dir)) # Create symbolic link try: os_symlink = getattr(os, "symlink", None) if callable(os_symlink): os.symlink(self.app_package_dir, destination_dir) else: def symlink_ms(source, dest): csl = ctypes.windll.kernel32.CreateSymbolicLinkW csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) csl.restype = ctypes.c_ubyte flags = 1 if os.path.isdir(source) else 0 if csl(dest, source.replace('/', '\\'), flags) == 0: raise ctypes.WinError() os.symlink = symlink_ms symlink_ms(self.app_package_dir, destination_dir) except Exception as e: with pretty_output(FG_BLACK) as p: p.write(e) try: shutil.rmtree(destination_dir) except Exception: os.remove(destination_dir) os.symlink(self.app_package_dir, destination_dir) # Install dependencies for dependency in self.dependencies: subprocess.call(['pip', 'install', dependency]) # Run the original develop command develop.run(self)
def link_command(args): """ Interact with Tethys Services (Spatial/Persistent Stores) to create them and/or link them to existing apps """ try: service = args.service setting = args.setting service_parts = service.split(':') setting_parts = setting.split(':') service_type = None service_uid = None setting_app_package = None setting_type = None setting_uid = None try: service_type = service_parts[0] service_uid = service_parts[1] setting_app_package = setting_parts[0] setting_type = setting_parts[1] setting_uid = setting_parts[2] except IndexError: with pretty_output(FG_RED) as p: p.write( 'Incorrect argument format. \nUsage: "tethys link <spatial|persistent>:<service_id|service_name> ' '<app_package>:<setting_type (ps_database|ps_connection|ds_spatial)><setting_id|setting_name>"' '\nCommand aborted.') exit(1) success = link_service_to_app_setting(service_type, service_uid, setting_app_package, setting_type, setting_uid) if not success: exit(1) exit(0) except Exception as e: with pretty_output(FG_RED) as p: p.write(e) p.write('An unexpected error occurred. Please try again.') exit(1)
def test_pretty_output_empty_msg(self, mock_print): in_msg = BOLD + 'Use this' + END + ' even with ' + BOLD + FG_RED + 'no parameters' + \ END + ' in the with statement' expected_string = '\x1b[1mUse this\x1b[0m even with \x1b[1m\x1b[31mno parameters\x1b' \ '[0m in the with statement\x1b[0m' with pretty_output() as p: p.write(in_msg) mock_print.assert_called_with(expected_string)
def _run_develop(self): """ The definition of the "run" method for the CustomDevelopCommand metaclass. """ # Get paths tethysapp_dir = get_tethysapp_directory() destination_dir = os.path.join(tethysapp_dir, self.app_package) # Notify user with pretty_output(FG_BLACK) as p: p.write('Creating Symbolic Link to App Package: {0} to {1}'.format(self.app_package_dir, destination_dir)) # Create symbolic link try: os_symlink = getattr(os, "symlink", None) if callable(os_symlink): os.symlink(self.app_package_dir, destination_dir) else: def symlink_ms(source, dest): csl = ctypes.windll.kernel32.CreateSymbolicLinkW csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) csl.restype = ctypes.c_ubyte flags = 1 if os.path.isdir(source) else 0 if csl(dest, source.replace('/', '\\'), flags) == 0: raise ctypes.WinError() os.symlink = symlink_ms symlink_ms(self.app_package_dir, destination_dir) except Exception as e: with pretty_output(FG_BLACK) as p: p.write(e) try: shutil.rmtree(destination_dir) except Exception: os.remove(destination_dir) os.symlink(self.app_package_dir, destination_dir) # Install dependencies for dependency in self.dependencies: subprocess.call(['pip', 'install', dependency]) # Run the original develop command develop.run(self)
def create_ps_database_setting(app_package, name, description='', required=False, initializer='', initialized=False, spatial=False, dynamic=False): from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_GREEN from tethys_apps.models import PersistentStoreDatabaseSetting from tethys_apps.models import TethysApp try: app = TethysApp.objects.get(package=app_package) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A Tethys App with the name "{}" does not exist. Aborted.'.format(app_package)) return False try: setting = PersistentStoreDatabaseSetting.objects.get(name=name) if setting: with pretty_output(FG_RED) as p: p.write('A PersistentStoreDatabaseSetting with name "{}" already exists. Aborted.'.format(name)) return False except ObjectDoesNotExist: pass try: ps_database_setting = PersistentStoreDatabaseSetting( tethys_app=app, name=name, description=description, required=required, initializer=initializer, initialized=initialized, spatial=spatial, dynamic=dynamic ) ps_database_setting.save() with pretty_output(FG_GREEN) as p: p.write('PersistentStoreDatabaseSetting named "{}" for app "{}" created successfully!'.format(name, app_package)) return True except Exception as e: print(e) with pretty_output(FG_RED) as p: p.write('The above error was encountered. Aborted.'.format(app_package)) return False
def link_command(args): """ Interact with Tethys Services (Spatial/Persistent Stores) to create them and/or link them to existing apps """ try: service = args.service setting = args.setting service_parts = service.split(':') setting_parts = setting.split(':') service_type = None service_uid = None setting_app_package = None setting_type = None setting_uid = None try: service_type = service_parts[0] service_uid = service_parts[1] setting_app_package = setting_parts[0] setting_type = setting_parts[1] setting_uid = setting_parts[2] except IndexError: with pretty_output(FG_RED) as p: p.write( 'Incorrect argument format. \nUsage: "tethys link <spatial|persistent>:<service_id|service_name> ' '<app_package>:<setting_type (ps_database|ps_connection|ds_spatial)><setting_id|setting_name>"' '\nCommand aborted.') exit(1) success = link_service_to_app_setting(service_type, service_uid, setting_app_package, setting_type, setting_uid) if not success: exit(1) exit(0) except Exception as e: with pretty_output(FG_RED) as p: p.write(e) p.write('An unexpected error occurred. Please try again.') exit(1)
def __init__(self, name, endpoint, username=None, password=None): """ Constructor """ self.name = name self.endpoint = endpoint self.username = username self.password = password with pretty_output(FG_WHITE) as p: p.write('DEPRECATION WARNING: Storing connection credentials for WPS Services in the app.py is a security ' 'leak. App configuration for WPS Services will be deprecated in version 1.2.')
def remove_ps_database_setting(app_package, name, force=False): from tethys_apps.models import TethysApp from tethys_apps.cli.cli_colors import pretty_output, FG_RED, FG_GREEN from tethys_apps.models import PersistentStoreDatabaseSetting try: app = TethysApp.objects.get(package=app_package) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A Tethys App with the name "{}" does not exist. Aborted.'.format(app_package)) return False try: setting = PersistentStoreDatabaseSetting.objects.get(tethys_app=app, name=name) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('An PersistentStoreDatabaseSetting with the name "{}" for app "{}" does not exist. Aborted.' .format(name, app_package)) return False if not force: proceed = input('Are you sure you want to delete the ' 'PersistentStoreDatabaseSetting named "{}"? [y/n]: '.format(name)) while proceed not in ['y', 'n', 'Y', 'N']: proceed = input('Please enter either "y" or "n": ') if proceed in ['y', 'Y']: setting.delete() with pretty_output(FG_GREEN) as p: p.write('Successfully removed PersistentStoreDatabaseSetting with name "{0}"!'.format(name)) return True else: with pretty_output(FG_RED) as p: p.write('Aborted. PersistentStoreDatabaseSetting not removed.') else: setting.delete() with pretty_output(FG_GREEN) as p: p.write('Successfully removed PersistentStoreDatabaseSetting with name "{0}"!'.format(name)) return True
def __init__(self, name, endpoint, username=None, password=None): """ Constructor """ self.name = name self.endpoint = endpoint self.username = username self.password = password with pretty_output(FG_WHITE) as p: p.write( 'DEPRECATION WARNING: Storing connection credentials for WPS Services in the app.py is a security ' 'leak. App configuration for WPS Services will be deprecated in version 1.2.' )
def __init__(self, name, type, endpoint, apikey=None, username=None, password=None): """ Constructor """ self.name = name # Validate the types if type in VALID_SPATIAL_ENGINES: self.type = type self.engine = VALID_SPATIAL_ENGINES[type] else: spatial_engine_key_list = list(VALID_SPATIAL_ENGINES) if len(VALID_SPATIAL_ENGINES) > 2: comma_separated_types = ', '.join( '"{0}"'.format(t) for t in spatial_engine_key_list[:-1]) last_type = '"{0}"'.format(spatial_engine_key_list[-1]) valid_types_string = '{0}, and {1}'.format( comma_separated_types, last_type) elif len(VALID_SPATIAL_ENGINES) == 2: valid_types_string = '"{0}" and "{1}"'.format( spatial_engine_key_list[0], spatial_engine_key_list[1]) else: valid_types_string = '"{0}"'.format(spatial_engine_key_list[0]) raise ValueError( 'The value "{0}" is not a valid for argument "type" of SpatialDatasetService.' ' Valid values for "type" argument include {1}.'.format( type, valid_types_string)) self.endpoint = endpoint self.apikey = apikey self.username = username self.password = password with pretty_output(FG_WHITE) as p: p.write( 'DEPRECATION WARNING: Storing connection credentials for Spatial Dataset Services ' 'in the app.py is a security leak. App configuration for Spatial Dataset Services ' 'will be deprecated in version 1.2.')
def get_manage_path(args): """ Validate user defined manage path, use default, or throw error """ # Determine path to manage.py file manage_path = os.path.join(get_tethys_src_dir(), 'manage.py') # Check for path option if hasattr(args, 'manage'): manage_path = args.manage or manage_path # Throw error if path is not valid if not os.path.isfile(manage_path): with pretty_output(FG_RED) as p: p.write('ERROR: Can\'t open file "{0}", no such file.'.format(manage_path)) exit(1) return manage_path
def get_manage_path(args): """ Validate user defined manage path, use default, or throw error """ # Determine path to manage.py file manage_path = os.path.join(get_tethys_src_dir(), 'manage.py') # Check for path option if hasattr(args, 'manage'): manage_path = args.manage or manage_path # Throw error if path is not valid if not os.path.isfile(manage_path): with pretty_output(FG_RED) as p: p.write('ERROR: Can\'t open file "{0}", no such file.'.format( manage_path)) exit(1) return manage_path
def theme_color_validator(value, default): """ Validate theme_color user input. """ # Generate random color if default option provided if value == default: return True, get_random_color() # Validate hexadecimal if provided try: if len(value) > 0 and '#' in value: value = value[1:] int(value, 16) value = '#' + value return True, value except ValueError: with pretty_output(FG_RED) as p: p.write("Error: Value given is not a valid hexadecimal color.") return False, value
def theme_color_validator(value, default): """ Validate theme_color user input. """ # Generate random color if default option provided if value == default: return True, get_random_color() # Validate hexadecimal if provided try: if len(value) > 0 and '#' in value: value = value[1:] int(value, 16) value = '#' + value return True, value except ValueError: with pretty_output(FG_RED) as p: p.write("Error: Value given is not a valid hexadecimal color.") return False, value
def process_exception(self, request, exception): if hasattr(social_exceptions, exception.__class__.__name__): if isinstance(exception, social_exceptions.AuthCanceled): if request.user.is_anonymous: return redirect('accounts:login') else: return redirect('user:settings', username=request.user.username) elif isinstance(exception, social_exceptions.AuthAlreadyAssociated): blurb = 'The {0} account you tried to connect to has already been associated with another account.' with pretty_output(FG_WHITE) as p: p.write(exception.backend.name) if 'google' in exception.backend.name: blurb = blurb.format('Google') elif 'linkedin' in exception.backend.name: blurb = blurb.format('LinkedIn') elif 'hydroshare' in exception.backend.name: blurb = blurb.format('HydroShare') elif 'facebook' in exception.backend.name: blurb = blurb.format('Facebook') else: blurb = blurb.format('social') messages.success(request, blurb) if request.user.is_anonymous: return redirect('accounts:login') else: return redirect('user:settings', username=request.user.username) elif isinstance(exception, social_exceptions.NotAllowedToDisconnect): blurb = 'Unable to disconnect from this social account.' messages.success(request, blurb) if request.user.is_anonymous: return redirect('accounts:login') else: return redirect('user:settings', username=request.user.username)
def scaffold_command(args): """ Create a new Tethys app projects in the current directory. """ # Log log = logging.getLogger('tethys') # log.setLevel(logging.DEBUG) log.debug('Command args: {}'.format(args)) # Get template dirs log.debug('APP_PATH: {}'.format(APP_PATH)) log.debug('EXTENSION_PATH: {}'.format(EXTENSION_PATH)) # Get template root directory is_extension = False if args.extension: is_extension = True template_name = args.template template_root = os.path.join(EXTENSION_PATH, args.template) else: template_name = args.template template_root = os.path.join(APP_PATH, args.template) log.debug('Template root directory: {}'.format(template_root)) # Validate template if not os.path.isdir(template_root): with pretty_output(FG_WHITE) as p: p.write('Error: "{}" is not a valid template.'.format(template_name)) exit(1) # Validate project name project_name = args.name # Only lowercase contains_uppers = False for letter in project_name: if letter.isupper(): contains_uppers = True break if contains_uppers: before = project_name project_name = project_name.lower() with pretty_output(FG_YELLOW) as p: p.write('Warning: Uppercase characters in project name "{0}" ' 'changed to lowercase: "{1}".'.format(before, project_name)) # Check for valid characters name project_error_regex = re.compile(r'^[a-zA-Z0-9_]+$') project_warning_regex = re.compile(r'^[a-zA-Z0-9_-]+$') # Only letters, numbers and underscores allowed in app names if not project_error_regex.match(project_name): # If the only offending character is a dash, replace dashes with underscores and notify user if project_warning_regex.match(project_name): before = project_name project_name = project_name.replace('-', '_') with pretty_output(FG_YELLOW) as p: p.write('Warning: Dashes in project name "{0}" have been replaced ' 'with underscores "{1}"'.format(before, project_name)) # Otherwise, throw error else: with pretty_output(FG_YELLOW) as p: p.write('Error: Invalid characters in project name "{0}". ' 'Only letters, numbers, and underscores.'.format(project_name)) exit(1) # Project name derivatives project_dir = '{0}-{1}'.format(EXTENSION_PREFIX if is_extension else APP_PREFIX, project_name) split_project_name = project_name.split('_') title_case_project_name = [x.title() for x in split_project_name] default_proper_name = ' '.join(title_case_project_name) class_name = ''.join(title_case_project_name) default_theme_color = get_random_color() with pretty_output(FG_WHITE) as p: p.write('Creating new Tethys project named "{0}".'.format(project_dir)) # Get metadata from user if not is_extension: metadata_input = ( { 'name': 'proper_name', 'prompt': 'Proper name for the app (e.g.: "My First App")', 'default': default_proper_name, 'validator': proper_name_validator }, { 'name': 'description', 'prompt': 'Brief description of the app', 'default': '', 'validator': None }, { 'name': 'color', 'prompt': 'App theme color (e.g.: "#27AE60")', 'default': default_theme_color, 'validator': theme_color_validator }, { 'name': 'tags', 'prompt': 'Tags: Use commas to delineate tags and ' 'quotes around each tag (e.g.: "Hydrology","Reference Timeseries")', 'default': '', 'validator': None }, { 'name': 'author', 'prompt': 'Author name', 'default': '', 'validator': None }, { 'name': 'author_email', 'prompt': 'Author email', 'default': '', 'validator': None }, { 'name': 'license_name', 'prompt': 'License name', 'default': '', 'validator': None }, ) else: metadata_input = ( { 'name': 'proper_name', 'prompt': 'Proper name for the extension (e.g.: "My First Extension")', 'default': default_proper_name, 'validator': proper_name_validator }, { 'name': 'description', 'prompt': 'Brief description of the extension', 'default': '', 'validator': None }, { 'name': 'author', 'prompt': 'Author name', 'default': '', 'validator': None }, { 'name': 'author_email', 'prompt': 'Author email', 'default': '', 'validator': None }, { 'name': 'license_name', 'prompt': 'License name', 'default': '', 'validator': None }, ) # Build up template context context = { 'project': project_name, 'project_dir': project_dir, 'project_url': project_name.replace('_', '-'), 'class_name': class_name, 'proper_name': default_proper_name, 'description': '', 'color': default_theme_color, 'tags': '', 'author': '', 'author_email': '', 'license_name': '' } if not args.use_defaults: # Collect metadata input from user for item in metadata_input: valid = False response = item['default'] while not valid: try: response = input('{0} ["{1}"]: '.format(item['prompt'], item['default'])) or item['default'] except (KeyboardInterrupt, SystemExit): with pretty_output(FG_YELLOW) as p: p.write('\nScaffolding cancelled.') exit(1) if callable(item['validator']): valid, response = item['validator'](response, item['default']) else: valid = True if not valid: with pretty_output(FG_RED) as p: p.write('Invalid response: {}'.format(response)) context[item['name']] = response log.debug('Template context: {}'.format(context)) # Create root directory project_root = os.path.join(os.getcwd(), project_dir) log.debug('Project root path: {}'.format(project_root)) if os.path.isdir(project_root): if not args.overwrite: valid = False negative_choices = ['n', 'no', ''] valid_choices = ['y', 'n', 'yes', 'no'] default = 'y' response = '' while not valid: try: response = input('Directory "{}" already exists. ' 'Would you like to overwrite it? [Y/n]: '.format(project_root)) or default except (KeyboardInterrupt, SystemExit): with pretty_output(FG_YELLOW) as p: p.write('\nScaffolding cancelled.') exit(1) if response.lower() in valid_choices: valid = True if response.lower() in negative_choices: with pretty_output(FG_YELLOW) as p: p.write('Scaffolding cancelled.') exit(0) try: shutil.rmtree(project_root) except OSError: with pretty_output(FG_YELLOW) as p: p.write('Error: Unable to overwrite "{}". ' 'Please remove the directory and try again.'.format(project_root)) exit(1) # Walk the template directory, creating the templates and directories in the new project as we go template_context = Context(context) for curr_template_root, dirs, template_files in os.walk(template_root): # print(curr_template_root, dirs, files) curr_project_root = curr_template_root.replace(template_root, project_root) curr_project_root = render_path(curr_project_root, context) # Create Root Directory os.makedirs(curr_project_root) with pretty_output(FG_WHITE) as p: p.write('Created: "{}"'.format(curr_project_root)) # Create Files for template_file in template_files: template_file_path = os.path.join(curr_template_root, template_file) project_file = template_file.replace(TEMPLATE_SUFFIX, '') project_file_path = os.path.join(curr_project_root, project_file) template = None # Load the template log.debug('Loading template: "{}"'.format(template_file_path)) try: with open(template_file_path, 'r') as tfp: template = Template(tfp.read()) except UnicodeDecodeError: with open(template_file_path, 'br') as tfp: with open(project_file_path, 'bw') as pfp: pfp.write(tfp.read()) continue # Render template if loaded log.debug('Rendering template: "{}"'.format(template_file_path)) if template: with open(project_file_path, 'w') as pfp: pfp.write(template.render(template_context)) with pretty_output(FG_WHITE) as p: p.write('Created: "{}"'.format(project_file_path)) with pretty_output(FG_WHITE) as p: p.write('Successfully scaffolded new project "{}"'.format(project_name))
def scaffold_command(args): """ Create a new Tethys app projects in the current directory. """ # Log log = logging.getLogger('tethys') # log.setLevel(logging.DEBUG) log.debug('Command args: {}'.format(args)) # Get template dirs log.debug('APP_PATH: {}'.format(APP_PATH)) log.debug('EXTENSION_PATH: {}'.format(EXTENSION_PATH)) # Get template root directory is_extension = False if args.extension: is_extension = True template_name = args.template template_root = os.path.join(EXTENSION_PATH, args.template) else: template_name = args.template template_root = os.path.join(APP_PATH, args.template) log.debug('Template root directory: {}'.format(template_root)) # Validate template if not os.path.isdir(template_root): with pretty_output(FG_WHITE) as p: p.write( 'Error: "{}" is not a valid template.'.format(template_name)) exit(1) # Validate project name project_name = args.name # Only lowercase contains_uppers = False for letter in project_name: if letter.isupper(): contains_uppers = True break if contains_uppers: before = project_name project_name = project_name.lower() with pretty_output(FG_YELLOW) as p: p.write('Warning: Uppercase characters in project name "{0}" ' 'changed to lowercase: "{1}".'.format( before, project_name)) # Check for valid characters name project_error_regex = re.compile(r'^[a-zA-Z0-9_]+$') project_warning_regex = re.compile(r'^[a-zA-Z0-9_-]+$') # Only letters, numbers and underscores allowed in app names if not project_error_regex.match(project_name): # If the only offending character is a dash, replace dashes with underscores and notify user if project_warning_regex.match(project_name): before = project_name project_name = project_name.replace('-', '_') with pretty_output(FG_YELLOW) as p: p.write( 'Warning: Dashes in project name "{0}" have been replaced ' 'with underscores "{1}"'.format(before, project_name)) # Otherwise, throw error else: with pretty_output(FG_YELLOW) as p: p.write('Error: Invalid characters in project name "{0}". ' 'Only letters, numbers, and underscores.'.format( project_name)) exit(1) # Project name derivatives project_dir = '{0}-{1}'.format( EXTENSION_PREFIX if is_extension else APP_PREFIX, project_name) split_project_name = project_name.split('_') title_case_project_name = [x.title() for x in split_project_name] default_proper_name = ' '.join(title_case_project_name) class_name = ''.join(title_case_project_name) default_theme_color = get_random_color() with pretty_output(FG_WHITE) as p: p.write('Creating new Tethys project named "{0}".'.format(project_dir)) # Get metadata from user if not is_extension: metadata_input = ( { 'name': 'proper_name', 'prompt': 'Proper name for the app (e.g.: "My First App")', 'default': default_proper_name, 'validator': proper_name_validator }, { 'name': 'description', 'prompt': 'Brief description of the app', 'default': '', 'validator': None }, { 'name': 'color', 'prompt': 'App theme color (e.g.: "#27AE60")', 'default': default_theme_color, 'validator': theme_color_validator }, { 'name': 'tags', 'prompt': 'Tags: Use commas to delineate tags and ' 'quotes around each tag (e.g.: "Hydrology","Reference Timeseries")', 'default': '', 'validator': None }, { 'name': 'author', 'prompt': 'Author name', 'default': '', 'validator': None }, { 'name': 'author_email', 'prompt': 'Author email', 'default': '', 'validator': None }, { 'name': 'license_name', 'prompt': 'License name', 'default': '', 'validator': None }, ) else: metadata_input = ( { 'name': 'proper_name', 'prompt': 'Proper name for the extension (e.g.: "My First Extension")', 'default': default_proper_name, 'validator': proper_name_validator }, { 'name': 'description', 'prompt': 'Brief description of the extension', 'default': '', 'validator': None }, { 'name': 'author', 'prompt': 'Author name', 'default': '', 'validator': None }, { 'name': 'author_email', 'prompt': 'Author email', 'default': '', 'validator': None }, { 'name': 'license_name', 'prompt': 'License name', 'default': '', 'validator': None }, ) # Build up template context context = { 'project': project_name, 'project_dir': project_dir, 'project_url': project_name.replace('_', '-'), 'class_name': class_name, 'proper_name': default_proper_name, 'description': '', 'color': default_theme_color, 'tags': '', 'author': '', 'author_email': '', 'license_name': '' } if not args.use_defaults: # Collect metadata input from user for item in metadata_input: valid = False response = item['default'] while not valid: try: response = input('{0} ["{1}"]: '.format( item['prompt'], item['default'])) or item['default'] except (KeyboardInterrupt, SystemExit): with pretty_output(FG_YELLOW) as p: p.write('\nScaffolding cancelled.') exit(1) if callable(item['validator']): valid, response = item['validator'](response, item['default']) else: valid = True if not valid: with pretty_output(FG_RED) as p: p.write('Invalid response: {}'.format(response)) context[item['name']] = response log.debug('Template context: {}'.format(context)) # Create root directory project_root = os.path.join(os.getcwd(), project_dir) log.debug('Project root path: {}'.format(project_root)) if os.path.isdir(project_root): if not args.overwrite: valid = False negative_choices = ['n', 'no', ''] valid_choices = ['y', 'n', 'yes', 'no'] default = 'y' response = '' while not valid: try: response = input('Directory "{}" already exists. ' 'Would you like to overwrite it? [Y/n]: '. format(project_root)) or default except (KeyboardInterrupt, SystemExit): with pretty_output(FG_YELLOW) as p: p.write('\nScaffolding cancelled.') exit(1) if response.lower() in valid_choices: valid = True if response.lower() in negative_choices: with pretty_output(FG_YELLOW) as p: p.write('Scaffolding cancelled.') exit(0) try: shutil.rmtree(project_root) except OSError: with pretty_output(FG_YELLOW) as p: p.write('Error: Unable to overwrite "{}". ' 'Please remove the directory and try again.'.format( project_root)) exit(1) # Walk the template directory, creating the templates and directories in the new project as we go template_context = Context(context) for curr_template_root, dirs, template_files in os.walk(template_root): # print(curr_template_root, dirs, files) curr_project_root = curr_template_root.replace(template_root, project_root) curr_project_root = render_path(curr_project_root, context) # Create Root Directory os.makedirs(curr_project_root) with pretty_output(FG_WHITE) as p: p.write('Created: "{}"'.format(curr_project_root)) # Create Files for template_file in template_files: template_file_path = os.path.join(curr_template_root, template_file) project_file = template_file.replace(TEMPLATE_SUFFIX, '') project_file_path = os.path.join(curr_project_root, project_file) template = None # Load the template log.debug('Loading template: "{}"'.format(template_file_path)) try: with open(template_file_path, 'r') as tfp: template = Template(tfp.read()) except UnicodeDecodeError: with open(template_file_path, 'br') as tfp: with open(project_file_path, 'bw') as pfp: pfp.write(tfp.read()) continue # Render template if loaded log.debug('Rendering template: "{}"'.format(template_file_path)) if template: with open(project_file_path, 'w') as pfp: pfp.write(template.render(template_context)) with pretty_output(FG_WHITE) as p: p.write('Created: "{}"'.format(project_file_path)) with pretty_output(FG_WHITE) as p: p.write( 'Successfully scaffolded new project "{}"'.format(project_name))
def app_settings_list_command(args): from tethys_apps.models import (TethysApp, PersistentStoreConnectionSetting, PersistentStoreDatabaseSetting, SpatialDatasetServiceSetting) setting_type_dict = { PersistentStoreConnectionSetting: 'ps_connection', PersistentStoreDatabaseSetting: 'ps_database', SpatialDatasetServiceSetting: 'ds_spatial' } app_package = args.app try: app = TethysApp.objects.get(package=app_package) app_settings = [] for setting in PersistentStoreConnectionSetting.objects.filter(tethys_app=app): app_settings.append(setting) for setting in PersistentStoreDatabaseSetting.objects.filter(tethys_app=app): app_settings.append(setting) for setting in SpatialDatasetServiceSetting.objects.filter(tethys_app=app): app_settings.append(setting) unlinked_settings = [] linked_settings = [] for setting in app_settings: if hasattr(setting, 'spatial_dataset_service') and setting.spatial_dataset_service \ or hasattr(setting, 'persistent_store_service') and setting.persistent_store_service: linked_settings.append(setting) else: unlinked_settings.append(setting) with pretty_output(BOLD) as p: p.write("\nUnlinked Settings:") if len(unlinked_settings) == 0: with pretty_output() as p: p.write('None') else: is_first_row = True for setting in unlinked_settings: if is_first_row: with pretty_output(BOLD) as p: p.write('{0: <10}{1: <40}{2: <15}'.format('ID', 'Name', 'Type')) is_first_row = False with pretty_output() as p: p.write('{0: <10}{1: <40}{2: <15}'.format(setting.pk, setting.name, setting_type_dict[type(setting)])) with pretty_output(BOLD) as p: p.write("\nLinked Settings:") if len(linked_settings) == 0: with pretty_output() as p: p.write('None') else: is_first_row = True for setting in linked_settings: if is_first_row: with pretty_output(BOLD) as p: p.write('{0: <10}{1: <40}{2: <15}{3: <20}'.format('ID', 'Name', 'Type', 'Linked With')) is_first_row = False service_name = setting.spatial_dataset_service.name if hasattr(setting, 'spatial_dataset_service') \ else setting.persistent_store_service.name print('{0: <10}{1: <40}{2: <15}{3: <20}'.format(setting.pk, setting.name, setting_type_dict[type(setting)], service_name)) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('The app you specified ("{0}") does not exist. Command aborted.'.format(app_package)) except Exception as e: with pretty_output(FG_RED) as p: p.write(e) p.write('Something went wrong. Please try again.')
def link_service_to_app_setting(service_type, service_uid, app_package, setting_type, setting_uid): """ Links a Tethys Service to a TethysAppSetting. :param service_type: The type of service being linked to an app. Must be either 'spatial' or 'persistent'. :param service_uid: The name or id of the service being linked to an app. :param app_package: The package name of the app whose setting is being linked to a service. :param setting_type: The type of setting being linked to a service. Must be one of the following: 'ps_database', 'ps_connection', or 'ds_spatial'. :param setting_uid: The name or id of the setting being linked to a service. :return: True if successful, False otherwise. """ from tethys_apps.cli.cli_colors import pretty_output, FG_GREEN, FG_RED from tethys_sdk.app_settings import (SpatialDatasetServiceSetting, PersistentStoreConnectionSetting, PersistentStoreDatabaseSetting) from tethys_services.models import (SpatialDatasetService, PersistentStoreService) from tethys_apps.models import TethysApp service_type_to_model_dict = { 'spatial': SpatialDatasetService, 'persistent': PersistentStoreService } setting_type_to_link_model_dict = { 'ps_database': { 'setting_model': PersistentStoreDatabaseSetting, 'service_field': 'persistent_store_service' }, 'ps_connection': { 'setting_model': PersistentStoreConnectionSetting, 'service_field': 'persistent_store_service' }, 'ds_spatial': { 'setting_model': SpatialDatasetServiceSetting, 'service_field': 'spatial_dataset_service' } } service_model = service_type_to_model_dict[service_type] try: try: service_uid = int(service_uid) service = service_model.objects.get(pk=service_uid) except ValueError: service = service_model.objects.get(name=service_uid) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A {0} with ID/Name "{1}" does not exist.'.format(str(service_model), service_uid)) return False try: app = TethysApp.objects.get(package=app_package) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A Tethys App with the name "{}" does not exist. Aborted.'.format(app_package)) return False try: linked_setting_model_dict = setting_type_to_link_model_dict[setting_type] except KeyError: with pretty_output(FG_RED) as p: p.write('The setting_type you specified ("{0}") does not exist.' '\nChoose from: "ps_database|ps_connection|ds_spatial"'.format(setting_type)) return False linked_setting_model = linked_setting_model_dict['setting_model'] linked_service_field = linked_setting_model_dict['service_field'] try: try: setting_uid = int(setting_uid) setting = linked_setting_model.objects.get(tethys_app=app, pk=setting_uid) except ValueError: setting = linked_setting_model.objects.get(tethys_app=app, name=setting_uid) setattr(setting, linked_service_field, service) setting.save() with pretty_output(FG_GREEN) as p: p.write('{} with name "{}" was successfully linked to "{}" with name "{}" of the "{}" Tethys App' .format(str(service_model), service_uid, linked_setting_model, setting_uid, app_package)) return True except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A {0} with ID/Name "{1}" does not exist.'.format(str(linked_setting_model), setting_uid)) return False
def link_service_to_app_setting(service_type, service_uid, app_package, setting_type, setting_uid): """ Links a Tethys Service to a TethysAppSetting. :param service_type: The type of service being linked to an app. Must be either 'spatial' or 'persistent'. :param service_uid: The name or id of the service being linked to an app. :param app_package: The package name of the app whose setting is being linked to a service. :param setting_type: The type of setting being linked to a service. Must be one of the following: 'ps_database', 'ps_connection', or 'ds_spatial'. :param setting_uid: The name or id of the setting being linked to a service. :return: True if successful, False otherwise. """ from tethys_apps.cli.cli_colors import pretty_output, FG_GREEN, FG_RED from tethys_sdk.app_settings import (SpatialDatasetServiceSetting, PersistentStoreConnectionSetting, PersistentStoreDatabaseSetting) from tethys_services.models import (SpatialDatasetService, PersistentStoreService) from tethys_apps.models import TethysApp service_type_to_model_dict = { 'spatial': SpatialDatasetService, 'persistent': PersistentStoreService } setting_type_to_link_model_dict = { 'ps_database': { 'setting_model': PersistentStoreDatabaseSetting, 'service_field': 'persistent_store_service' }, 'ps_connection': { 'setting_model': PersistentStoreConnectionSetting, 'service_field': 'persistent_store_service' }, 'ds_spatial': { 'setting_model': SpatialDatasetServiceSetting, 'service_field': 'spatial_dataset_service' } } service_model = service_type_to_model_dict[service_type] try: try: service_uid = int(service_uid) service = service_model.objects.get(pk=service_uid) except ValueError: service = service_model.objects.get(name=service_uid) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A {0} with ID/Name "{1}" does not exist.'.format(str(service_model), service_uid)) return False try: app = TethysApp.objects.get(package=app_package) except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A Tethys App with the name "{}" does not exist. Aborted.'.format(app_package)) return False try: linked_setting_model_dict = setting_type_to_link_model_dict[setting_type] except KeyError: with pretty_output(FG_RED) as p: p.write('The setting_type you specified ("{0}") does not exist.' '\nChoose from: "ps_database|ps_connection|ds_spatial"'.format(setting_type)) return False linked_setting_model = linked_setting_model_dict['setting_model'] linked_service_field = linked_setting_model_dict['service_field'] try: try: setting_uid = int(setting_uid) setting = linked_setting_model.objects.get(tethys_app=app, pk=setting_uid) except ValueError: setting = linked_setting_model.objects.get(tethys_app=app, name=setting_uid) setattr(setting, linked_service_field, service) setting.save() with pretty_output(FG_GREEN) as p: p.write('{} with name "{}" was successfully linked to "{}" with name "{}" of the "{}" Tethys App' .format(str(service_model), service_uid, linked_setting_model, setting_uid, app_package)) return True except ObjectDoesNotExist: with pretty_output(FG_RED) as p: p.write('A {0} with ID/Name "{1}" does not exist.'.format(str(linked_setting_model), setting_uid)) return False