import six
import collections
__all__ = ('empty_cache', 'Cache', 'ObjectCache')
[docs]class ObjectCache(dict):
""" Cache for object / model data
Automatically generates empty data dicts for records requested.
Also contains object context
"""
__slots__ = ('_root_cache', '_object', '_context')
def __init__(self, root, obj, *args, **kwargs):
self._root_cache = root
self._object = obj
self._context = kwargs.pop('context', None)
super(ObjectCache, self).__init__(*args, **kwargs)
@property
def context(self):
""" Return context instance related to this cache
"""
return self._context
def __missing__(self, key):
self[key] = {'id': key}
return self[key]
[docs] def update_keys(self, keys):
""" Add new IDs to cache.
:param list keys: list of new IDs to be added to cache
:return: self
:rtype: ObjectCache
"""
if not self:
# for large amounts of data, this may be faster (no need for set
# and difference calls)
self.update({cid: {'id': cid} for cid in keys})
else:
self.update({cid: {'id': cid}
for cid in set(keys).difference(six.viewkeys(self))})
return self
[docs] def update_context(self, new_context):
""" Updates or sets new context for thes ObjectCache instance
:param dict new_context: context dictionary to update cached
context with
:return: updated context
"""
if new_context is not None:
if self.context is None:
self._context = dict(new_context)
else:
self._context.update(new_context)
return self.context
[docs] def get_ids_to_read(self, *fields):
""" Return list of ids, that have no at least one of specified
fields in cache
For example::
cache.get_ids_to_read('name', 'country_id', 'parent_id')
This code will traverse all record ids managed by this cache,
and find those that have no at least one field in cache.
This is highly useful in prefetching
"""
return [key for key, val in six.viewitems(self)
if any(((field not in val) for field in fields))]
[docs] def cache_field(self, rid, ftype, field_name, value):
""" This method impelment additional caching functionality,
like caching related fields, and so...
:param int rid: Record ID
:param str ftype: field type
:param str field_name: name of field
:param value: value to cache for field
"""
self[rid][field_name] = value
if value and ftype == 'many2one':
rcache = self._root_cache[self._object.
columns_info[field_name]['relation']]
if isinstance(value, six.integer_types): # pragma: no cover
# internal dict {'id': key} will be created by default
# (see ObjectCache.__missing__)
rcache[value]
elif isinstance(value, (list, tuple)):
# usualy for many2one fields odoo returns tuples like
# (id, name), where id is ID of remote record, and name
# is human readable name of record (result of name_get method)
# so we cache this name for futher usage too
rcache[value[0]]['__name_get_result'] = value[1]
elif value and ftype in ('many2many', 'one2many'):
rcache = self._root_cache[self._object.
columns_info[field_name]['relation']]
rcache.update_keys(value)
[docs] def parse_prefetch_fields(self, fields):
""" Parse fields to be prefetched, sparating, cache's object fields
and related fields.
Used internaly
:param list fields: list of fields to prefetch
:return: returns ``tuple(prefetch_fields, related_fields)``,
where ``prefetch_fields`` is list of fields,
to be read for current object, and ``related_fields`` is
dictionary of form:
``{'related.object': ['relatedfield1', 'relatedfield2.relatedfield']}``
:rtype: tuple
""" # noqa
rel_fields = collections.defaultdict(list)
prefetch_fields = set()
for field in fields:
field_path = field.split('.', 1)
xfield = field_path.pop(0)
xfield_info = self._object.columns_info.get(xfield, None)
if xfield_info is not None:
prefetch_fields.add(xfield)
relation = xfield_info.get('relation', False)
if field_path and relation:
# only one item left
rel_fields[relation].append(field_path[0])
return list(prefetch_fields), rel_fields
[docs] def prefetch_fields(self, fields):
""" Prefetch specified fields for this cache.
Also, dot (".") may be used in field name
to prefetch related fields::
cache.prefetch_fields(
['myfield1', 'myfields2_ids.relatedfield'])
:param list fields: list of fields to prefetch
"""
to_prefetch, related = self.parse_prefetch_fields(fields)
col_info = self._object.columns_info
for data in self._object.read(self.get_ids_to_read(*to_prefetch),
to_prefetch):
for field, value in data.items():
# Fill related cache
ftype = col_info.get(field, {}).get('type', None)
self.cache_field(data['id'], ftype, field, value)
if related:
# TODO: think how to avoid infinite recursion and double reads
for obj_name, rfields in related.items():
self._root_cache[obj_name].prefetch_fields(rfields)
[docs]class Cache(dict):
""" Cache to be used for Record's data.
This is root cache, which manages model local cache
cache['res.partner'] -> ObjectCache('res.partner')
"""
__slots__ = ('_client',)
def __init__(self, client, *args, **kwargs):
self._client = client
super(Cache, self).__init__(*args, **kwargs)
@property
def client(self):
""" Access to Client instance this cache belongs to
"""
return self._client
def __missing__(self, key):
try:
obj = self._client.get_obj(key)
except ValueError:
raise KeyError("There is no object with such name: %s" % key)
# TODO: FIX: Object caches generated without context
self[key] = ObjectCache(self, obj)
return self[key]
[docs]def empty_cache(client):
""" Create instance of empty cache for Record
:param Client client: instance of Client to create cache for
:return: instance of Cache class
:rtype: Cache
Cache is dictionary-like object with structure like::
cache = {
'product.product': {
1: {
'id': 1,
'name': 'product1',
'default_code': 'product1',
},
},
}
"""
return Cache(client)