PK!;odoorpc_utils/__init__.pyfrom odoorpc_utils.odoorpc_instantiator import OdooInstantiator from odoorpc_utils.fast_deep_queries import FastDeepQueries, ID_FIELD_TUPLE_ID_NAME, ID_FIELD_LIST_ID, ID_FIELD_INT PK!a9>!!"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, force_method: str = 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 isinstance(force_method, str) and force_method in ['read', 'search_read', 'search']: method = force_method 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 method in ['read', 'search_read'] and 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: dict or list, domain=None, ids=None, order=None, limit=None, offset=None, format_for__id_fields: int = None, n_threads: int = 6, model_name: str = None): assert (fields is not None) if isinstance(fields, list): if model_name is None: if len(fields) == 2 and isinstance(fields[0], str) and isinstance(fields[1], list): fields = self.build_fields_deep_structure(model_name=fields[0], fields=fields[1]) else: raise ValueError("fields is either a fields_deep structure as obtained from " "'build_fields_deep_structure' or a tuple ('model_name', fields) or " "simply a list with fields if you also pass the model_name argument.") else: fields = self.build_fields_deep_structure(model_name=model_name, fields=fields) if format_for__id_fields is None: format_for__id_fields = self.format_for__id_fields retval = self.query_fast(model_name=fields['model_name'], domain=domain, ids=ids, fields=fields['fields'], order=order, limit=limit, offset=offset, format_for__id_fields=format_for__id_fields) related_fields = fields.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=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.7.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.7.dist-info/WHEEL HM K-*ϳR03rOK-J,/R(O-)$qzd&Y)r$UV&UrPK!H>;&odoorpc_utils-1.0.7.dist-info/METADATAXr7}WtÑd9bV[8rb3 (a}O^lbﮪT pqeZQ +k&<"9![[j`<*FV%=K4!HGdt+q+^Be7++63=7y*i<6avO^YWoE; E밴&]:ܻ %v,tX'[.~ nN9 uB;=).ޜi὚+ *MT ?zNL-:D{X.het)̢i \૓/]_~;wYndk ֭ώS-S2>&HƷ1Bq]]%WRyjliӼ3('~6>`Ͻ@H‡UԸwmk:q>*i[Z|,ZĮ,?)ЕeIs NFRol$ƺ[,ۯ/E1Oa+]xqs^!ҭxKϒ"NT /QˈN/e紶N"q%߃yQ;ӋCguM+.#ӏ-7ʚZLa됳]j9p"bcd@R:$fNGc6 9aؽDŽ=ù؍L_0 @lP!$ne:= \FG)Ĕ!o*a"Өp ={1!ԀbW`3BWaC~: w-CۡPh]"uqg*PW. kt@_ WJuA7MCѧɈVFʃ%oS\Ӣ`j6E".bTՒbi6q"&g:h͸ 䃃EUh\SY BNwO7t_teE{Qs4GJl$-5I*;I: JL?vKNּSk*@@wgra]-|,ΧK宗(=|& ίSdaΟ~G̹5\qRk)9O>1H@ נb/c#7L 6)7lX]X MǫA')qV>ގ ^x1to̺܎ Ok-[QnƪtQOcz `L80Im~{x}GHo/~6ՈH)|yoygxėi k_m*qU|+7)7[Y{kW_CA5v7GGْc,qtxU"6)c53w&3HV7m*rv͎QmwiAMKm&h*Zr!F`ҟl1"<uV)zK뙫jU62:>y&<ǡYO{`}.ڑݎ3J؎-Bw2uԑ?]YQb$y|d<ﶡݛ[. AP&F [hyJD(Qz1JzS}w94"LglM/;yPiτ;Th̋_y;tʼn{ڀ&tfȽޞwEL8Y XH-־O1A] p?K*}xElMun(HP.\%gif?..}&nŀ1eӡra!>9HPK!HI1]hG$odoorpc_utils-1.0.7.dist-info/RECORDйr@>ςP"˨a,ñO4$i,+->rɕ=9ie7ITMig ?Nc@/E'$xO]I1X!s\]ދA9~*d3D*qԄA[jף-EQ -g`g|vQ]t>Z{s$TʲBYKc Tsu@twh{Xu:uY7sN=OW+<ةT_qv k߾WAvPsp'ڰ-$k'ƦAK LT3fJYfc >gW  PK!;odoorpc_utils/__init__.pyPK!a9>!!"odoorpc_utils/fast_deep_queries.pyPK!%"odoorpc_utils/odoorpc_instantiator.pyPK!r6}%2odoorpc_utils-1.0.7.dist-info/LICENSEPK!Hu)GTU#3odoorpc_utils-1.0.7.dist-info/WHEELPK!H>;&*4odoorpc_utils-1.0.7.dist-info/METADATAPK!HI1]hG$t<odoorpc_utils-1.0.7.dist-info/RECORDPK4>