diff --git a/.isort.cfg b/.isort.cfg index fbbfe22..9eead09 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,4 +2,4 @@ line_length=120 multi_line_output=3 default_section=THIRDPARTY -skip=.eggs,egg-info,builds,dist,dev.py +skip=.eggs,egg-info,builds,dist,dev.py,.tox,scripts diff --git a/README.rst b/README.rst index d081c00..ef1d28f 100644 --- a/README.rst +++ b/README.rst @@ -53,6 +53,34 @@ The instance name will be set as default and used in all CLI commands. If you want to override this setting for a specific command, use --instance-name eg:: syncano sync --instance-name new-instance-1234 pull + +If you need to change default instance name, used for all future commands, use:: + + syncano default name_of_new_default_instance + + +If you do not have an Syncano account use `syncano init` command:: + + syncano init + +And follow the steps. CLI will ask you about `email` and `password`, it will also create an Instance for you. +After `syncano init` you can start with getting the list of your Instances:: + + syncano instances list + + +To obtain a help, type:: + + syncano --help + +To display a help for specific command, type:: + + syncano instances --help + +And:: + + syncano instances list --help + Documentation ============= @@ -107,8 +135,8 @@ Will display custom sockets from `my_instance_name` - because it is set to be a After a registration - there's no default instance set. So it's desired to create one and set it as default:: - syncano instance create my_new_instance - syncano instance default my_new_instance + syncano instances create my_new_instance + syncano instances default my_new_instance It's worth to note that `instance_name` must be unique - but you will get appropriate message if you encounter such case. @@ -259,19 +287,19 @@ Syncano Hosting Syncano Hosting is a simple way to host your static files on Syncano servers. The CLI supports it in the following way: -This command will list files for currently hosted website:: +This command will list currently defined hostings in the instance:: syncano hosting list +This command will list files for currently hosted website (for `default` hosting):: + + syncano hosting list files + This command will publish all files inside ** to the default Syncano Hosting instance. When publishing the whole directory, the structure will be mapped on Syncano.:: syncano hosting publish -This command will unpublish currently published hosting:: - - syncano hosting unpublish - This command will permamently delete the hosting:: @@ -285,6 +313,14 @@ This command will update single file:: syncano hosting update hosting/file/path local/file/path +For each of the above command you can specify the domain to change just after hosting command, example:: + + syncano hosting --domain staging publish + +Will create a new hosting which will be available under: `--staging.syncano.site` +If this hosting is also a default one, it will be available under: `.syncano.site`. + + Custom Sockets ============== @@ -311,6 +347,10 @@ Display chosen Custom Socket details:: syncano sockets details socket_name +Display Custom Socket config (with name: `socket_name`):: + + syncano sockets config socket_name + Delete a Custom Socket:: syncano sockets delete socket_name @@ -323,7 +363,7 @@ Create a template from an existing Custom Socket:: syncano sockets template /path/to/out --socket socket_name -Run endpoint defined in Custom Socket::s +Run endpoint defined in Custom Socket:: syncano sockets run socket_name/endpoint_name @@ -331,6 +371,7 @@ Run endpoint providing POST data:: syncano sockets run socket_name/my_endpoint_12 POST -d one=1 + In all of the above cases you can override the Syncano instance being used:: --instance-name my_instance_name diff --git a/circle.yml b/circle.yml index cab6c0c..2f63c54 100644 --- a/circle.yml +++ b/circle.yml @@ -1,18 +1,18 @@ machine: python: - version: 2.7.5 + version: 2.7.10 dependencies: pre: - pip install -U setuptools + - pip install -r requirements-tests.txt + post: + - pyenv local 3.4.3 2.7.10 test: override: - - pip install -r requirements-tests.txt - - flake8 . - - isort --recursive --check-only . - python setup.py check -s --restructuredtext - - python setup.py test + - tox deployment: production: diff --git a/docs/custom_sockets/docs.md b/docs/custom_sockets/docs.md index f0996fc..e5395fc 100644 --- a/docs/custom_sockets/docs.md +++ b/docs/custom_sockets/docs.md @@ -7,6 +7,13 @@ author: name: Sebastian email: sebastian@syncano.com + config: + constants: + secret_key: value + prompt: + user_key: + type: string + description: A Syncano user key icon: name: icon_name color: red @@ -34,6 +41,20 @@ runtime_name: python_library_v5.0 file: scripts/script3.py + classes: + country: + schema: + - name: name + type: string + - name: topLevelDomain + type: string + - name: capital + type: string + - name: alpha2Code + type: string + - name: alpha3Code + type: string + ### YAML file structure explanation * `name` is the name of your new Custom Socket - this should be unique; @@ -46,6 +67,8 @@ * can be found in `metadata` field on Custom Socket in Syncano Dasboard. * `icon` is metadata information about your Custom Socket - it stores the icon name used and its color (used in Syncano Dashboard) * `endpoints` - definition of the endpoints in a Custom Socket; Currently supported endpoints can be only of `script` type. +* `config` - stores the metadata about custom socket configuration; constants are config variables that are passed one-to-one + from yaml file definitions; the `prompt` config section - this variables will be requested from user during installation. Consider this example: @@ -76,8 +99,8 @@ The difference is that we now define what happens for the different HTTP methods. When the GET HTTP method is used, `script_endpoint_3` script endpoint will be run. When the POST HTTP method is used - `script_endpoint_2` endpoint will be executed. - Currently only Script Endpoints are supported, which run scripts under the hood. But don't worry, - we are working on adding more options! + Currently Script Endpoints and Classes are supported, which run scripts under the hood. + We are working on adding more options! * `dependencies` - the definition of your Custom Socket dependencies. They define all dependency objects which will be called when the endpoint is requested. @@ -90,7 +113,7 @@ which will be called when the endpoint is requested. runtime_name: python_library_v5.0 file: scripts/script1.py - Above YAML snippet defines one dependency: + Above YAML snippet defines four dependencies (three of type `script` and one of type `class`): * `script` - type of the dependency (defined using `scripts` keyword). * `script_endpoint_1` - name of the dependency; it's an important element, because that's the place where you connect a dependency to an endpoint. * `runtime_name` is name of the runtime used in a script; @@ -99,6 +122,26 @@ which will be called when the endpoint is requested. It should be noted that when defining Custom Scripts, we suggest following some basic directory structure- for better work organization. We recommend storing scripts under the `scripts` directory - this is why the filename is a relative path: `scripts/script1.py`. Of course your can also follow your own rules, e.g. by using a flat file structure. + + The class dependency looks as follows: + + classes: + country: + schema: + - name: name + type: string + - name: topLevelDomain + type: string + - name: capital + type: string + - name: alpha2Code + type: string + - name: alpha3Code + type: string + + This simple mean that Custom Socket requires a class `country` to work properly. Under the hood - Syncano Platform + will check if this class exists (if not - create it) and ensure that all required files defined in `schema` are present. + ## Custom Socket directory structure diff --git a/requirements-tests.txt b/requirements-tests.txt index 5dcf92a..52865d9 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,3 +1,4 @@ +tox==2.3.1 flake8==2.4.1 isort==4.0.0 mock==1.3.0 diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..5917669 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +flake8 . +isort --recursive --check-only . + +python setup.py test diff --git a/setup.py b/setup.py index e42234a..bb05cf9 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name='syncano-cli', - version='0.7', + version='0.8', description='Syncano command line utilities', long_description=README, author='Marcin Swiderski, Sebastian Opalczynski', @@ -14,7 +14,7 @@ url='https://github.com/Syncano/syncano-cli', packages=find_packages(), license='MIT', - install_requires=['syncano>=5.4.2', 'PyYaml>=3.11', 'watchdog>=0.8.3', 'click>=6.6'], + install_requires=['syncano>=5.4.4', 'PyYaml>=3.11', 'watchdog>=0.8.3', 'click>=6.6'], test_suite='tests', entry_points=""" [console_scripts] diff --git a/syncano_cli/account/command.py b/syncano_cli/account/command.py index 1ac1ad5..b779782 100644 --- a/syncano_cli/account/command.py +++ b/syncano_cli/account/command.py @@ -10,7 +10,7 @@ def __init__(self, config_path): self.connection = syncano.connect() self.config_path = config_path - def register(self, email, password, first_name, last_name, invitation_key): + def register(self, email, password, first_name=None, last_name=None, invitation_key=None): api_key = self.connection.connection().register( email=email, password=password, @@ -19,6 +19,6 @@ def register(self, email, password, first_name, last_name, invitation_key): invitation_key=invitation_key ) - ACCOUNT_CONFIG.set('DEFAULT', 'api_key', api_key) - with open(self.config_path, 'wb') as fp: + ACCOUNT_CONFIG.set('DEFAULT', 'key', api_key) + with open(self.config_path, 'wt') as fp: ACCOUNT_CONFIG.write(fp) diff --git a/syncano_cli/account/commands.py b/syncano_cli/account/commands.py index e690c09..c40ccb3 100644 --- a/syncano_cli/account/commands.py +++ b/syncano_cli/account/commands.py @@ -12,9 +12,7 @@ def top_account(): @click.pass_context @click.option('--config', help=u'Account configuration file.') def accounts(ctx, config): - """ - Handle Syncano account functionality; - """ + """Handle Syncano account functionality.""" account_commands = AccountCommands(config_path=config or ACCOUNT_CONFIG_PATH) ctx.obj['account_commands'] = account_commands diff --git a/syncano_cli/base/connection.py b/syncano_cli/base/connection.py index db275c8..f5a157a 100644 --- a/syncano_cli/base/connection.py +++ b/syncano_cli/base/connection.py @@ -1,11 +1,17 @@ # -*- coding: utf-8 -*- -from ConfigParser import NoOptionError, NoSectionError - +import six import syncano from syncano.exceptions import SyncanoException from syncano_cli.base.exceptions import BadCredentialsException, InstanceNotFoundException from syncano_cli.config import ACCOUNT_CONFIG, ACCOUNT_CONFIG_PATH +if six.PY2: + from ConfigParser import NoOptionError, NoSectionError +elif six.PY3: + from configparser import NoOptionError, NoSectionError +else: + raise ImportError() + def get_instance_name(config, instance_name): ACCOUNT_CONFIG.read(config) diff --git a/syncano_cli/base/exceptions.py b/syncano_cli/base/exceptions.py index 74fdfe8..b204bd2 100644 --- a/syncano_cli/base/exceptions.py +++ b/syncano_cli/base/exceptions.py @@ -23,11 +23,11 @@ class JSONParseException(CLIBaseException): class BadCredentialsException(CLIBaseException): - default_message = u'Wrong credential provided when login.' + default_message = u'Wrong login credentials provided.' class NotLoggedInException(CLIBaseException): - default_message = u'Do a login first: `syncano login`.' + default_message = u'Please log in to your account: `syncano login`.' class InstanceNotFoundException(CLIBaseException): diff --git a/syncano_cli/commands.py b/syncano_cli/commands.py index bd7b794..9bedfb2 100644 --- a/syncano_cli/commands.py +++ b/syncano_cli/commands.py @@ -19,8 +19,7 @@ def top_level(): @click.option('--instance-name', help=u'Default instance name.') def login(context, config, instance_name): """ - Log in to syncano using email and password and store ACCOUNT_KEY - in configuration file. + Log in to syncano using email and password. """ config = config or ACCOUNT_CONFIG_PATH context.obj['config'] = config @@ -33,7 +32,7 @@ def login(context, config, instance_name): ACCOUNT_CONFIG.set('DEFAULT', 'key', ACCOUNT_KEY) if instance_name: ACCOUNT_CONFIG.set('DEFAULT', 'instance_name', instance_name) - with open(context.obj['config'], 'wb') as fp: + with open(context.obj['config'], 'wt') as fp: ACCOUNT_CONFIG.write(fp) click.echo("INFO: Login successful.") except SyncanoException: diff --git a/syncano_cli/config.py b/syncano_cli/config.py index 732168f..598ff44 100644 --- a/syncano_cli/config.py +++ b/syncano_cli/config.py @@ -1,7 +1,15 @@ # -*- coding: UTF=8 -*- import os -from ConfigParser import ConfigParser + +import six + +if six.PY2: + from ConfigParser import ConfigParser +elif six.PY3: + from configparser import ConfigParser +else: + raise ImportError() ACCOUNT_CONFIG_PATH = os.path.join(os.path.expanduser('~'), '.syncano') ACCOUNT_CONFIG = ConfigParser() diff --git a/syncano_cli/config_commands/commands.py b/syncano_cli/config_commands/commands.py index 5a6b0d0..68286ec 100644 --- a/syncano_cli/config_commands/commands.py +++ b/syncano_cli/config_commands/commands.py @@ -15,6 +15,7 @@ def top_config(): @click.option('--config', help=u'Account configuration file.') @click.option('--instance-name', help=u'Instance name.') def config(ctx, config, instance_name): + """Allow to manage global instance config.""" instance = get_instance(config, instance_name) config_command = ConfigCommand(instance=instance) ctx.obj['config_command'] = config_command @@ -27,6 +28,7 @@ def config(ctx, config, instance_name): @click.argument('name') @click.argument('value') def add(ctx, name, value): + """Add config variable to global instance config.""" config_command = ctx.obj['config_command'] config_command.add(name, value) @@ -36,6 +38,7 @@ def add(ctx, name, value): @click.argument('name') @click.argument('value') def modify(ctx, name, value): + """Modify config value in global instance config.""" config_command = ctx.obj['config_command'] config_command.modify(name, value) @@ -44,5 +47,6 @@ def modify(ctx, name, value): @click.pass_context @click.argument('name') def delete(ctx, name): + """Removes config value from global instance config.""" config_command = ctx.obj['config_command'] config_command.delete(name) diff --git a/syncano_cli/custom_sockets/command.py b/syncano_cli/custom_sockets/command.py index 68d3258..bbce82b 100644 --- a/syncano_cli/custom_sockets/command.py +++ b/syncano_cli/custom_sockets/command.py @@ -31,6 +31,12 @@ def details(self, socket_name): cs = CustomSocket.please.get(name=socket_name, instance_name=self.instance.name) click.echo(SocketFormatter.format_socket_details(cs)) + def config(self, socket_name): + cs = CustomSocket.please.get(name=socket_name, instance_name=self.instance.name) + click.echo('Config for socket `{}`'.format(cs.name)) + for name, value in six.iteritems(cs.config): + click.echo('{:20}: {}'.format(name, value)) + def list_endpoints(self): endpoints = SocketEndpoint.get_all_endpoints(instance_name=self.instance.name) click.echo(SocketFormatter.format_endpoints_list(socket_endpoints=endpoints)) @@ -45,18 +51,18 @@ def install_from_dir(self, dir_path): with open(os.path.join(dir_path, self.SOCKET_FILE_NAME)) as socket_file: yml_file = yaml.safe_load(socket_file) - self.set_up_config(yml_file) + config = self.set_up_config(yml_file) api_data = SocketFormatter.to_json(socket_yml=yml_file, directory=dir_path) api_data.update({'instance_name': self.instance.name}) + api_data.update({'config': config}) custom_socket = CustomSocket.please.create(**api_data) click.echo('INFO: socket {} created.'.format(custom_socket.name)) def install_from_url(self, url_path, name): socket_yml = self.fetch_file(url_path) - self.set_up_config(socket_yml) - - CustomSocket(name=name).install_from_url(url=url_path, instance_name=self.instance.name) + config = self.set_up_config(socket_yml) + CustomSocket(name=name).install_from_url(url=url_path, instance_name=self.instance.name, config=config) click.echo('INFO: Installing socket from url: do `syncano sockets list` to obtain the status.') def run(self, endpoint_name, method='GET', data=None): @@ -108,13 +114,9 @@ def create_template_from_local_template(self, destination): script_file.write(script_source) def set_up_config(self, socket_yml): - instance_config = self.instance.get_config() - socket_config = SocketConfigParser(socket_yml=socket_yml) if socket_config.is_valid(): - provided_config = socket_config.ask_for_config(instance_config) - instance_config.update(provided_config) - self.instance.set_config(instance_config) + return socket_config.ask_for_config() def fetch_file(self, url_path): response = requests.get(url_path) diff --git a/syncano_cli/custom_sockets/commands.py b/syncano_cli/custom_sockets/commands.py index 659c6e7..3b98144 100644 --- a/syncano_cli/custom_sockets/commands.py +++ b/syncano_cli/custom_sockets/commands.py @@ -18,7 +18,7 @@ def top_sockets(): @click.option('--instance-name', help=u'Instance name.') def sockets(ctx, config, instance_name): """ - Allow to create a custom socket. + Create and manage custom sockets. """ instance = get_instance(config, instance_name) socket_command = SocketCommand(instance=instance) @@ -63,6 +63,14 @@ def details(ctx, socket_name): socket_command.details(socket_name=socket_name) +@sockets.command() +@click.pass_context +@click.argument('socket_name') +def config(ctx, socket_name): + socket_command = ctx.obj['socket_command'] + socket_command.config(socket_name=socket_name) + + @sockets.command() @click.pass_context @click.argument('socket_name') diff --git a/syncano_cli/custom_sockets/exceptions.py b/syncano_cli/custom_sockets/exceptions.py index 7f7a294..f21a66e 100644 --- a/syncano_cli/custom_sockets/exceptions.py +++ b/syncano_cli/custom_sockets/exceptions.py @@ -19,12 +19,8 @@ class SocketFileFetchException(CLIBaseException): default_message = u'Can not fetch the file: {}.' -class ConfigNameMissingException(CLIBaseException): - default_message = u'Variable name should be provided in config.' - - -class OneEndpointPerMethodException(CLIBaseException): - default_message = u'Only one endpoint per method allowed.' +class BadConfigFormatException(CLIBaseException): + default_message = u'Bad config format.' class BadYAMLDefinitionInEndpointsException(CLIBaseException): diff --git a/syncano_cli/custom_sockets/formatters.py b/syncano_cli/custom_sockets/formatters.py index 486c968..182eb7c 100644 --- a/syncano_cli/custom_sockets/formatters.py +++ b/syncano_cli/custom_sockets/formatters.py @@ -1,18 +1,31 @@ # -*- coding: utf-8 -*- import os +from collections import defaultdict import six import yaml -from syncano_cli.custom_sockets.exceptions import BadYAMLDefinitionInEndpointsException, OneEndpointPerMethodException +from syncano_cli.custom_sockets.exceptions import BadYAMLDefinitionInEndpointsException from syncano_cli.sync.scripts import ALLOWED_RUNTIMES +class DependencyTypeE(): + CLASS = 'class' + SCRIPT = 'script' + + class SocketFormatter(object): SOCKET_FIELDS = ['name', 'description', 'endpoints', 'dependencies'] HTTP_METHODS = ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'] ENDPOINT_TYPES = ['script'] - DEPENDENCY_TYPES = {'scripts': 'script'} + DEPENDENCY_TYPES = {'scripts': DependencyTypeE.SCRIPT, 'classes': DependencyTypeE.CLASS} + + @classmethod + def get_dependency_handlers(cls): + handlers = {} + for yml_name, dependency_type in six.iteritems(cls.DEPENDENCY_TYPES): + handlers[dependency_type] = getattr(cls, 'get_{}_dependency'.format(dependency_type)) + return handlers @classmethod def to_yml(cls, socket_object): @@ -68,9 +81,32 @@ def _json_process_endpoints(cls, endpoints): for name, endpoint_data in six.iteritems(endpoints): api_endpoints[name] = {'calls': cls._get_calls(endpoint_data)} + api_endpoints[name].update({'metadata': cls._get_metadata(endpoint_data)}) return api_endpoints + @classmethod + def _get_metadata(cls, endpoint_data): + metadata = defaultdict(dict) + for data_key, data in six.iteritems(endpoint_data): + if data_key in cls.HTTP_METHODS: + for metadata_key, inner_data in six.iteritems(data): + if metadata_key in cls.ENDPOINT_TYPES: + continue + if metadata_key == 'parameters': + metadata[metadata_key][data_key] = inner_data + else: + metadata[metadata_key] = inner_data + + elif data_key not in cls.ENDPOINT_TYPES: + if data_key == 'parameters': + metadata[data_key]['*'] = data + else: + metadata[data_key] = data + metadata[data_key] = data + + return metadata + @classmethod def _get_calls(cls, endpoint_data): calls = [] @@ -89,33 +125,55 @@ def _get_calls(cls, endpoint_data): }) elif type_or_method in cls.HTTP_METHODS: - if len(data) != 1: - raise OneEndpointPerMethodException() - for call_type, name in six.iteritems(data): - calls.append({ - 'type': call_type, - 'methods': [type_or_method], - 'name': name - }) + if call_type in cls.ENDPOINT_TYPES: + calls.append({ + 'type': call_type, + 'methods': [type_or_method], + 'name': name + }) return calls @classmethod def _json_process_dependencies(cls, dependencies, directory): api_dependencies = [] - + dependency_handlers = cls.get_dependency_handlers() for dependencies_type, dependency in six.iteritems(dependencies): if dependencies_type in cls.DEPENDENCY_TYPES: for dependency_name, data in six.iteritems(dependency): - api_dependencies.append({ - 'type': cls.DEPENDENCY_TYPES[dependencies_type], - 'runtime_name': data['runtime_name'], + dependency_type = cls.DEPENDENCY_TYPES[dependencies_type] + base_dependency_data = { 'name': dependency_name, - 'source': cls._get_source(data['file'], directory) - }) + 'type': dependency_type + } + typed_dependency_data = dependency_handlers[dependency_type](data, directory=directory) + typed_dependency_data.update(base_dependency_data) + api_dependencies.append(typed_dependency_data) return api_dependencies + @classmethod + def get_script_dependency(cls, data, **kwargs): + """ + Note: when definig new depenency processors use following pattern: + get_{name}_dependency -> where {name} is one of the defined in DepedencyTypeE + this allows to easily extend dependency handling; + :param data: + :param directory: + :return: + """ + directory = kwargs.get('directory') + return { + 'runtime_name': data['runtime_name'], + 'source': cls._get_source(data['file'], directory), + } + + @classmethod + def get_class_dependency(cls, data, **kwargs): + return { + 'schema': data['schema'] + } + @classmethod def _get_source(cls, file_name, directory): with open(os.path.join(directory, '{}'.format(file_name)), 'r+') as source_file: @@ -126,8 +184,13 @@ def _yml_process_endpoints(cls, endpoints): yml_endpoints = {} for endpoint_name, endpoint_data in six.iteritems(endpoints): yml_endpoints[endpoint_name] = cls._yml_process_calls(endpoint_data['calls']) + yml_endpoints.update(cls._yml_process_metadata(endpoint_data)) return yml_endpoints + @classmethod + def _yml_process_metadata(cls, endpoint_data): + return endpoint_data.get('metadata', {}) # some old custom sockets do not have this field; + @classmethod def _yml_process_calls(cls, data_calls): calls = {} diff --git a/syncano_cli/custom_sockets/parsers.py b/syncano_cli/custom_sockets/parsers.py index b53bd59..5389861 100644 --- a/syncano_cli/custom_sockets/parsers.py +++ b/syncano_cli/custom_sockets/parsers.py @@ -1,34 +1,42 @@ # -*- coding: utf-8 -*- - import click -from syncano_cli.custom_sockets.exceptions import ConfigNameMissingException +import six +from syncano_cli.custom_sockets.exceptions import BadConfigFormatException class SocketConfigParser(object): + PROMPT_FIELD = 'prompt' + CONSTANTS_FIELD = 'constants' + def __init__(self, socket_yml): self.config = socket_yml.get('config', []) def is_valid(self): - for config_var in self.config: - if not config_var.get('name'): - raise ConfigNameMissingException() + valid = True + for field_name in [self.PROMPT_FIELD, self.CONSTANTS_FIELD]: + valid &= self._is_valid(field_name) + if not valid: + raise BadConfigFormatException() + return valid + + def _is_valid(self, field_name): + if field_name in self.config and not len(self.config[field_name]): + return False return True - def ask_for_config(self, instance_config): + def ask_for_config(self): provided_config = {} - for config_var in self.config: - config_var_name = config_var['name'] - - if config_var_name not in instance_config: - config_var_value = click.prompt(self.get_prompt_str(config_var)) + if self.PROMPT_FIELD in self.config: + for config_var_name, config_metadata in six.iteritems(self.config[self.PROMPT_FIELD]): + config_var_value = click.prompt(self.get_prompt_str(config_var_name, config_metadata)) provided_config[config_var_name] = config_var_value return provided_config @staticmethod - def get_prompt_str(config_var): - prompt_str = 'Provide value for {}'.format(config_var['name']) - if config_var.get('description', None): - prompt_str = '{} ({})'.format(prompt_str, config_var['description']) + def get_prompt_str(field_name, config_metadata): + prompt_str = 'Provide value for {}'.format(field_name) + if config_metadata.get('description'): + prompt_str = '{} ({})'.format(prompt_str, config_metadata['description']) return prompt_str diff --git a/syncano_cli/custom_sockets/templates/socket_template.py b/syncano_cli/custom_sockets/templates/socket_template.py index a5ddecb..35b6c2c 100644 --- a/syncano_cli/custom_sockets/templates/socket_template.py +++ b/syncano_cli/custom_sockets/templates/socket_template.py @@ -8,8 +8,12 @@ name: icon-name color: ffee11 config: - - name: custom_api_key - description: an Api Key for third party service + constants: + some_key: some_value + prompt: + custom_api_key: + type: sting + description: an Api Key for third party service description: Some custom integration endpoints: custom_endpoint: @@ -32,6 +36,20 @@ custom_script_2: runtime_name: python_library_v5.0 file: scripts/custom_script_2.py + + classes: + country: + schema: + - name: name + type: string + - name: topLevelDomain + type: string + - name: capital + type: string + - name: alpha2Code + type: string + - name: alpha3Code + type: string """ SCRIPTS = { diff --git a/syncano_cli/execute/utils.py b/syncano_cli/execute/utils.py index 33c9a0a..f02351f 100644 --- a/syncano_cli/execute/utils.py +++ b/syncano_cli/execute/utils.py @@ -3,6 +3,8 @@ import json +import six + def print_response(response): if hasattr(response, 'result'): @@ -16,7 +18,7 @@ def print_response(response): def _print_result(result): try: - if type(result) in [str, unicode]: + if isinstance(result, six.string_types): output = json.loads(result) else: output = result diff --git a/syncano_cli/hosting/command.py b/syncano_cli/hosting/command.py index b415f0b..c15cbdc 100644 --- a/syncano_cli/hosting/command.py +++ b/syncano_cli/hosting/command.py @@ -1,18 +1,34 @@ # -*- coding: utf-8 -*- import os +import re import click from syncano_cli.base.command import BaseInstanceCommand -from syncano_cli.hosting.exceptions import NoDefaultHostingFoundException, PathNotFoundException, UnicodeInPathException +from syncano_cli.hosting.exceptions import NoHostingFoundException, PathNotFoundException, UnicodeInPathException class HostingCommands(BaseInstanceCommand): - def list_hosting(self): + VALID_PATH_REGEX = re.compile(r'^(?!/)([a-zA-Z0-9\-\._]+/{0,1})+(?