def __init__(self, *args, **kwargs): args_len = len(args) meta = getmeta(self) if args_len > len(meta.init_fields): raise TypeError('This resource takes %s positional arguments but %s where given.' % ( len(meta.init_fields), args_len)) # The ordering of the zip calls matter - zip throws StopIteration # when an iter throws it. So if the first iter throws it, the second # is *not* consumed. We rely on this, so don't change the order # without changing the logic. fields_iter = iter(meta.init_fields) if args_len: if not kwargs: for val, field in zip(args, fields_iter): setattr(self, field.attname, val) else: for val, field in zip(args, fields_iter): setattr(self, field.attname, val) kwargs.pop(field.name, None) # Now we're left with the unprocessed fields that *must* come from # keywords, or default. for field in fields_iter: try: val = kwargs.pop(field.attname) except KeyError: val = field.get_default() setattr(self, field.attname, val) if kwargs: raise TypeError("'%s' is an invalid keyword argument for this function" % list(kwargs)[0])
def on_resource_ready(self): # Extract reference to fields meta = getmeta(self.resource) try: self._fields = tuple(meta.field_map[name] for name in self.field_names) except KeyError as ex: raise AttributeError("Attribute {0} not found on {1!r}".format(ex, self.resource))
def clean_fields(self, exclude=None, ignore_not_provided=False): errors = {} meta = getmeta(self) for f in meta.fields: if exclude and f.name in exclude: continue raw_value = f.value_from_object(self) if (f.null and raw_value is None) or (ignore_not_provided and raw_value is NotProvided): continue try: raw_value = f.clean(raw_value) except ValidationError as e: errors[f.name] = e.messages # Check for resource level clean methods. clean_method = getattr(self, "clean_%s" % f.attname, None) if callable(clean_method): try: raw_value = clean_method(raw_value) except ValidationError as e: errors.setdefault(f.name, []).extend(e.messages) if f not in meta.readonly_fields: setattr(self, f.attname, raw_value) if errors: raise ValidationError(errors)
def generate_definition(resource): """ Generate a `Swagger Definitions Object <http://swagger.io/specification/#definitionsObject>`_ from a resource. """ meta = getmeta(resource) definition = { 'type': "object", 'properties': {} } for field in meta.all_fields: field_definition = { 'type': SWAGGER_SPEC_TYPE_MAPPING.get(field, 'string') } if field in SWAGGER_SPEC_FORMAT_MAPPING: field_definition['format'] = SWAGGER_SPEC_FORMAT_MAPPING[field] if field.doc_text: field_definition['description'] = field.doc_text definition['properties'][field.name] = field_definition return {meta.name: definition}
def clean_fields(self, exclude=None): errors = {} for f in getmeta(self).fields: if exclude and f.name in exclude: continue raw_value = f.value_from_object(self) if f.null and raw_value is None: continue try: raw_value = f.clean(raw_value) except ValidationError as e: errors[f.name] = e.messages # Check for resource level clean methods. clean_method = getattr(self, "clean_%s" % f.attname, None) if callable(clean_method): try: raw_value = clean_method(raw_value) except ValidationError as e: errors.setdefault(f.name, []).extend(e.messages) setattr(self, f.attname, raw_value) if errors: raise ValidationError(errors)
def test_field_sorting(self): class SampleProxy(proxy.ResourceProxy): class Meta: field_sorting = True resource = Book assert [f.attname for f in getmeta(SampleProxy).fields] == [ 'title', 'isbn', 'num_pages', 'rrp', 'fiction', 'genre', 'published', 'authors', 'publisher']
def field_names(self): """ Field names from resource. """ fields = getmeta(self.resource_type).fields if self.ignore_header_case: return CaseLessStringList(field.name for field in fields) else: return tuple(field.name for field in fields)
def test_field_sorting__custom(self): class SampleProxy(proxy.ResourceProxy): class Meta: def field_sorting(fields): return sorted(fields, key=lambda f: f.attname) resource = Book assert [f.attname for f in getmeta(SampleProxy).fields] == [ 'authors', 'fiction', 'genre', 'isbn', 'num_pages', 'published', 'publisher', 'rrp', 'title']
def apply_to(cls, sources, include=None, exclude=None, **kwargs): """ Convenience method that applies include/exclude lists to all items in an iterable collection of resources. :param sources: Source resources being wrapped. :param include: Fields that should be explicitly included on the adapter. :param exclude: Fields to explicitly exclude on the adapter. """ meta_objects = {} for resource in sources: try: meta = meta_objects[getmeta(resource).resource_name] except KeyError: meta = cls._create_options_adapter(getmeta(resource), include, exclude) meta_objects[getmeta(resource).resource_name] = meta yield cls(resource, meta=meta, **kwargs)
def to_python(self, value): if value is None: return None if isinstance(value, self.of): return value if isinstance(value, dict): return create_resource_from_dict(value, getmeta(self.of).resource_name) msg = self.error_messages['invalid'] % self.of raise exceptions.ValidationError(msg)
def default(self, o): if isinstance(o, (resources.ResourceBase, ResourceAdapter)): meta = getmeta(o) obj = o.to_dict(self.include_virtual_fields) obj[meta.type_field] = meta.resource_name return obj elif isinstance(o, bases.ResourceIterable): return list(o) elif o.__class__ in TYPE_SERIALIZERS: return TYPE_SERIALIZERS[o.__class__](o)
def __init__(self, *args, **kwargs): meta = getmeta(self) # Get shadowed resource if supplied shadow = kwargs.pop('__resource', None) if shadow is None: # Create a new instance self._shadow = meta.resource() super(ResourceProxyBase, self).__init__(*args, **kwargs) else: self._shadow = shadow
def contribute_to_class(self, cls, _): cls._meta = self cls_name = cls.__name__ self.name = cls_name self.class_name = "{}.{}".format(cls.__module__, cls_name) # Get and filter meta attributes meta_attrs = self.meta.__dict__.copy() for name in self.meta.__dict__: if name.startswith('_'): del meta_attrs[name] # Get the required resource object self.resource = meta_attrs.pop('resource', None) if not self.resource: raise AttributeError('`resource` has not been defined.') self.shadow = shadow = getmeta(self.resource) # Extract all meta options and fetch from shadow if not defined proxy_attrs = { 'name': cls_name, 'verbose_name': cls_name.replace('_', ' ').strip('_ '), } for attr_name in self.META_OPTION_NAMES: if attr_name in meta_attrs: value = meta_attrs.pop(attr_name) if attr_name == 'verbose_name': # If defined generate pluralised form base on this name. if 'verbose_name_plural' not in proxy_attrs: proxy_attrs['verbose_name_plural'] = value + 's' elif attr_name == 'namespace': # Allow meta to be defined as namespace attr_name = 'name_space' elif attr_name == 'key_field_name': # Remap to key_field names attr_name = 'key_field_names' value = [value] proxy_attrs[attr_name] = value elif hasattr(shadow, attr_name): proxy_attrs.setdefault(attr_name, getattr(shadow, attr_name)) # Any leftover attributes must be invalid. if meta_attrs != {}: raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs.keys())) del self.meta # Apply to self for attr_name, value in proxy_attrs.items(): setattr(self, attr_name, value)
def default(self, o): if isinstance(o, (resources.ResourceBase, ResourceAdapter)): meta = getmeta(o) obj = o.to_dict(self.include_virtual_fields) if self.include_type_field: obj[meta.type_field] = meta.resource_name return obj elif isinstance(o, LIST_TYPES): return list(o) elif o.__class__ in JSON_TYPES: return JSON_TYPES[o.__class__](o) return super(OdinEncoder, self).default(o)
def __next__(self): if self._resource_iters: if self._field_iters: # Check if the last entry in the field stack has any unprocessed fields. if self._field_iters[-1]: # Select the very last field in the field stack. field = self._field_iters[-1][0] # Request a list of resources along with keys from the composite field. self._resource_iters.append(field.item_iter_from_object(self.current_resource)) # Update the path self._path.append((NotSupplied, NotSupplied, field.name)) self._resource_stack.append(None) # Remove the field from the list (and remove this field entry if it has been emptied) self._field_iters[-1].pop(0) else: self._field_iters.pop() if self.current_resource: if hasattr(self, 'on_exit'): self.on_exit() try: key, next_resource = next(self._resource_iters[-1]) except StopIteration: # End of the current list of resources pop this list off and get the next list. self._path.pop() self._resource_iters.pop() self._resource_stack.pop() return next(self) else: next_meta = getmeta(next_resource) # If we have a key (ie DictOf, ListOf composite fields) update the path key field. if key is not None: _, name, field = self._path[-1] if next_meta.key_field: key = next_meta.key_field.value_from_object(next_resource) name = next_meta.key_field.name self._path[-1] = (key, name, field) # Get list of any composite fields for this resource (this is a cached field). self._field_iters.append(list(next_meta.composite_fields)) # self.current_resource = next_resource self._resource_stack[-1] = next_resource if hasattr(self, 'on_enter'): self.on_enter() return next_resource else: raise StopIteration()
def register_resources(self, *resources): """ Register a resource (or resources). :param resources: Argument list of resources to register. """ for resource in resources: meta = getmeta(resource) resource_name = meta.resource_name.lower() self.resources[resource_name] = resource class_name = meta.class_name.lower() if resource_name != class_name: self.resources[class_name] = resource
def create_resource_from_iter(i, resource, full_clean=True, default_to_not_provided=False): """ Create a resource from an iterable sequence :param i: Iterable of values (it is assumed the values are in field order) :param resource: A resource type, resource name or list of resources and names to use as the base for creating a resource. :param full_clean: Perform a full clean as part of the creation, this is useful for parsing data with known columns (eg CSV data). :param default_to_not_provided: If an value is not supplied keep the value as NotProvided. This is used to support merging an updated value. :return: New instance of resource type specified in the *resource* param. """ i = list(i) resource_type = resource fields = getmeta(resource_type).fields # Optimisation to allow the assumption that len(fields) == len(i) len_fields = len(fields) len_i = len(i) extra = None if len_i < len_fields: i += [NotProvided] * (len_fields - len_i) elif len_i > len_fields: extra = i[len_fields:] i = i[:len_fields] attrs = [] errors = {} for f, value in zip(fields, i): if value is NotProvided: if not default_to_not_provided: value = f.get_default() if f.use_default_if_not_provided else None else: try: value = f.to_python(value) except ValidationError as ve: errors[f.name] = ve.error_messages attrs.append(value) if errors: raise ValidationError(errors) new_resource = resource_type(*attrs) if extra: new_resource.extra_attrs(extra) if full_clean: new_resource.full_clean() return new_resource
def __init__(self, source, include=None, exclude=None, meta=None): """ Initialise the adapter. :param source: Source resource being wrapped. :param include: Fields that should be explicitly included on the adapter. :param exclude: Fields to explicitly exclude on the adapter. """ self.__dict__['_source'] = source if not meta: meta = self._create_options_adapter(getmeta(source), include, exclude) self._meta = meta
def to_dict(self, include_virtual=True): """ Convert this resource into a `dict` of field_name/value pairs. .. note:: This method is not recursive, it only operates on this single resource, any sub resources are returned as is. The use case that prompted the creation of this method is within codecs when a resource must be converted into a type that can be serialised, these codecs then operate recursively on the returned `dict`. :param include_virtual: Include virtual fields when generating `dict`. """ meta = getmeta(self) fields = meta.all_fields if include_virtual else meta.fields return {f.name: v for f, v in field_iter_items(self, fields)}
def add_directive_header(self, _): domain = getattr(self, 'domain', 'py') directive = getattr(self, 'directivetype', self.objtype) source_name = self.get_sourcename() if self.options.get('api_documentation'): meta = getmeta(self.object) name = meta.resource_name else: name = self.format_name() self.add_line(u'.. %s:%s:: %s' % (domain, directive, name), source_name) if self.options.noindex: self.add_line(u' :noindex:', source_name)
def reader(f, resource, includes_header=False, csv_module=csv, full_clean=True, *args, **kwargs): """ CSV reader that returns resource objects :param f: file like object :param resource: :param includes_header: File includes a header that should be used to map columns :param csv_module: Specify an alternate csv module (eg unicodecsv); defaults to the builtin csv as this module is implemented in C. :param full_clean: Perform a full clean on each object :return: Iterable reader object """ csv_reader = csv_module.reader(f, *args, **kwargs) if includes_header: fields = getmeta(resource).fields # Pre-generate field mapping header = csv_reader.next() mapping = [] for field in fields: if field.name in header: mapping.append(header.index(field.name)) else: mapping.append(None) # Iterate CSV and process input for row in csv_reader: yield create_resource_from_iter( (NOT_PROVIDED if s is None else row[s] for s in mapping), resource, full_clean ) else: # Iterate CSV and process input for row in csv_reader: yield create_resource_from_iter( (NOT_PROVIDED if col is None else col for col in row), resource, full_clean )
def dumps(fmt=FORMAT_TEMPLATE_RST, exclude=None, template_path=None): """ Dump resource documentation to a string. :param fmt: Format template, default is restructured text (can be used with Sphinx). :param exclude: List of resources to exclude from generation. :param template_path: An additional template_path for customisation of generated documentation. :returns: string representation of documentation. If an additional template path is supplied it will be made the first path in the template search paths and will override any built in templates. """ exclude = exclude or [] # Get template search_path = (template_path, _TEMPLATE_ROOT) if template_path else _TEMPLATE_ROOT env = Environment(loader=FileSystemLoader(search_path), autoescape=_auto_escape, ) template = env.get_template(fmt) # Build resources list resources = [ResourceDocumentation(r) for r in registration.cache if getmeta(r).resource_name not in exclude] return template.render(resources=resources)
def get_value(self, root_resource): """ Get a value from a resource structure. """ result = root_resource for value, key, attr in self: meta = getmeta(result) try: field = meta.field_map[attr] except KeyError: raise InvalidPathError(self, "Unknown field `{0}`".format(attr)) result = field.value_from_object(result) if value is NotSupplied: # Nothing is required continue elif key is NotSupplied: # Index or key into element value = field.key_to_python(value) try: result = result[value] except (KeyError, IndexError): raise NoMatchError(self, "Could not find index `{0}` in {1}.".format(value, field)) else: # Filter elements if isinstance(result, dict): result = result.values() results = tuple(r for r in result if getattr(r, key) == value) if len(results) == 0: raise NoMatchError( self, "Filter matched no values; `{0}` == `{1}` in {2}.".format(key, value, field)) elif len(results) > 1: raise MultipleMatchesError( self, "Filter matched multiple values; `{0}` == `{1}`.".format(key, value)) else: result = results[0] return result
def __repr__(self): return '<Proxy of {!r}>'.format(getmeta(self.resource))
def test_options(self, attr, actual): target = getmeta(BookProxy) assert actual == getattr(target, attr)
def contribute_to_class(self, cls, name): self.set_attributes_from_name(name) self.resource = cls getmeta(cls).add_field(self)
def reference_to(obj): if hasattr(obj, '_meta'): return ":py:class:`%s`" % getmeta(obj).name return obj
def __init__(self, resource): self.resource = resource self._meta = getmeta(resource) self._fields = None
def represent_resource(self, data): obj = data.to_dict(self.include_virtual_fields) if self.include_type_field: meta = getmeta(data) obj[meta.type_field] = meta.resource_name return self.represent_dict(obj)
def resolve_resource_type(resource): if isinstance(resource, type) and issubclass(resource, ResourceBase): meta = getmeta(resource) return meta.resource_name, meta.type_field else: return resource, DEFAULT_TYPE_FIELD
def get_field_dict(self): """Return a dictionary of fields along with their names.""" return getmeta(self.obj).field_map
def field_map(self): return getmeta(Book).field_map
def test_options_type(self): actual = getmeta(BookProxy) assert isinstance(actual, proxy.ResourceProxyOptions)
def parent_resource_names(self): """ List of parent resource names. """ return tuple(getmeta(p).resource_name for p in self.parents)
def value_fields(resource): """ Iterator to get non-composite (eg value) fields for export """ meta = getmeta(resource) return [f for f in meta.all_fields if f not in meta.composite_fields]
def create_resource_from_dict(d, resource=None, full_clean=True, copy_dict=True, default_to_not_provided=False): # type: (Dict[str, Any], R, bool, bool, bool) -> Instance[R] """ Create a resource from a dict. :param d: dictionary of data. :param resource: A resource type, resource name or list of resources and names to use as the base for creating a resource. If a list is supplied the first item will be used if a resource type is not supplied; this could also be a parent(s) of any resource defined by the dict. :param full_clean: Perform a full clean as part of the creation. :param copy_dict: Use a copy of the input dictionary rather than destructively processing the input dict. :param default_to_not_provided: If an value is not supplied keep the value as NOT_PROVIDED. This is used to support merging an updated value. """ if not isinstance(d, dict): raise TypeError('`d` must be a dict instance.') if copy_dict: d = d.copy() if resource: resource_type = None # Convert to single resource then resolve document type if isinstance(resource, (tuple, list)): resources = (resolve_resource_type(r) for r in resource) else: resources = [resolve_resource_type(resource)] for resource_name, type_field in resources: # See if the input includes a type field and check it's registered document_resource_name = d.get(type_field, None) if document_resource_name: resource_type = registration.get_resource( document_resource_name) else: resource_type = registration.get_resource(resource_name) if not resource_type: raise exceptions.ResourceException( "Resource `%s` is not registered." % document_resource_name) if document_resource_name: # Check resource types match or are inherited types if (resource_name == document_resource_name or resource_name in getmeta(resource_type).parent_resource_names): break # We are done else: break if not resource_type: raise exceptions.ResourceException( "Incoming resource does not match [%s]" % ', '.join(r for r, t in resources)) else: # No resource specified, relay on type field document_resource_name = d.pop(DEFAULT_TYPE_FIELD, None) if not document_resource_name: raise exceptions.ResourceException("Resource not defined.") # Get an instance of a resource type resource_type = registration.get_resource(document_resource_name) if not resource_type: raise exceptions.ResourceException( "Resource `%s` is not registered." % document_resource_name) attrs = [] errors = {} meta = getmeta(resource_type) for f in meta.init_fields: value = d.pop(f.name, NotProvided) if value is NotProvided: if not default_to_not_provided: value = f.get_default( ) if f.use_default_if_not_provided else None else: try: value = f.to_python(value) except ValidationError as ve: errors[f.name] = ve.error_messages attrs.append(value) if errors: raise ValidationError(errors) new_resource = resource_type(*attrs) if d: new_resource.extra_attrs(d) if full_clean: new_resource.full_clean() return new_resource
def __str__(self): return '%s resource' % getmeta(self).resource_name
def create_resource_from_dict(d, resource=None, full_clean=True, copy_dict=True, default_to_not_provided=False): # type: (Dict[str, Any], R, bool, bool, bool) -> Instance[R] """ Create a resource from a dict. :param d: dictionary of data. :param resource: A resource type, resource name or list of resources and names to use as the base for creating a resource. If a list is supplied the first item will be used if a resource type is not supplied; this could also be a parent(s) of any resource defined by the dict. :param full_clean: Perform a full clean as part of the creation. :param copy_dict: Use a copy of the input dictionary rather than destructively processing the input dict. :param default_to_not_provided: If an value is not supplied keep the value as NOT_PROVIDED. This is used to support merging an updated value. """ if not isinstance(d, dict): raise TypeError('`d` must be a dict instance.') if copy_dict: d = d.copy() if resource: resource_type = None # Convert to single resource then resolve document type if isinstance(resource, (tuple, list)): resources = (resolve_resource_type(r) for r in resource) else: resources = [resolve_resource_type(resource)] for resource_name, type_field in resources: # See if the input includes a type field and check it's registered document_resource_name = d.get(type_field, None) if document_resource_name: resource_type = registration.get_resource(document_resource_name) else: resource_type = registration.get_resource(resource_name) if not resource_type: raise exceptions.ResourceException("Resource `%s` is not registered." % document_resource_name) if document_resource_name: # Check resource types match or are inherited types if (resource_name == document_resource_name or resource_name in getmeta(resource_type).parent_resource_names): break # We are done else: break if not resource_type: raise exceptions.ResourceException( "Incoming resource does not match [%s]" % ', '.join(r for r, t in resources)) else: # No resource specified, relay on type field document_resource_name = d.pop(DEFAULT_TYPE_FIELD, None) if not document_resource_name: raise exceptions.ResourceException("Resource not defined.") # Get an instance of a resource type resource_type = registration.get_resource(document_resource_name) if not resource_type: raise exceptions.ResourceException("Resource `%s` is not registered." % document_resource_name) attrs = [] errors = {} meta = getmeta(resource_type) for f in meta.init_fields: value = d.pop(f.name, NotProvided) if value is NotProvided: if not default_to_not_provided: value = f.get_default() if f.use_default_if_not_provided else None else: try: value = f.to_python(value) except ValidationError as ve: errors[f.name] = ve.error_messages attrs.append(value) if errors: raise ValidationError(errors) new_resource = resource_type(*attrs) if d: new_resource.extra_attrs(d) if full_clean: new_resource.full_clean() return new_resource
def resource_reference(resource): return "{} <{}.{}>".format(getmeta(resource).resource_name, resource.__module__, resource.__name__)