import dateutil.parser
from datetime import datetime
from .utils import snake_case, camel_case, base_path_for, sanitize_date
"""
contentful_management.resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module implements the Resource, FieldResource, PublishResource, ArchiveResource and Link classes.
API reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes
:copyright: (c) 2018 by Contentful GmbH.
:license: MIT, see LICENSE for more details.
"""
[docs]class Resource(object):
"""
Base resource class.
Implements common resource attributes.
API reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes
"""
def __init__(self, item, default_locale='en-US', client=None):
self.raw = item
self.default_locale = default_locale
self._client = client
self.sys = self._hydrate_sys(item)
[docs] @classmethod
def base_url(klass, space_id='', resource_id=None, environment_id=None, **kwargs):
"""
Returns the URI for the resource.
"""
url = "spaces/{0}".format(
space_id)
if environment_id is not None:
url = url = "{0}/environments/{1}".format(url, environment_id)
url = "{0}/{1}".format(
url,
base_path_for(klass.__name__)
)
if resource_id:
url = "{0}/{1}".format(url, resource_id)
return url
[docs] @classmethod
def create_attributes(klass, attributes, previous_object=None):
"""
Attributes for resource creation.
"""
result = {}
if previous_object is not None:
result = {k: v for k, v in previous_object.to_json().items() if k != 'sys'}
result.update(attributes)
return result
[docs] @classmethod
def update_attributes_map(klass):
"""
Defines keys and default values for non-generic attributes.
"""
return {}
[docs] def delete(self):
"""
Deletes the resource.
"""
return self._client._delete(
self.__class__.base_url(
self.sys['space'].id,
self.sys['id'],
environment_id=self._environment_id
)
)
[docs] def update(self, attributes=None):
"""
Updates the resource with attributes.
"""
if attributes is None:
attributes = {}
headers = self.__class__.create_headers(attributes)
headers.update(self._update_headers())
result = self._client._put(
self._update_url(),
self.__class__.create_attributes(attributes, self),
headers=headers
)
self._update_from_resource(result)
return self
[docs] def save(self):
"""
Saves the current state of the resource.
"""
return self.update()
[docs] def reload(self, result=None):
"""
Reloads the resource.
"""
if result is None:
result = self._client._get(
self.__class__.base_url(
self.sys['space'].id,
self.sys['id'],
environment_id=self._environment_id
)
)
self._update_from_resource(result)
return self
[docs] def to_link(self):
"""
Returns a link for the resource.
"""
link_type = self.link_type if self.type == 'Link' else self.type
return Link({'sys': {'linkType': link_type, 'id': self.sys.get('id')}}, client=self._client)
[docs] def to_json(self):
"""
Returns the JSON representation of the resource.
"""
result = {
'sys': {}
}
for k, v in self.sys.items():
if k in ['space', 'content_type', 'created_by',
'updated_by', 'published_by']:
v = v.to_json()
if k in ['created_at', 'updated_at', 'deleted_at',
'first_published_at', 'published_at', 'expires_at']:
v = v.isoformat()
result['sys'][camel_case(k)] = v
return result
def _hydrate_sys(self, item):
sys = {}
for k, v in item['sys'].items():
if k in self._linkables():
v = self._build_link(v)
if k in self._dateables():
v = dateutil.parser.parse(v)
sys[snake_case(k)] = v
return sys
def _linkables(self):
return ['space', 'contentType', 'createdBy',
'updatedBy', 'publishedBy', 'environment']
def _dateables(self):
return ['createdAt', 'updatedAt', 'deletedAt',
'firstPublishedAt', 'publishedAt', 'expiresAt']
def _build_link(self, link):
return Link(link, client=self._client)
def _update_headers(self):
return {'x-contentful-version': str(self.sys['version'])}
def _update_url(self):
return self.__class__.base_url(
self.sys['space'].id,
self.sys['id'],
environment_id=self._environment_id
)
def _update_from_resource(self, other):
if hasattr(other, 'sys'):
self.sys = other.sys
for attr, default in self.__class__.update_attributes_map().items():
value = getattr(other, attr, default)
self_value = getattr(self, attr)
if value == default and value != self_value:
value = self_value
setattr(self, attr, value)
@property
def _environment_id(self):
"""
Returns the Environment ID.
"""
try:
return super(Resource, self)._environment_id
except AttributeError:
# In Resources which do not inherit EnvironmentAwareResource an AttributeError will happen
return None
def __getattr__(self, name, *args, **kwargs):
if name in ['__getstate__', '__setstate__']:
return super(Resource, self).__getattr__(name, *args, **kwargs)
if name in self.sys:
return self.sys[name]
raise AttributeError(
"'{0}' object has no attribute '{1}'".format(
self.__class__.__name__,
name
)
)
[docs]class FieldsResource(Resource):
"""
Fields resource class.
Implements locale handling for resource fields.
"""
[docs] @classmethod
def create_attributes(klass, attributes, previous_object=None):
"""
Attributes for resource creation.
"""
if 'fields' not in attributes:
if previous_object is None:
attributes['fields'] = {}
else:
attributes['fields'] = previous_object.to_json()['fields']
return {'fields': attributes['fields']}
def __init__(self, item, **kwargs):
super(FieldsResource, self).__init__(item, **kwargs)
self._fields = self._hydrate_fields(item)
[docs] def fields(self, locale=None):
"""
Get fields for a specific locale.
:param locale: (optional) Locale to fetch, defaults to default_locale.
"""
if locale is None:
locale = self._locale()
return self._fields.get(locale, {})
[docs] def fields_with_locales(self):
"""
Get fields with locales per field.
"""
result = {}
for locale, fields in self._fields.items():
for k, v in fields.items():
real_field_id = self._real_field_id_for(k)
if real_field_id not in result:
result[real_field_id] = {}
result[real_field_id][locale] = self._serialize_value(v)
return result
[docs] def to_json(self):
"""
Returns the JSON Representation of the resource.
"""
result = super(FieldsResource, self).to_json()
result['fields'] = self.fields_with_locales()
return result
@property
def locale(self):
"""
Returns the resource locale.
"""
return self.sys.get('locale', None)
def _real_field_id_for(self, field_id):
for raw_field_id in self.raw['fields'].keys():
if snake_case(raw_field_id) == field_id:
return raw_field_id
def _serialize_value(self, value):
if isinstance(value, Resource):
return value.to_link().to_json()
elif isinstance(value, list) and value:
if isinstance(value[0], Resource):
return [resource.to_link().to_json() for resource in value]
elif isinstance(value, datetime):
return value.isoformat()
return value
def _hydrate_fields(self, item):
fields = {}
if 'fields' not in item:
return fields
for k, locales in item['fields'].items():
for locale, v in locales.items():
if locale not in fields:
fields[locale] = {}
fields[locale][snake_case(k)] = self._coerce(v)
return fields
def _coerce(self, value):
return value
def _locale(self):
return self.locale or self.__dict__['default_locale']
def _update_from_resource(self, other):
super(FieldsResource, self)._update_from_resource(other)
if hasattr(other, '_fields'):
self._fields = other._fields
def __getattr__(self, name, *args, **kwargs):
if name in ['__getstate__', '__setstate__']:
return super(FieldsResource, self).__getattr__(name, *args, **kwargs)
locale = self._locale()
if name in self._fields.get(locale, {}):
return self._fields[locale][name]
return super(FieldsResource, self).__getattr__(name)
def __setattr__(self, name, value):
if name not in ['raw', 'sys', 'default_locale',
'_client', '_fields', '__CONTENT_TYPE__', '_metadata']:
locale = self._locale()
if (name in self._fields.get(locale, {}) or
self._is_missing_field(name)):
if locale not in self._fields:
self._fields[locale] = {}
self._fields[locale][name] = value
return self._fields[locale][name]
return super(FieldsResource, self).__setattr__(name, value)
def _is_missing_field(self, name):
"""
By default, fields not appearing on responses are considered
as object meta-data, and they will not be added to `_fields`,
making them not part of the serialization when sent back to
the API for saving.
"""
return False
[docs]class PublishResource(object):
"""
Allows for resource publish/unpublish.
"""
@property
def is_published(self):
"""
Checks if resource is published.
"""
return bool(self.sys.get('published_at', False))
@property
def is_updated(self):
"""
Checks if a resource has been updated since last publish.
Returns False if resource has not been published before.
"""
if not self.is_published:
return False
return sanitize_date(self.sys['published_at']) < sanitize_date(self.sys['updated_at'])
[docs] def publish(self):
"""
Publishes the resource.
"""
result = self._client._put(
"{0}/published".format(
self.__class__.base_url(
self.sys['space'].id,
self.sys['id'],
environment_id=self._environment_id
),
),
{},
headers=self._update_headers()
)
return self.reload(result)
[docs] def unpublish(self):
"""
Unpublishes the resource.
"""
self._client._delete(
"{0}/published".format(
self.__class__.base_url(
self.sys['space'].id,
self.sys['id'],
environment_id=self._environment_id
),
),
headers=self._update_headers()
)
return self.reload()
[docs]class ArchiveResource(object):
"""
Allows for resource archive/unarchive.
"""
@property
def is_archived(self):
"""
Checks if Resource is archived.
"""
return bool(self.sys.get('archived_version', False))
[docs] def archive(self):
"""
Archives the resource.
"""
self._client._put(
"{0}/archived".format(
self.__class__.base_url(
self.sys['space'].id,
self.sys['id'],
environment_id=self._environment_id
),
),
{},
headers=self._update_headers()
)
return self.reload()
[docs] def unarchive(self):
"""
Unarchives the resource.
"""
self._client._delete(
"{0}/archived".format(
self.__class__.base_url(
self.sys['space'].id,
self.sys['id'],
environment_id=self._environment_id
),
),
headers=self._update_headers()
)
return self.reload()
[docs]class EnvironmentAwareResource(object):
"""
Allows environment aware resources to resolve the environment ID.
"""
@property
def _environment_id(self):
"""
Returns the Environment ID.
"""
environment = self.sys.get('environment', None)
if environment is not None:
return environment.id
return 'master'
[docs]class Link(Resource):
"""
Link Class
API reference: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/links
"""
[docs] def resolve(self, space_id=None, environment_id=None):
"""
Resolves link to a specific resource.
"""
proxy_method = getattr(
self._client,
base_path_for(self.link_type)
)
if self.link_type == 'Space':
return proxy_method().find(self.id)
elif environment_id is not None:
return proxy_method(space_id, environment_id).find(self.id)
else:
return proxy_method(space_id).find(self.id)
[docs] def to_json(self):
"""
Returns the JSON representation of the link.
"""
return {
'sys': {
'type': 'Link',
'linkType': self.sys.get('link_type'),
'id': self.sys.get('id')
}
}
def __repr__(self):
return "<Link[{0}] id='{1}'>".format(
self.link_type,
self.id
)