PK!odoorpc_utils/__init__.py__version__ = '0.2.0' from odoorpc_utils.odoorpc_instantiator import OdooInstantiator from odoorpc_utils.fast_deep_queries import FastDeepQueries PK!j"odoorpc_utils/fast_deep_queries.pyimport os import concurrent.futures import configparser QUERIES_INI_FILE = ['queries.ini', 'deep_queries.ini'] ID_FIELD_TUPLE_ID_NAME = 1 ID_FIELD_LIST_ID = 2 ID_FIELD_INT = 3 class FastDeepQueries: def __init__(self, odoo_obj, prefetched_metadata: dict = None, prefetch_model_names: list = None, queries_ini: str = None, format_for__id_fields: int = ID_FIELD_INT): self.format_for__id_fields = format_for__id_fields self.odoo = odoo_obj if prefetched_metadata: self.prefetched_metadata = prefetched_metadata else: self.prefetched_metadata = self.prefetch_metadata(prefetch_model_names) self.queries_parser = None if queries_ini is None: for f in QUERIES_INI_FILE: ini_name = os.path.join('.',f) if os.path.isfile(ini_name): with open(ini_name, 'r') as f: self.queries_parser = configparser.ConfigParser() self.queries_parser.read(ini_name) else: self.queries_parser = configparser.ConfigParser() self.queries_parser.read_string(queries_ini) def prefetch_metadata(self, model_names: list = None): domain = [('ttype', 'in', ['one2many', 'many2one'])] if isinstance(model_names, list) and len(model_names) > 0: domain.append(['model', 'in', model_names]) fields = self.query_fast('ir.model.fields', domain=domain, fields=['name', 'model', 'relation']) # will keep relations on its own 'relations' entry to permit changing the cache format # when we implement support for many2many fields later on cache = dict() for f in fields: cache.setdefault(f['model'], {'relations': {}})['relations'][f['name']] = f['relation'] return cache def query_fast(self, model_name, domain=None, ids=None, fields=None, order=None, limit=None, offset=None, format_for__id_fields: int = None): if format_for__id_fields is None: format_for__id_fields = self.format_for__id_fields if domain is not None and ids is not None: domain.append(('id', 'in', ids)) ids = None if domain is None and ids is None: domain = [] method: str = None args = [] if domain is not None and fields is not None: method = 'search_read' args = [domain] elif domain is not None: method = 'search' args = [domain] elif ids is not None: method, args = ('read', [ids]) if fields else ('search', [[('id', 'in', ids)]]) if method is None: raise(RuntimeError('Wrong paramaters on call to function query_opt')) kwargs = {'context': self.odoo.env.context} if isinstance(fields, list): kwargs['fields'] = fields if order: kwargs['order'] = order if limit: kwargs['limit'] = limit if offset: kwargs['offset'] = offset resp = self.odoo.execute_kw(model_name, method, args, kwargs) if format_for__id_fields != ID_FIELD_TUPLE_ID_NAME: for r in resp: for k in r.keys(): if isinstance(k, str) and k.endswith('_id') and isinstance(r[k], list) and len(r[k]) == 2: if format_for__id_fields == ID_FIELD_INT: r[k] = r[k][0] elif format_for__id_fields == ID_FIELD_LIST_ID: r[k].pop() return resp def build_fields_deep_structure(self, model_name, fields_deep: list): def _parse(f): splitted_field = f.split(sep='.', maxsplit=1) mfs = [x.strip() for x in splitted_field[0].split(',') if len(x) > 0] rfs = (mfs[-1], splitted_field[1]) if len(splitted_field) == 2 and len(mfs) > 0 else None return mfs, rfs mfs = list() rfs = dict() for f in fields_deep: mf, rf = _parse(f) mfs.extend(mf) if rf is not None: if rfs.get(rf[0]) is None: rfs[rf[0]] = [self.prefetched_metadata[model_name]['relations'][rf[0]]] rfs[rf[0]].append(rf[1]) # Logic to support '*', '*.*', 'product_id.*'-like syntax if '*' in mfs: mfs = '*' star = rfs.get('*') if star is not None: for f in self.prefetched_metadata[model_name]['relations'].keys(): if rfs.get(f) is None: rfs[f] = [self.prefetched_metadata[model_name]['relations'][f]] rfs[f].extend(star) del rfs['*'] if rfs: rfs = {f: self.build_fields_deep_structure(rfs[f][0], rfs[f][1:]) for f in rfs} else: rfs = None return {'model_name': model_name, 'fields': list(set(mfs)), 'related_fields': rfs if rfs else None} def query_deep(self, fields_deep: dict, domain=None, ids=None, order=None, limit=None, offset=None, format_for__id_fields: int = None, n_threads: int = 6): assert (fields_deep is not None) if format_for__id_fields is None: format_for__id_fields = self.format_for__id_fields retval = self.query_fast(model_name=fields_deep['model_name'], domain=domain, ids=ids, fields=fields_deep['fields'], order=order, limit=limit, offset=offset, format_for__id_fields=format_for__id_fields) related_fields = fields_deep.get('related_fields') if related_fields: rfield_names = related_fields.keys() related_ids = \ {f: list(set([i for r in retval for i in r.setdefault(f, [None]) or [None] if isinstance(i, int)])) for f in rfield_names} with concurrent.futures.ThreadPoolExecutor(max_workers=n_threads) as ex: calls = {ex.submit(self.query_deep, fields_deep=related_fields[f], ids=related_ids[f], format_for__id_fields=format_for__id_fields): f for f in rfield_names} cache = {} for future in concurrent.futures.as_completed(calls): f = calls[future] data = future.result() cache[f] = dict(zip(map(lambda x: x['id'], data), data)) for r in retval: r[f] = [cache[f][id] for id in r[f] or [None] if isinstance(id, int)] return retval def get_named_query_params(self, query_name: str): params = self.queries_parser[query_name] fd = params.get('fields') mn = params.get('model name') if fd is None or mn is None: raise ValueError("[queries.ini]: 'fields' and 'model name' are required parameters") fd = self.build_fields_deep_structure(mn, fd) del params['fields'] del params['model name'] params['fields_deep'] = fd return params def named_query(self, query_name: str, **kwargs): params = self.get_named_query_params(query_name) for key in kwargs: params[key] = kwargs[key] return self.query_deep(**params) PK!%odoorpc_utils/odoorpc_instantiator.pyfrom configparser import ConfigParser import os import shlex CONF_FILENAMES = ['odoorpc_utils.ini', 'odoorpc-utils.ini', 'odooly.ini'] class OdooInstantiator: def __init__(self, odoorpc_obj, config_opts: dict = None): self.odoo = odoorpc_obj self.config_opts = config_opts def get_client(self): return self.odoo def get_configopts(self): return self.config_opts @classmethod def from_config_file(cls, library_name: str, section: str, path: str = None, **kwargs): kwargs['section'] = section kwargs['path'] = path if library_name == 'odoorpc': return cls.odoorpc_from_config_file(**kwargs) elif library_name == 'odooly': return cls.odooly_from_config_file(**kwargs) elif library_name == 'odoo_fastrpc': return cls.fastrpc_from_config_file(**kwargs) @classmethod def fastrpc_from_config_file(cls, section: str, path: str = None, n_threads: int = 8): # TO-DO: will be part of this package raise NotImplementedError @classmethod def odoorpc_from_config_file(cls, section: str, path: str = None, opener=None): import odoorpc env = cls.parse_config(section, path) if env.get('host') is None: env['host'] = 'localhost' if env.get('protocol_odoorpc') is not None: env['protocol'] = env.get('protocol_odoorpc') if env.get('protocol') is None: env['protocol'] = 'jsonrpc+ssl' if env.get('scheme') == 'https' else 'jsonrpc' if env.get('port') is None: env['port'] = 8069 if env.get('timeout') is None: env['timeout'] = 120 if env.get('db') is None: env['db'] = env.get('database') if env.get('login') is None: env['login'] = env.get('user') odoo = odoorpc.ODOO(host=env['host'], protocol=env['protocol'], port=env['port'], timeout=env['timeout'], version=env.get('version'), opener=opener) odoo.login(env['db'], env['login'], env['password']) return cls(odoo, env) @classmethod def odooly_from_config_file(cls, section: str, path: str = None, user: str = None, transport=None, verbose: bool = False): import odooly env = cls.parse_config(section, path) if verbose is None: verbose = env.get('verbose') if env.get('scheme') is None: env['scheme'] = 'https' if env['scheme'] == 'local': env['server'] = shlex.split(env.get('options', '')) else: if env.get('protocol') is None: env['protocol'] = 'jsonrpc' env['server'] = '%s://%s:%s/%s' % (env['scheme'], env['host'], env['port'], env['protocol']) if env.get('database') is None and env.get('db') is not None: env['database'] = env['db'] if env.get('user') is None: env['user'] = env.get('login') if user is None else user elif user and user != env.get('user'): env['password'] = None client = odooly.Client(env['server'], user=env['user'], password=env['password'], transport=transport, verbose=verbose) client.env.name = section client.login() return cls(client, env) @classmethod def parse_config(cls, section: str, path: str = None): if path is None: for fn in CONF_FILENAMES: obj = cls.parse_config(section, os.path.join(os.curdir, fn)) if obj is not None: return obj return None if path[:1] != '/': path = os.path.join(os.curdir, path) p = ConfigParser() with os.open(path) as f: p.read_file(f) if section is None: return p.sections() return dict(p.items(section)) PK!r6}%odoorpc_utils-1.0.3.dist-info/LICENSECC0 LICENSE =========== To the extent possible under law, Marcelo Bello has waived all copyright and related or neighboring rights to odoorpc-utils. This work is published from: Brasil. PK!Hu)GTU#odoorpc_utils-1.0.3.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!H04T&odoorpc_utils-1.0.3.dist-info/METADATAXr7}WtÑd9bV[ujH̓g@Q~{N^lbﮪT pqeZQ +k&<,9![[j`<*Oon=;UAIOR#%dQ%9]l6r܊!~rxPa6LjmM^J ggGcKW~[Nㆂm:,IlnBDF(?;|u4V˭_SNqBNOʯ7gZxJʪc8qFNJ] 5sx[KU#ecnЬSL~N_.ɺrJ㭱DĞj2)xx^ݳ>}m:QsÈ*VjJY6Ƶ?lyu{{8x-~W-ISY>[x- bZBTʲs'K[Z76c-WA@'0.RM<89bKVV%gIW'*t[eD'ɗ2OsZێZ'8cK㨝EϡX_GHie\-uHy.L8Vh F12w`)3a0^cB{/ezZj6}|tDZ2X.ʣbN 7qϰ@iT8֞Jj@1+ƂeR銫0!D eaBTػ!Ot(@u}Ⱥ^M Be3D +SU|B5:/+Eº ؛&!dD+#k )iчty0Y5prnYPZjI 4\QȸjZ3jf\lGRѪg4a)άX!;ϧOKcgivE:2NW9#?gn6ǚ$ Z˝$Ji%~\xh'k^)] 뻳Wq9 fDήH>r^ΥrK>)~ 2ӰrO#.8){5Z ?$T[~k{G[i|ll6. SƓG+poHބ|E:V7^~fUnG絖s-(7c:̨1^pFULV0r|$6UKĽI>ꀣh$wܷ?pwyjD$^<㔛k߼5+䯡}i;䛣lI18|:h<*L˱;$n+ ݛMT?`j;fǨ4Z&6N[E-0fJ6N}m:n+j*ut<Ь=EF i0ۃ>nGIpOji%[lǖ;H Ղ.ìn f(1D>2\wP-mj(p#-d<%bc"NQ(B`ݜ3E6榗GUCg O*mV/żw:NĀqcmbb :3oϻ"&F,ńF,k p☠ y%>RQ"ph?&Ty7L@$dxE`T m>Q_Tb혲?VP0$PK!HfG$odoorpc_utils-1.0.3.dist-info/RECORD̽r0$: Z%G h #Oɻ|0ֵ9+ƴE{9[ ꒭'c{z J;Ply?I Eo}т QvoZ ('g;ڍhڴT$տ!p5fuxs%xt7a!w)?u`9&P. e-BM^w#z Q>)7j*Bwg}uƔI5'y!{7aOI\U-*widVWg ;dOEcE#giVݺ*,# rЌ{0bI|~p$PK!odoorpc_utils/__init__.pyPK!j"odoorpc_utils/fast_deep_queries.pyPK!%odoorpc_utils/odoorpc_instantiator.pyPK!r6}%.odoorpc_utils-1.0.3.dist-info/LICENSEPK!Hu)GTU#/odoorpc_utils-1.0.3.dist-info/WHEELPK!H04T&`0odoorpc_utils-1.0.3.dist-info/METADATAPK!HfG$8odoorpc_utils-1.0.3.dist-info/RECORDPK4R: