def construct_cli_auth(self, instance, spec, loc, context): """ Constructor of `.cli_auth` predicate. It adds an additional option to all commands in order to specify credentials to interact with the API. There are multiple authentication model that can be supported according to the specification. However, credentials must be stored in a configuration file of specific format (`JSON` and `YAML` are supported at the momment). """ if self.ADAPTER_CONF not in instance: raise doc.DeferConstructor auth_format = doc.doc_get(spec, ('.auth_format', 'format')) auth_modes = self.get_structural_elements(spec) auth_schema = {} for auth_mode in auth_modes: schema = doc.doc_get(spec, (auth_mode, )) auth_schema[auth_mode] = schema commands = doc.doc_get(instance, (self.ADAPTER_CONF, 'actions')) assert commands, ('Loc: %s, commands have not been constructed yet.' % (str(loc))) credential_option = click.option('--credentials', required=True, type=Credentials( schema=auth_schema, file_type=auth_format)) for command in commands: credential_option(command) return instance
def construct_drf_collection(self, instance, spec, loc, context): """ Constructor for `.drf_collection` predicate. It generates the required, `Serializer` class, and `ViewSet` class based on the field schema, actions, permissions and additional configuation (filter_fields, mixins) as specified on spec. """ parent = context.get('parent_name') constructed = context.get('constructed') if '.collection' not in constructed: raise doc.DeferConstructor field_schema = doc.doc_get(instance, ('*',)) actions = doc.doc_get(instance, ('actions', self.ADAPTER_CONF)) or [] model = self._get_or_import_model(parent, loc + ('model',), context.get('top_spec')) model_serializers = spec.pop('model_serializers', []) extra_serializers = spec.pop('serializers', []) serializer = self.generate_serializer( field_schema, parent, model=model, model_serializers=model_serializers, extra_serializers=extra_serializers) kwargs = {k: v for k, v in spec.iteritems() if k != 'model'} permissions = self.get_permissions(parent, context.get('top_spec')) view = generate_view(parent, serializer, model, actions=actions, permissions=permissions, **kwargs) instance[self.ADAPTER_CONF] = view self.serializers[parent] = serializer self.views[parent] = view return instance
def construct_field(self, instance, spec, loc, context): """ Constructor of `.field` predicate. It constructs a dictionary corresponding to a cerberus validation schema along with all rules based on spec. """ def default(instance, spec, loc, context, **kwargs): return instance parent_name = context.get('parent_name') nested_structures = {'.struct', '.structarray'} field_type = self.extract_type(instance) if not field_type: raise ex.ApimasException( 'You have to specify field type for field `%s`' % (parent_name)) self.init_adapter_conf(instance) if field_type in nested_structures: return self.construct_nested_field(instance, spec, loc, context, field_type) method_name = '_add_' + field_type[1:] + '_params' params = doc.doc_get(instance, (field_type, )) return getattr(self, method_name, default)(instance, spec, loc, context, **params)
def construct_cli_option(self, instance, spec, loc, context): """ Constructor for '.cli_option' predicate. It constructs a dictionary keyed by option name which contains all required keyword arguments for `click.option()` constructor. """ parent_name = context.get('parent_name') extra_params = {'.ref': self._add_ref_params} if instance == SKIP: return instance predicate_type = self.extract_type(instance) option_name = doc.doc_get(spec, ('option_name', )) or parent_name if predicate_type == '.struct': return self.construct_struct_option(instance, parent_name, spec, loc, option_name) instance = self.init_adapter_conf(instance, initial={option_name: {}}) kwargs = { 'type': self.construct_option_type(instance, spec, loc, context, predicate_type) } extra = extra_params.get(predicate_type) if extra: kwargs.update(extra(instance, spec, loc, context)) if '.required' in instance: kwargs.update({'required': True}) instance[self.ADAPTER_CONF][option_name] = kwargs return instance
def construct_struct_option(self, instance, parent_name, spec, loc, option_name): """ Constructor for `.struct` predicate. This field corresponds to a python `dict` with all options and their keyword parameters associated with this field. A struct is consisted of all options of its field schema. Example: A struct named 'cart' which incorporates fields `id` and `products` corresponds to the following options: * --cart-id * --cart-products """ option_kwargs = {} self.init_adapter_conf(instance) for _, schema in doc.doc_get(instance, ('.struct', )).iteritems(): if schema == SKIP: continue for nested, params in schema.get(self.ADAPTER_CONF).iteritems(): option_kwargs.update({option_name + '-' + nested: params}) self.struct_map[option_name + '-' + nested] = ( parent_name, ) + self.struct_map.get(nested, (nested, )) instance[self.ADAPTER_CONF].update(option_kwargs) return instance
def partial_validate(self, data, raise_exception=True, schema=None): """ Validates data that are going to be sent for a partial update of a resource. Construct a cerberus schema validator by taking into consideration only the fields that are included in the request. :param raise_exception: True if an exception should be raised when validation fails. """ schema = schema or self.validation_schema cerberus_paths = to_cerberus_paths(data) validated_subdocs = self._validate_subdata(data, schema, raise_exception) partial_schema_paths = { path: doc.doc_get(schema, path.split('/')) for path in cerberus_paths } partial_schema = doc.doc_from_ns(partial_schema_paths) validator = ApimasValidator(partial_schema) is_valid = validator.validate(data) if raise_exception and not is_valid: raise ex.ApimasClientException(validator.errors) for k, v in validated_subdocs.iteritems(): doc.doc_set(validator.document, k, v) return validator.document
def get_permissions(self, collection, top_spec): """ It constructs permissions rules for every collection. Typically, permission rules are provided at a global scope. Then, this method, actually matches all permissions rules which are applied to a specific collection and then it returns all permissions that are compatible and apply on the collection. """ permission_path = ('.endpoint', 'permissions') nu_columns = 6 permission_doc = {} permissions = doc.doc_get(top_spec, permission_path) or [] permissions = [[doc.parse_pattern(segment) for segment in row] for row in permissions] for rule in permissions: doc.doc_set(permission_doc, rule[:-1], rule[-1]) patterns = [[collection], [doc.ANY], [doc.ANY], [doc.ANY], [doc.ANY]] matches = list(doc.doc_match_levels( permission_doc, patterns, expand_pattern_levels=range(nu_columns))) if not matches: return None return map((lambda x: x[1:]), matches)
def construct_collection(self, instance, spec, loc, context): self.init_adapter_conf(instance) field_schema = doc.doc_get(instance, ('*', )) assert len(loc) >= 3 if not field_schema: raise ex.ApimasException( 'A collection must define its field schema.' ' Empty collection found: %s' % (loc[-2])) return instance
def get_constructor_params(self, spec, loc, params): """ Get constructor params for all the constructors that represent a structure, e.g. `.struct`, `.collection`, etc. """ for structure in self.STRUCTURES.keys(): struct_doc = doc.doc_get(spec, loc[:-1]) or {} structure_params = doc.doc_get( struct_doc, (self.STRUCTURES[structure],)) or {} onmodel = structure_params.get('onmodel', True) if structure in struct_doc and onmodel: if structure == '.collection': params.append((structure, {'model': self.models[loc[-2]]})) continue source = structure_params.get('source') params.append((structure, {'source': source or loc[-2]})) if loc[:-1]: return self.get_constructor_params(spec, loc[:-1], params) return params
def _get_or_import_model(self, collection, model_path, top_spec): """ This function checks if a model of a collection is already specified and imported and retrieves it. If this is not the case, then it imports it and retrieves it. """ if collection not in self.models: model = utils.import_object( doc.doc_get(top_spec, model_path)) self.models[collection] = model else: model = self.models[collection] return model
def generate_nested_drf_field(self, instance, name, predicate_type, model, onmodel=True, **kwargs): """ Generate a nested drf field, which is actually a `Serializer` class. """ kwargs.update(self.get_default_properties(predicate_type, kwargs)) field_schema = doc.doc_get(instance, (predicate_type,)) many = predicate_type == '.structarray' model_serializers = kwargs.pop('model_serializers', []) extra_serializers = kwargs.pop('serializers', []) serializer = self.generate_serializer( field_schema, name, onmodel=onmodel, model_serializers=model_serializers, extra_serializers=extra_serializers, model=model) return serializer(many=many, **kwargs)
def _classify_fields(self, field_schema): """ Seperates the model fields fro the non-model fields. It also returns a dictionary of instance sources (if they are exist) for the non-model fields. """ model_fields = {} extra_fields = {} instance_sources = {} for field_name, properties in field_schema.iteritems(): onmodel = doc.doc_get(properties, ('.drf_field', 'onmodel')) if onmodel is None: onmodel = True field_path = (self.ADAPTER_CONF, 'field') instance_path = (self.ADAPTER_CONF, 'source') field = doc.doc_get(properties, field_path) if onmodel: model_fields[field_name] = field else: extra_fields[field_name] = field instance_sources[field_name] = doc.doc_get(properties, instance_path) return model_fields, extra_fields, instance_sources
def apply(self): """ Create django rest views based on the constructed adapter spec. """ if not self.adapter_spec: raise utils.DRFAdapterException( 'Cannot apply an empty adapter specification') structural_elements = self.get_structural_elements(self.adapter_spec) api = structural_elements[0] router = routers.DefaultRouter() for collection, spec in doc.doc_get( self.adapter_spec, (api,)).iteritems(): view = spec.get(self.ADAPTER_CONF) router.register(collection, view, base_name=collection) self.urls = url(r'^' + api + '/', include(router.urls))
def construct_cli_commands(self, instance, spec, loc, context): """ Constructor for '.cli_commands' predicate. Gets all commands corresponding to actions and attaches the appropriate options to them based on field schema. """ parent_name = context.get('parent_name') instance = self.init_adapter_conf(instance, initial={'actions': set()}) commands = doc.doc_get(instance, ('actions', self.ADAPTER_CONF)) or {} for action, command in commands.iteritems(): command = self.construct_command(instance, parent_name, spec, loc, action, command) instance[self.ADAPTER_CONF]['actions'].add(command) return instance
def construct_collection(self, instance, spec, loc, context): """ Constructor for `.collection` predicate. This constructor aims to aggregate the cerberus validation schemas for every single field defined by the collection. """ instance = super(self.__class__, self).construct_collection(instance, spec, loc, context) self.init_adapter_conf(instance) schema = { field_name: schema.get(self.ADAPTER_CONF, {}) for field_name, schema in doc.doc_get(instance, ( '*', )).iteritems() } instance[self.ADAPTER_CONF] = schema return instance
def apply(self): """ Apply generated cerberus specification and create `ApimasClient` objects for every resource defined in the specification. """ if not self.adapter_spec: raise ex.ApimasException( 'Cannot create clients from an empty spec') structural_elements = self.get_structural_elements(self.adapter_spec) assert len(structural_elements) == 1 for collection, spec in doc.doc_get( self.adapter_spec, (structural_elements[0], )).iteritems(): schema = spec.get(self.ADAPTER_CONF, {}) endpoint = urljoin( self.root_url, TRAILING_SLASH.join([structural_elements[0], collection])) endpoint += TRAILING_SLASH self.clients[collection] = ApimasClient(endpoint, schema)
def construct_command(self, instance, command_name, spec, loc, action, command): """ Construct command's options for a specific collection according to the `APIMAS` specification. """ field_schema = doc.doc_get(instance, ('*', )) for field_name, spec in field_schema.iteritems(): if spec == SKIP: continue for option_name, params in spec.get(self.ADAPTER_CONF).iteritems(): option_constructor = self.OPTION_CONSTRUCTORS[action] if not self.option_allowed(action, spec, option_constructor): continue command = option_constructor(option_name, params)(command) path = (field_name,) if '.struct' not in spec\ else self.struct_map[option_name] command.register_option_mapping(option_name.replace('-', '_'), path) return base_group.command(name=loc[-2] + '-' + action)(command)
def validate_ref(self, instance, name, loc, top_spec, source): """ Validates that the referenced field is a foreign key to the same django model table as the model defined in the referenced collection of spec. Otherwise, an exception with explanatory message is raised. """ root_loc = loc[0:1] ref = doc.doc_get(instance, ('.ref', 'to')) django_conf = self.get_constructor_params(top_spec, loc, []) model = self.extract_model(source or name, django_conf) auto = True try: model_field = model._meta.get_field(source or name) path = root_loc + (ref, '.drf_collection', 'model') ref_model = self._get_or_import_model(ref, path, top_spec) model_attr = _validate_relational_field( name, ref_model, model_field) except FieldDoesNotExist: auto = False model_attr = _validate_model_attribute(name, model, source or name) return model_attr, model, auto
def construct_property(self, instance, spec, loc, context, property_name): """ Constuctor for predicates that indicate a property of a field, e.g. nullable, readonly, required, etc. This constructor generates the corresponding spec syntax. However, it requires field to be initialized, otherwise, construction is defered. """ if property_name not in self.PROPERTY_MAPPING: raise ex.ApimasException('Unknown property name %s' % (property_name)) constructed = context.get('constructed') predicate_type = self.extract_type(instance) if predicate_type not in constructed: raise doc.DeferConstructor if predicate_type in self.SKIP_FIELDS: return instance field_schema = doc.doc_get(instance, (self.ADAPTER_CONF, )) field_schema.update( {self.PROPERTY_MAPPING.get(property_name, property_name): True}) return instance
def default_field_constructor(self, instance, spec, loc, context, predicate_type): """ A common constructor for the drf fields. There are two cases: * If the field is a model field, then it does not initialize a `serializers.Field` object, but it stores all its properties in dictionary in order to be initialized later from the serializer. * If the field is a non-model field or its type is either `.struct` or `.structarry`, then the corresponding `serializers.Field` is contructed. Moreover, this method checks if the field conforms to the model configuations before being constructed. """ model, automated = self.validate_model_configuration( instance, spec, loc, context, predicate_type) path = (self.ADAPTER_CONF,) instance_source = spec.pop('instance_source', None) onmodel = spec.get('onmodel', True) if instance_source and onmodel: raise utils.DRFAdapterException( 'You don\'t have to specify `instance_source` if' ' `onmodel` is set') field_kwargs = {k: v for k, v in spec.iteritems() if k != 'onmodel'} field_kwargs.update(doc.doc_get(instance, path) or {}) if predicate_type == '.ref': field_kwargs.update(self._get_ref_params( instance, loc, context.get('top_spec'), onmodel and automated, field_kwargs)) doc.doc_set(instance, (self.ADAPTER_CONF, 'source'), instance_source) drf_field = self._generate_field( instance, context.get('parent_name'), predicate_type, model, automated and onmodel, **field_kwargs) doc.doc_set(instance, (self.ADAPTER_CONF, 'field'), drf_field) return instance
def construct_nested_field(self, instance, spec, loc, context, field_type=None): """ Constructor for predicates that include nested schemas. Typically, `.struct` and `.structarray` predicates are included in this category of fields. This constructor generates the corresponding cerberus syntax for having a `list` of dicts or a `dict` in accordance to the aforementioned structures. """ bound_field = { '.struct': lambda x: { 'type': 'dict', 'schema': x }, '.structarray': lambda x: { 'type': 'list', 'schema': { 'type': 'dict', 'schema': x } } } params = doc.doc_get(instance, (field_type, )) field_schema = { field_name: schema.get(self.ADAPTER_CONF, {}) for field_name, schema in params.iteritems() } instance[self.ADAPTER_CONF].update( bound_field[field_type](field_schema)) return instance
def get_ref_collections(spec, collection): structural_element = get_structural_element(spec) loc = (structural_element, collection, '*') field_schema = doc.doc_get(spec, loc) refs = get_refs(field_schema, spec) return refs
def get_required_fields(spec, collection): structural_element = get_structural_element(spec) loc = (structural_element, collection, '*') field_schema = doc.doc_get(spec, loc) return filter_field_schema(field_schema, included=['.required'])
def get_fields(spec, collection, excluded=None, included=None): structural_element = get_structural_element(spec) loc = (structural_element, collection, '*') field_schema = doc.doc_get(spec, loc) return filter_field_schema(field_schema, excluded, included)
def action_exists(spec, collection, action): structural_element = get_structural_element(spec) loc = (structural_element, collection, 'actions') actions = doc.doc_get(spec, loc) or {} return action in actions
def _add_ref_params(self, instance, spec, loc, context): many = doc.doc_get(instance, ('.ref', 'many')) return {'multiple': True} if many else {}