1515
1616from __future__ import annotations
1717
18- import ssl
19- import urllib . request
18+ import json
19+ import random
2020
2121from pathlib import Path
22- from typing import TYPE_CHECKING , Literal , Type , overload
22+ from typing import TYPE_CHECKING , Any , Type
23+
24+ import httpx
2325
24- from odoorpc import ODOO # type: ignore[import]
2526from packaging .version import Version
2627from typing_extensions import get_type_hints # 3.11 and later
2728
28- from ..util import is_subclass
29+ from ..util import JSONDecoder , JSONEncoder , is_subclass
2930from .record import RecordBase
3031from .record_manager import RecordManagerBase
3132
3233if TYPE_CHECKING :
33- from odoorpc .db import DB # type: ignore[import]
34- from odoorpc .env import Environment # type: ignore[import]
35- from odoorpc .report import Report # type: ignore[import]
34+ from collections .abc import Mapping
3635
3736
3837class ClientBase :
@@ -81,92 +80,22 @@ class ClientBase:
8180 :type version: str | None, optional
8281 """
8382
84- @overload
8583 def __init__ (
8684 self ,
8785 * ,
88- hostname : str | None = ...,
89- database : str | None = ...,
90- username : str | None = ...,
91- password : str | None = ...,
92- protocol : str = "jsonrpc" ,
93- port : int = 8069 ,
94- verify : bool | str | Path = ...,
95- version : str | None = ...,
96- odoo : ODOO ,
97- ) -> None : ...
98-
99- @overload
100- def __init__ (
101- self ,
102- * ,
103- hostname : str ,
86+ base_url : str ,
10487 database : str ,
10588 username : str ,
10689 password : str ,
107- protocol : str = "jsonrpc" ,
108- port : int = 8069 ,
109- verify : bool | str | Path = ...,
110- version : str | None = ...,
111- odoo : Literal [None ] = ...,
112- ) -> None : ...
113-
114- @overload
115- def __init__ (
116- self ,
117- * ,
118- hostname : str | None = ...,
119- database : str | None = ...,
120- username : str | None = ...,
121- password : str | None = ...,
122- protocol : str = "jsonrpc" ,
123- port : int = 8069 ,
124- verify : bool | str | Path = ...,
125- version : str | None = ...,
126- odoo : ODOO | None = ...,
127- ) -> None : ...
128-
129- def __init__ (
130- self ,
131- * ,
132- hostname : str | None = None ,
133- database : str | None = None ,
134- username : str | None = None ,
135- password : str | None = None ,
136- protocol : str = "jsonrpc" ,
137- port : int = 8069 ,
13890 verify : bool | str | Path = True ,
139- version : str | None = None ,
140- odoo : ODOO | None = None ,
91+ timeout : int | None = None ,
14192 ) -> None :
142- # If an OdooRPC object is provided, use that directly.
143- # Otherwise, make a new one with the provided settings.
144- if odoo :
145- self ._odoo = odoo
146- else :
147- opener = None
148- if protocol .endswith ("+ssl" ):
149- ssl_verify = verify is not False
150- ssl_cafile = (
151- str (verify ) if isinstance (verify , (Path , str )) else None
152- )
153- if not ssl_verify or ssl_cafile :
154- ssl_context = ssl .create_default_context (cafile = ssl_cafile )
155- if not ssl_verify :
156- ssl_context .check_hostname = False
157- ssl_context .verify_mode = ssl .CERT_NONE
158- opener = urllib .request .build_opener (
159- urllib .request .HTTPSHandler (context = ssl_context ),
160- urllib .request .HTTPCookieProcessor (),
161- )
162- self ._odoo = ODOO (
163- protocol = protocol ,
164- host = hostname ,
165- port = port ,
166- version = version ,
167- opener = opener ,
168- )
169- self ._odoo .login (database , username , password )
93+ self ._base_url = base_url
94+ self ._database = database
95+ self ._username = username
96+ self ._password = password
97+ self ._verify = str (verify ) if isinstance (verify , Path ) else verify
98+ self ._timeout = timeout
17099 self ._env_manager_mapping : dict [str , RecordManagerBase ] = {}
171100 """An internal mapping between env (model) names and their managers.
172101
@@ -189,47 +118,170 @@ def __init__(
189118 for attr_name , attr_type in get_type_hints (type (self )).items ():
190119 if is_subclass (attr_type , RecordManagerBase ):
191120 setattr (self , attr_name , attr_type (self ))
192-
193- @property
194- def odoo (self ) -> ODOO :
195- """The OdooRPC connection object currently being used
196- by this client.
197- """
198- return self ._odoo
199-
200- @property
201- def db (self ) -> DB :
202- """The database management service."""
203- return self ._odoo .db
204-
205- @property
206- def report (self ) -> Report :
207- """The report management service."""
208- return self ._odoo .report
209-
210- @property
211- def env (self ) -> Environment :
212- """The OdooRPC environment wrapper object.
213-
214- This allows interacting with models that do not have managers
215- within this Odoo client.
216- Usage is the same as on a native ``odoorpc.ODOO`` object.
217- """
218- return self ._odoo .env
121+ self .login ()
219122
220123 @property
221124 def user_id (self ) -> int :
222125 """The ID for the currently logged in user."""
223- return self ._odoo . env . uid
126+ return self ._user_id
224127
225128 @property
226129 def version (self ) -> Version :
227130 """The version of the server,
228131 as a comparable ``packaging.version.Version`` object.
229132 """
230- return Version ( self ._odoo . version )
133+ return self ._odoo_version
231134
232135 @property
233136 def version_str (self ) -> str :
234137 """The version of the server, as a string."""
235- return self ._odoo .version
138+ return self ._odoo_version_str
139+
140+ def _jsonrpc (
141+ self ,
142+ * ,
143+ params : Mapping [str , Any ] | None = None ,
144+ url : str = "/jsonrpc" ,
145+ ) -> httpx .Response :
146+ return self ._http_client .post (
147+ url ,
148+ headers = {"Content-Type" : "application/json" },
149+ content = json .dumps (
150+ {
151+ "jsonrpc" : "2.0" ,
152+ "method" : "call" ,
153+ "params" : dict (params ) if params else {},
154+ "id" : random .randint (0 , 1000000000 ), # noqa: S311
155+ },
156+ cls = JSONEncoder ,
157+ separators = ("," , ":" ),
158+ ),
159+ )
160+
161+ def login (self ) -> None :
162+ """Login to the Odoo database with the configured
163+ username and password.
164+
165+ Fetches and stores a new session cookie, usable until
166+ it expires after a pre-determined amount of time.
167+
168+ If a request is made to Odoo after the session cookie
169+ has expired, the Odoo client will automatically run this
170+ method to refresh the session cookie.
171+ """
172+ # Set up the HTTP client session that will be used
173+ # for all requests.
174+ self ._http_client = httpx .Client (
175+ base_url = self ._base_url ,
176+ verify = self ._verify ,
177+ timeout = self ._timeout ,
178+ )
179+ # Login, and set up the user context.
180+ response = self ._jsonrpc (
181+ params = {
182+ "service" : "common" ,
183+ "method" : "login" ,
184+ "args" : [self ._database , self ._username , self ._password ],
185+ },
186+ )
187+ # TODO(callumdickinson): Handle HTTP 401.
188+ user_id : int | None = response .json ()["result" ]
189+ if not user_id :
190+ # TODO(callumdickinson): Custom exception class.
191+ raise ValueError ("Incorrect username or password" )
192+ response = self ._jsonrpc (
193+ params = {
194+ "service" : "object" ,
195+ "method" : "execute" ,
196+ "args" : [
197+ self ._database ,
198+ self ._username ,
199+ self ._password ,
200+ "res.users" ,
201+ "context_get" ,
202+ ],
203+ },
204+ )
205+ context : dict [str , Any ] = response .json ()["result" ]
206+ context ["uid" ] = user_id
207+ self ._user_id = user_id
208+ self ._context = context
209+ # Discover the Odoo server's version.
210+ response = self ._jsonrpc (url = "/web/webclient/version_info" )
211+ self ._odoo_version_str : str = response .json ()["server_version" ]
212+ self ._odoo_version = Version (self ._odoo_version_str )
213+
214+ def close (self ) -> None :
215+ self ._http_client .close ()
216+
217+ def execute (
218+ self ,
219+ model : str ,
220+ method : str ,
221+ / ,
222+ * args : Any ,
223+ ) -> Any :
224+ """Invoke a method on the given model,
225+ passing all other positional arguments
226+ as parameters, and return the result.
227+
228+ :param model: The model to run the method on
229+ :type model: str
230+ :param method: The method to invoke
231+ :type method: str
232+ :return: The return value of the method
233+ :rtype: Any
234+ """
235+ response = self ._jsonrpc (
236+ params = {
237+ "service" : "object" ,
238+ "method" : "execute" ,
239+ "args" : [
240+ self ._database ,
241+ self ._user_id ,
242+ self ._password ,
243+ model ,
244+ method ,
245+ * args ,
246+ ],
247+ },
248+ )
249+ data : dict [str , Any ] = json .loads (response .text , cls = JSONDecoder )
250+ return data .get ("result" )
251+
252+ def execute_kw (
253+ self ,
254+ model : str ,
255+ method : str ,
256+ / ,
257+ * args : Any ,
258+ ** kwargs : Any ,
259+ ) -> Any :
260+ """Invoke a method on the given model,
261+ passing all other positional arguments
262+ and all keyword arguments as parameters,
263+ and return the result.
264+
265+ :param model: The model to run the method on
266+ :type model: str
267+ :param method: The method to invoke
268+ :type method: str
269+ :return: The return value of the method
270+ :rtype: Any
271+ """
272+ response = self ._jsonrpc (
273+ params = {
274+ "service" : "object" ,
275+ "method" : "execute_kw" ,
276+ "args" : [
277+ self ._database ,
278+ self ._user_id ,
279+ self ._password ,
280+ model ,
281+ method ,
282+ [args , kwargs ],
283+ ],
284+ },
285+ )
286+ data : dict [str , Any ] = json .loads (response .text , cls = JSONDecoder )
287+ return data .get ("result" )
0 commit comments