Source code for aiida.common.setup

# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved.                     #
# This file is part of the AiiDA code.                                    #
#                                                                         #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida_core #
# For further information on the license, see the LICENSE.txt file        #
# For further information please visit http://www.aiida.net               #
###########################################################################
"""Functions to interactively or non-interactively set up the AiiDA instance and a profile."""
from __future__ import division
from __future__ import absolute_import
from __future__ import print_function

import os

from six.moves import input

from aiida.common import exceptions
from aiida.manage.configuration import Profile

USE_TZ = True

# Keyword that is used in test profiles, databases and repositories to
# differentiate them from non-testing ones.
TEST_KEYWORD = 'test_'


[docs]def validate_email(email): """Validate an email address using Django's email validator. :param email: the email address :return: boolean, True if email address is valid, False otherwise """ from django.core.validators import validate_email as django_validate_email # pylint: disable=import-error,no-name-in-module from django import forms # pylint: disable=no-name-in-module try: django_validate_email(email) except forms.ValidationError: return False else: return True
[docs]def create_profile_noninteractive(config, profile_name='default', force_overwrite=False, **kwargs): """ Non-interactively creates a profile. :raises: a ValueError if the profile exists. :raises: a ValueError if one of the values not a valid input :param profile: The profile to be configured :param values: The configuration inputs :return: The populated profile that was also stored """ from aiida.manage.configuration.settings import DEFAULT_UMASK from aiida.manage.configuration import load_config config = load_config() try: existing_profile = config.get_profile(profile_name) except exceptions.ProfileConfigurationError: existing_profile = None if existing_profile and not force_overwrite: raise ValueError(('profile {} exists! Cannot non-interactively edit a profile.').format(profile_name)) profile = {} # setting backend backend_possibilities = ['django', 'sqlalchemy'] backend_v = kwargs.pop('backend') if backend_v in backend_possibilities: profile['AIIDADB_BACKEND'] = backend_v else: raise ValueError('{} is not a valid backend choice.'.format(backend_v)) # Setting email email_v = kwargs.pop('email') if validate_email(email_v): profile[Profile.KEY_DEFAULT_USER] = email_v else: raise ValueError('{} is not a valid email address.'.format(email_v)) # setting up db profile['AIIDADB_ENGINE'] = 'postgresql_psycopg2' profile['AIIDADB_HOST'] = kwargs.pop('db_host') profile['AIIDADB_PORT'] = kwargs.pop('db_port') profile['AIIDADB_NAME'] = kwargs.pop('db_name') profile['AIIDADB_USER'] = kwargs.pop('db_user') profile['AIIDADB_PASS'] = kwargs.pop('db_pass', '') # setting repo repo_v = kwargs.pop('repo') repo_path = os.path.expanduser(repo_v) if not os.path.isabs(repo_path): raise ValueError('The repository path must be an absolute path') if not os.path.isdir(repo_path): old_umask = os.umask(DEFAULT_UMASK) try: os.makedirs(repo_path) finally: os.umask(old_umask) profile['AIIDADB_REPOSITORY_URI'] = 'file://' + repo_path # Generate a new profile UUID or get it from the existing profile that is being overwritten if existing_profile: profile[Profile.KEY_PROFILE_UUID] = existing_profile.uuid else: profile[Profile.KEY_PROFILE_UUID] = Profile.generate_uuid() return profile
[docs]def create_profile(config, profile_name='default'): """ :param profile_name: The profile to be configured :return: The populated profile that was also stored. """ # pylint: disable=too-many-locals,too-many-statements,too-many-branches import click import readline from aiida.manage.configuration.settings import DEFAULT_UMASK, DEFAULT_AIIDA_USER print("Setting up profile {}.".format(profile_name)) is_test_profile = False if profile_name.startswith(TEST_KEYWORD): print("This is a test profile. All the data that will be stored under " "this profile are subjected to possible deletion or " "modification (repository and database data).") is_test_profile = True try: profile = config.get_profile(profile_name).dictionary except exceptions.ProfileConfigurationError: profile = {} profile_key_explanation = { "AIIDADB_ENGINE": "Database engine", "AIIDADB_PASS": "AiiDA Database password", "AIIDADB_NAME": "AiiDA Database name", "AIIDADB_HOST": "Database host", "AIIDADB_BACKEND": "AiiDA backend", "AIIDADB_PORT": "Database port", "AIIDADB_REPOSITORY_URI": "AiiDA repository directory", "AIIDADB_USER": "AiiDA Database user", Profile.KEY_DEFAULT_USER: "Default user email", Profile.KEY_PROFILE_UUID: "UUID that identifies the AiiDA profile", } # if there is an existing configuration, print it and ask if the user wants to modify it. updating_existing_prof = False if profile: print("The following configuration found corresponding to profile {}.".format(profile_name)) for key, value in profile.items(): if key in profile_key_explanation: print("{}: {}".format(profile_key_explanation.get(key), value)) else: print("{}: {}".format(key, value)) # If the user doesn't want to change it, we abandon if not click.confirm('Would you like to change it?'): return profile # Otherwise, we continue. updating_existing_prof = True this_new_confs = {} try: # Defining the backend to be used aiida_backend = profile.get('AIIDADB_BACKEND') if updating_existing_prof: print("The backend of already stored profiles can not be " "changed. The current backend is {}.".format(aiida_backend)) this_new_confs['AIIDADB_BACKEND'] = aiida_backend else: backend_possibilities = ['django', 'sqlalchemy'] if backend_possibilities: valid_aiida_backend = False while not valid_aiida_backend: backend_ans = input('AiiDA backend (available: {} - sqlalchemy is in beta mode): '.format( ', '.join(backend_possibilities))) if backend_ans in backend_possibilities: valid_aiida_backend = True else: print("* ERROR! Invalid backend inserted.") print("* The available middlewares are {}".format(', '.join(backend_possibilities))) this_new_confs['AIIDADB_BACKEND'] = backend_ans aiida_backend = backend_ans # Setting the email valid_email = False readline.set_startup_hook(lambda: readline.insert_text(profile.get(DEFAULT_AIIDA_USER))) while not valid_email: this_new_confs[Profile.KEY_DEFAULT_USER] = input('Default user email: ') valid_email = validate_email(this_new_confs[Profile.KEY_DEFAULT_USER]) if not valid_email: print("** Invalid email provided!") # Setting the database engine db_possibilities = [] if aiida_backend == 'django': db_possibilities.extend(['postgresql_psycopg2', 'mysql']) elif aiida_backend == 'sqlalchemy': db_possibilities.extend(['postgresql_psycopg2']) if db_possibilities: db_engine = profile.get('AIIDADB_ENGINE', db_possibilities[0]) readline.set_startup_hook(lambda: readline.insert_text(db_engine)) valid_db_engine = False while not valid_db_engine: db_engine_ans = input('Database engine (available: {} - mysql is deprecated): '.format( ', '.join(db_possibilities))) if db_engine_ans in db_possibilities: valid_db_engine = True else: print("* ERROR! Invalid database engine inserted.") print("* The available engines are {}".format(', '.join(db_possibilities))) this_new_confs['AIIDADB_ENGINE'] = db_engine_ans if 'postgresql_psycopg2' in this_new_confs['AIIDADB_ENGINE']: this_new_confs['AIIDADB_ENGINE'] = 'postgresql_psycopg2' old_host = profile.get('AIIDADB_HOST', 'localhost') if not old_host: old_host = 'localhost' readline.set_startup_hook(lambda: readline.insert_text(old_host)) this_new_confs['AIIDADB_HOST'] = input('PostgreSQL host: ') old_port = profile.get('AIIDADB_PORT', '5432') if not old_port: old_port = '5432' readline.set_startup_hook(lambda: readline.insert_text(old_port)) this_new_confs['AIIDADB_PORT'] = input('PostgreSQL port: ') readline.set_startup_hook(lambda: readline.insert_text(profile.get('AIIDADB_NAME'))) db_name = '' while True: db_name = input('AiiDA Database name: ') if is_test_profile and db_name.startswith(TEST_KEYWORD): break if (not is_test_profile and not db_name.startswith(TEST_KEYWORD)): break print("The test databases should start with the prefix {} and " "the non-test databases should not have this prefix.".format(TEST_KEYWORD)) this_new_confs['AIIDADB_NAME'] = db_name old_user = profile.get('AIIDADB_USER', 'aiida') if not old_user: old_user = 'aiida' readline.set_startup_hook(lambda: readline.insert_text(old_user)) this_new_confs['AIIDADB_USER'] = input('AiiDA Database user: ') readline.set_startup_hook(lambda: readline.insert_text(profile.get('AIIDADB_PASS'))) this_new_confs['AIIDADB_PASS'] = input('AiiDA Database password: ') elif 'mysql' in this_new_confs['AIIDADB_ENGINE']: this_new_confs['AIIDADB_ENGINE'] = 'mysql' old_host = profile.get('AIIDADB_HOST', 'localhost') if not old_host: old_host = 'localhost' readline.set_startup_hook(lambda: readline.insert_text(old_host)) this_new_confs['AIIDADB_HOST'] = input('mySQL host: ') old_port = profile.get('AIIDADB_PORT', '3306') if not old_port: old_port = '3306' readline.set_startup_hook(lambda: readline.insert_text(old_port)) this_new_confs['AIIDADB_PORT'] = input('mySQL port: ') readline.set_startup_hook(lambda: readline.insert_text(profile.get('AIIDADB_NAME'))) db_name = '' while True: db_name = input('AiiDA Database name: ') if is_test_profile and db_name.startswith(TEST_KEYWORD): break if (not is_test_profile and not db_name.startswith(TEST_KEYWORD)): break print("The test databases should start with the prefix {} and " "the non-test databases should not have this prefix.".format(TEST_KEYWORD)) this_new_confs['AIIDADB_NAME'] = db_name old_user = profile.get('AIIDADB_USER', 'aiida') if not old_user: old_user = 'aiida' readline.set_startup_hook(lambda: readline.insert_text(old_user)) this_new_confs['AIIDADB_USER'] = input('AiiDA Database user: ') readline.set_startup_hook(lambda: readline.insert_text(profile.get('AIIDADB_PASS'))) this_new_confs['AIIDADB_PASS'] = input('AiiDA Database password: ') else: raise ValueError("You have to specify a valid database (valid choices are 'mysql', 'postgres')") # This part for the time being is a bit oddly written # it should change in the future to add the possibility of having a # remote repository. Atm, I act as only a local repo is possible repository_dirpath = os.path.join(config.dirpath, 'repository/{}/'.format(profile_name)) existing_repo = profile.get('AIIDADB_REPOSITORY_URI', repository_dirpath) default_protocol = 'file://' if existing_repo.startswith(default_protocol): existing_repo = existing_repo[len(default_protocol):] readline.set_startup_hook(lambda: readline.insert_text(existing_repo)) new_repo_path = input('AiiDA repository directory: ') # Constructing the repo path new_repo_path = os.path.expanduser(new_repo_path) if not os.path.isabs(new_repo_path): raise ValueError("You must specify an absolute path") # Check if the new repository is a test repository and if it already exists. if is_test_profile: if TEST_KEYWORD not in os.path.basename(new_repo_path.rstrip('/')): raise ValueError("The repository directory for test profiles should " "contain the test keyword '{}'".format(TEST_KEYWORD)) if os.path.isdir(new_repo_path): print("The repository {} already exists. It will be used for " "tests. Any content may be deleted.".format(new_repo_path)) else: if TEST_KEYWORD in os.path.basename(new_repo_path): raise ValueError("The repository directory for non-test profiles cannot " "contain the test keyword '{}'".format(TEST_KEYWORD)) if not os.path.isdir(new_repo_path): print("The repository {} will be created.".format(new_repo_path)) old_umask = os.umask(DEFAULT_UMASK) try: os.makedirs(new_repo_path) finally: os.umask(old_umask) this_new_confs['AIIDADB_REPOSITORY_URI'] = 'file://' + new_repo_path # Add the profile uuid this_new_confs[Profile.KEY_PROFILE_UUID] = Profile.generate_uuid() return this_new_confs finally: readline.set_startup_hook(lambda: readline.insert_text(""))
[docs]def parse_repository_uri(repository_uri): """ This function validates the REPOSITORY_URI, that should be in the format protocol://address :note: At the moment, only the file protocol is supported. :return: a tuple (protocol, address). """ import uritools parts = uritools.urisplit(repository_uri) if parts.scheme != u'file': raise exceptions.ConfigurationError("The current AiiDA version supports only a local repository") if parts.scheme == u'file': if not os.path.isabs(parts.path): raise exceptions.ConfigurationError("The current repository is specified with a " "file protocol but with a relative path") # Normalize path to its absolute path return parts.scheme, os.path.expanduser(parts.path)