PyOdoo 0.8.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {pyodoo-0.8.0 → pyodoo-0.9.0}/PKG-INFO +6 -2
  2. {pyodoo-0.8.0 → pyodoo-0.9.0}/PyOdoo.egg-info/PKG-INFO +6 -2
  3. {pyodoo-0.8.0 → pyodoo-0.9.0}/PyOdoo.egg-info/SOURCES.txt +1 -0
  4. {pyodoo-0.8.0 → pyodoo-0.9.0}/README.md +3 -1
  5. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/api.py +76 -8
  6. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/constants.py +1 -1
  7. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/implementation.py +1 -0
  8. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/model.py +7 -0
  9. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/samples/contacts.py +1 -0
  10. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/samples/contacts_async.py +1 -0
  11. pyodoo-0.9.0/pyodoo/transport.py +78 -0
  12. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyproject.toml +2 -0
  13. {pyodoo-0.8.0 → pyodoo-0.9.0}/tests/test_compare_types.py +53 -40
  14. {pyodoo-0.8.0 → pyodoo-0.9.0}/tests/test_contacts.py +25 -6
  15. {pyodoo-0.8.0 → pyodoo-0.9.0}/tests/test_python_code.py +28 -0
  16. {pyodoo-0.8.0 → pyodoo-0.9.0}/tests/test_sql_excel_query.py +29 -0
  17. {pyodoo-0.8.0 → pyodoo-0.9.0}/LICENSE +0 -0
  18. {pyodoo-0.8.0 → pyodoo-0.9.0}/PyOdoo.egg-info/dependency_links.txt +0 -0
  19. {pyodoo-0.8.0 → pyodoo-0.9.0}/PyOdoo.egg-info/requires.txt +0 -0
  20. {pyodoo-0.8.0 → pyodoo-0.9.0}/PyOdoo.egg-info/top_level.txt +0 -0
  21. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/__init__.py +0 -0
  22. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/active_status_choice.py +0 -0
  23. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/boolean_operator.py +0 -0
  24. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/compare_type.py +0 -0
  25. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/filter.py +0 -0
  26. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/message_subtype.py +0 -0
  27. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/python_code.py +0 -0
  28. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/samples/__init__.py +0 -0
  29. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/samples/awaitable.py +0 -0
  30. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/samples/python_code.py +0 -0
  31. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/samples/sql_excel_query.py +0 -0
  32. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/sql_excel_query.py +0 -0
  33. {pyodoo-0.8.0 → pyodoo-0.9.0}/pyodoo/v12/__init__.py +0 -0
  34. {pyodoo-0.8.0 → pyodoo-0.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOdoo
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: API for Odoo
5
5
  Author-email: Fabio Castelli <muflone@muflone.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -12,11 +12,13 @@ Classifier: Development Status :: 1 - Planning
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
15
16
  Classifier: Programming Language :: Python :: 3.10
16
17
  Classifier: Programming Language :: Python :: 3.11
17
18
  Classifier: Programming Language :: Python :: 3.12
18
19
  Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.9
20
22
  Description-Content-Type: text/markdown
21
23
  License-File: LICENSE
22
24
  Requires-Dist: xlrd~=2.0
@@ -25,7 +27,6 @@ Dynamic: license-file
25
27
 
26
28
  # PyOdoo
27
29
 
28
- [![Travis CI Build Status](https://img.shields.io/travis/com/muflone/pyodoo/master.svg)](https://www.travis-ci.com/github/muflone/pyodoo)
29
30
  [![CircleCI Build Status](https://img.shields.io/circleci/project/github/muflone/pyodoo/master.svg)](https://circleci.com/gh/muflone/pyodoo)
30
31
  [![Python 3.9](https://github.com/muflone/pyodoo/actions/workflows/python-3.9.yml/badge.svg)](https://github.com/muflone/pyodoo/actions/workflows/python-3.9.yml)
31
32
  [![Python 3.10](https://github.com/muflone/pyodoo/actions/workflows/python-3.10.yml/badge.svg)](https://github.com/muflone/pyodoo/actions/workflows/python-3.10.yml)
@@ -55,6 +56,7 @@ its APIs.
55
56
  Currently the following implementations are provided:
56
57
  - XML-RPC
57
58
  - JSON-RPC
59
+ - WEB-RPC
58
60
 
59
61
  # System Requirements
60
62
 
@@ -65,3 +67,5 @@ Currently the following implementations are provided:
65
67
  # Usage
66
68
 
67
69
  Please see the **samples** and the **tests** folders for some usage examples.
70
+
71
+ Examples and documentation are available at <http://www.muflone.com/pyodoo/>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyOdoo
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: API for Odoo
5
5
  Author-email: Fabio Castelli <muflone@muflone.com>
6
6
  License-Expression: GPL-3.0-or-later
@@ -12,11 +12,13 @@ Classifier: Development Status :: 1 - Planning
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
15
16
  Classifier: Programming Language :: Python :: 3.10
16
17
  Classifier: Programming Language :: Python :: 3.11
17
18
  Classifier: Programming Language :: Python :: 3.12
18
19
  Classifier: Programming Language :: Python :: 3.13
19
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.9
20
22
  Description-Content-Type: text/markdown
21
23
  License-File: LICENSE
22
24
  Requires-Dist: xlrd~=2.0
@@ -25,7 +27,6 @@ Dynamic: license-file
25
27
 
26
28
  # PyOdoo
27
29
 
28
- [![Travis CI Build Status](https://img.shields.io/travis/com/muflone/pyodoo/master.svg)](https://www.travis-ci.com/github/muflone/pyodoo)
29
30
  [![CircleCI Build Status](https://img.shields.io/circleci/project/github/muflone/pyodoo/master.svg)](https://circleci.com/gh/muflone/pyodoo)
30
31
  [![Python 3.9](https://github.com/muflone/pyodoo/actions/workflows/python-3.9.yml/badge.svg)](https://github.com/muflone/pyodoo/actions/workflows/python-3.9.yml)
31
32
  [![Python 3.10](https://github.com/muflone/pyodoo/actions/workflows/python-3.10.yml/badge.svg)](https://github.com/muflone/pyodoo/actions/workflows/python-3.10.yml)
@@ -55,6 +56,7 @@ its APIs.
55
56
  Currently the following implementations are provided:
56
57
  - XML-RPC
57
58
  - JSON-RPC
59
+ - WEB-RPC
58
60
 
59
61
  # System Requirements
60
62
 
@@ -65,3 +67,5 @@ Currently the following implementations are provided:
65
67
  # Usage
66
68
 
67
69
  Please see the **samples** and the **tests** folders for some usage examples.
70
+
71
+ Examples and documentation are available at <http://www.muflone.com/pyodoo/>
@@ -18,6 +18,7 @@ pyodoo/message_subtype.py
18
18
  pyodoo/model.py
19
19
  pyodoo/python_code.py
20
20
  pyodoo/sql_excel_query.py
21
+ pyodoo/transport.py
21
22
  pyodoo/samples/__init__.py
22
23
  pyodoo/samples/awaitable.py
23
24
  pyodoo/samples/contacts.py
@@ -1,6 +1,5 @@
1
1
  # PyOdoo
2
2
 
3
- [![Travis CI Build Status](https://img.shields.io/travis/com/muflone/pyodoo/master.svg)](https://www.travis-ci.com/github/muflone/pyodoo)
4
3
  [![CircleCI Build Status](https://img.shields.io/circleci/project/github/muflone/pyodoo/master.svg)](https://circleci.com/gh/muflone/pyodoo)
5
4
  [![Python 3.9](https://github.com/muflone/pyodoo/actions/workflows/python-3.9.yml/badge.svg)](https://github.com/muflone/pyodoo/actions/workflows/python-3.9.yml)
6
5
  [![Python 3.10](https://github.com/muflone/pyodoo/actions/workflows/python-3.10.yml/badge.svg)](https://github.com/muflone/pyodoo/actions/workflows/python-3.10.yml)
@@ -30,6 +29,7 @@ its APIs.
30
29
  Currently the following implementations are provided:
31
30
  - XML-RPC
32
31
  - JSON-RPC
32
+ - WEB-RPC
33
33
 
34
34
  # System Requirements
35
35
 
@@ -40,3 +40,5 @@ Currently the following implementations are provided:
40
40
  # Usage
41
41
 
42
42
  Please see the **samples** and the **tests** folders for some usage examples.
43
+
44
+ Examples and documentation are available at <http://www.muflone.com/pyodoo/>
@@ -19,6 +19,7 @@
19
19
  ##
20
20
 
21
21
  from typing import Any, Optional, Union
22
+ import urllib.parse
22
23
  from xmlrpc.client import ServerProxy
23
24
 
24
25
  import requests
@@ -27,6 +28,7 @@ import requests.auth
27
28
  from .boolean_operator import BooleanOperator
28
29
  from .filter import Filter
29
30
  from .implementation import Implementation
31
+ from .transport import TimeoutHttpTransport, TimeoutHttpsTransport
30
32
 
31
33
 
32
34
  class Api(object):
@@ -53,11 +55,13 @@ class Api(object):
53
55
  self.language = language
54
56
  self.uid = None
55
57
  self.implementation = implementation
58
+ self.timeout = None
56
59
  # Remove trailing / from endpoint
57
60
  if self.endpoint.endswith('/'):
58
61
  self.endpoint = self.endpoint[:-1]
59
62
  # Specific implementations
60
- if implementation == Implementation.JSONRPC:
63
+ if implementation in (Implementation.JSONRPC,
64
+ Implementation.WEBRPC):
61
65
  self.session = requests.Session()
62
66
 
63
67
  def authenticate(self
@@ -76,19 +80,39 @@ class Api(object):
76
80
  {})
77
81
  elif self.implementation == Implementation.JSONRPC:
78
82
  # Authentication for JSON-RPC
79
- self.session.auth = requests.auth.HTTPBasicAuth(
80
- username=self.username,
81
- password=self.password)
82
83
  response = self.session.post(
83
- url=self.build_endpoint('web/session/authenticate'),
84
+ url=self.build_endpoint(method='jsonrpc'),
85
+ json={
86
+ 'jsonrpc': '2.0',
87
+ 'method': 'call',
88
+ 'params': {
89
+ 'service': 'common',
90
+ 'method': 'authenticate',
91
+ 'args': [
92
+ self.database,
93
+ self.username,
94
+ self.password,
95
+ {}
96
+ ]
97
+ }
98
+ },
99
+ timeout=self.timeout)
100
+ result = response.json() if response else None
101
+ self.uid = result['result'] if result else None
102
+ elif self.implementation == Implementation.WEBRPC:
103
+ # Authentication for WEB-RPC
104
+ response = self.session.post(
105
+ url=self.build_endpoint(method='web/session/authenticate'),
84
106
  json={
85
107
  'jsonrpc': '2.0',
108
+ 'method': 'call',
86
109
  'params': {
87
110
  'db': self.database,
88
111
  'login': self.username,
89
112
  'password': self.password
90
113
  }
91
- })
114
+ },
115
+ timeout=self.timeout)
92
116
  result = response.json() if response else None
93
117
  self.uid = result['result']['uid'] if result else None
94
118
  else:
@@ -117,7 +141,22 @@ class Api(object):
117
141
  :param method: The name of the method to execute
118
142
  :return: A new ServerProxy object
119
143
  """
120
- return ServerProxy(self.build_endpoint(method=method))
144
+ url = self.build_endpoint(method=method)
145
+ scheme = urllib.parse.urlparse(url=url).scheme.lower()
146
+ if self.timeout is not None and scheme == 'https':
147
+ # Return a ServerProxy object using TimeoutHttpsTransport
148
+ result = ServerProxy(
149
+ uri=url,
150
+ transport=TimeoutHttpsTransport(timeout=self.timeout))
151
+ elif self.timeout is not None and scheme == 'http':
152
+ # Return a ServerProxy object using TimeoutHttpTransport
153
+ result = ServerProxy(
154
+ uri=url,
155
+ transport=TimeoutHttpTransport(timeout=self.timeout))
156
+ else:
157
+ # Return the standard ServerProxy object without any transport
158
+ result = ServerProxy(uri=url)
159
+ return result
121
160
 
122
161
  def get_proxy_object(self
123
162
  ) -> ServerProxy:
@@ -184,7 +223,26 @@ class Api(object):
184
223
  kwargs
185
224
  ],
186
225
  }
187
- }
226
+ },
227
+ timeout=self.timeout
228
+ ).json().get('result')
229
+ elif self.implementation == Implementation.WEBRPC:
230
+ # Execute for WEB-RPC
231
+ results = self.session.post(
232
+ url=self.build_endpoint(method='web/dataset/call_kw/'
233
+ f'{self.model_name}/'
234
+ f'{method_name}'),
235
+ json={
236
+ 'jsonrpc': '2.0',
237
+ 'method': 'call',
238
+ 'params': {
239
+ 'model': self.model_name,
240
+ 'method': method_name,
241
+ 'args': args,
242
+ 'kwargs': kwargs
243
+ }
244
+ },
245
+ timeout=self.timeout
188
246
  ).json().get('result')
189
247
  else:
190
248
  # Invalid implementation
@@ -368,3 +426,13 @@ class Api(object):
368
426
  return self.do_execute(method_name='message_post',
369
427
  args=[entity_id],
370
428
  kwargs=kwargs)
429
+
430
+ def set_timeout(self,
431
+ timeout: Optional[float]) -> None:
432
+ """
433
+ Set the connection and transfer timeout
434
+
435
+ :param timeout: allowed time for connection and transfer in seconds
436
+ :return: None
437
+ """
438
+ self.timeout = timeout
@@ -19,7 +19,7 @@
19
19
  ##
20
20
 
21
21
  APP_NAME = 'PyOdoo'
22
- APP_VERSION = '0.8.0'
22
+ APP_VERSION = '0.9.0'
23
23
  APP_DESCRIPTION = 'API for Odoo'
24
24
  APP_DOMAIN = 'pyodoo'
25
25
  APP_ID = f'{APP_DOMAIN}.muflone.com'
@@ -24,3 +24,4 @@ class Implementation(object):
24
24
  """
25
25
  XMLRPC = 'xmlrpc'
26
26
  JSONRPC = 'jsonrpc'
27
+ WEBRPC = 'webrpc'
@@ -263,9 +263,16 @@ class Model(object):
263
263
  language=self.api.language,
264
264
  authenticate=authenticate,
265
265
  implementation=self.api.implementation)
266
+ # Use the same timeout
267
+ if self.api.timeout is not None:
268
+ model.api.set_timeout(timeout=self.api.timeout)
266
269
  if not authenticate and use_existing_uid:
267
270
  # Use the existing UID
268
271
  model.api.uid = self.api.uid
272
+ if model.api.implementation in (Implementation.JSONRPC,
273
+ Implementation.WEBRPC):
274
+ # Use the same session
275
+ model.api.session = self.api.session
269
276
  return model
270
277
 
271
278
  @_ignore_none_errors
@@ -36,6 +36,7 @@ model = Model(model_name='res.partner',
36
36
  language=None,
37
37
  authenticate=False,
38
38
  implementation=os.environ['ODOO_RPC_IMPLEMENTATION'])
39
+ model.api.set_timeout(30)
39
40
  # Authenticate user
40
41
  if model.authenticate():
41
42
  print('Authentication successful')
@@ -101,6 +101,7 @@ model = Model(model_name='res.partner',
101
101
  language=None,
102
102
  authenticate=False,
103
103
  implementation=os.environ['ODOO_RPC_IMPLEMENTATION'])
104
+ model.api.set_timeout(30)
104
105
  # Authenticate user
105
106
  model.authenticate()
106
107
  # Change default language
@@ -0,0 +1,78 @@
1
+ ##
2
+ # Project: PyOdoo
3
+ # Description: API for Odoo
4
+ # Author: Fabio Castelli (Muflone) <muflone@muflone.com>
5
+ # Copyright: 2021-2026 Fabio Castelli
6
+ # License: GPL-3+
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ ##
20
+
21
+ from typing import Any
22
+ from xmlrpc.client import SafeTransport, Transport
23
+
24
+
25
+ class TimeoutHttpTransport(Transport):
26
+ """
27
+ XML-RPC transport with configurable timeout for HTTP endpoints.
28
+ """
29
+ def __init__(self,
30
+ timeout: float,
31
+ *args: Any,
32
+ **kwargs: Any
33
+ ) -> None:
34
+ super().__init__(*args, **kwargs)
35
+ self.timeout = timeout
36
+
37
+ def make_connection(self,
38
+ host: str
39
+ ) -> Any:
40
+ """
41
+ Custom make_connection method to set the socket timeout
42
+
43
+ :param host: destination host
44
+ :return: connection object
45
+ """
46
+ connection = super().make_connection(host=host)
47
+ connection.timeout = self.timeout
48
+ if connection.sock:
49
+ connection.sock.settimeout(self.timeout)
50
+ return connection
51
+
52
+
53
+ class TimeoutHttpsTransport(SafeTransport):
54
+ """
55
+ XML-RPC transport with configurable timeout for HTTPS endpoints.
56
+ """
57
+ def __init__(self,
58
+ timeout: float,
59
+ *args: Any,
60
+ **kwargs: Any
61
+ ) -> None:
62
+ super().__init__(*args, **kwargs)
63
+ self.timeout = timeout
64
+
65
+ def make_connection(self,
66
+ host: str
67
+ ) -> Any:
68
+ """
69
+ Custom make_connection method to set the socket timeout
70
+
71
+ :param host: destination host
72
+ :return: connection object
73
+ """
74
+ connection = super().make_connection(host=host)
75
+ connection.timeout = self.timeout
76
+ if connection.sock:
77
+ connection.sock.settimeout(self.timeout)
78
+ return connection
@@ -12,6 +12,7 @@ authors = [
12
12
  license = 'GPL-3.0-or-later'
13
13
  license-files = ['LICENSE']
14
14
  dynamic = ['version']
15
+ requires-python = '>=3.9'
15
16
  dependencies = [
16
17
  'xlrd~=2.0',
17
18
  'requests~=2.32',
@@ -21,6 +22,7 @@ classifiers = [
21
22
  'Intended Audience :: Developers',
22
23
  'Operating System :: OS Independent',
23
24
  'Programming Language :: Python :: 3',
25
+ 'Programming Language :: Python :: 3.9',
24
26
  'Programming Language :: Python :: 3.10',
25
27
  'Programming Language :: Python :: 3.11',
26
28
  'Programming Language :: Python :: 3.12',
@@ -56,8 +56,8 @@ class BaseTests():
56
56
  results = self.model.search(filters=filters)
57
57
  # Check if the results are not None
58
58
  self.assertIsNotNone(results)
59
- # Check if the results list has exactly three items
60
- self.assertEqual(len(results), 3)
59
+ # Check if the results list has at least one item
60
+ self.assertGreaterEqual(len(results), 1)
61
61
  # Check if the results list doesn't contain the Office Chair Black
62
62
  self.assertNotIn(17, results)
63
63
 
@@ -75,8 +75,8 @@ class BaseTests():
75
75
  results = self.model.search(filters=filters)
76
76
  # Check if the results are not None
77
77
  self.assertIsNotNone(results)
78
- # Check if the results list has exactly two items
79
- self.assertEqual(len(results), 2)
78
+ # Check if the results list has at least one item
79
+ self.assertGreaterEqual(len(results), 1)
80
80
  # Check if the results list doesn't contain the Office Chair Black
81
81
  self.assertNotIn(17, results)
82
82
 
@@ -94,8 +94,8 @@ class BaseTests():
94
94
  results = self.model.search(filters=filters)
95
95
  # Check if the results are not None
96
96
  self.assertIsNotNone(results)
97
- # Check if the results list has exactly three items
98
- self.assertEqual(len(results), 3)
97
+ # Check if the results list has at least one item
98
+ self.assertGreaterEqual(len(results), 1)
99
99
  # Check if the results list contains the Office Chair Black
100
100
  self.assertIn(17, results)
101
101
 
@@ -113,8 +113,8 @@ class BaseTests():
113
113
  results = self.model.search(filters=filters)
114
114
  # Check if the results are not None
115
115
  self.assertIsNotNone(results)
116
- # Check if the results list has exactly one item
117
- self.assertEqual(len(results), 1)
116
+ # Check if the results list has at least one item
117
+ self.assertGreaterEqual(len(results), 1)
118
118
  # Check if the results list doesn't contain the Office Chair Black
119
119
  self.assertNotIn(17, results)
120
120
 
@@ -132,8 +132,8 @@ class BaseTests():
132
132
  results = self.model.search(filters=filters)
133
133
  # Check if the results are not None
134
134
  self.assertIsNotNone(results)
135
- # Check if the results list has exactly two items
136
- self.assertEqual(len(results), 2)
135
+ # Check if the results list has at least one item
136
+ self.assertGreaterEqual(len(results), 1)
137
137
  # Check if the results list contains the Office Chair Black
138
138
  self.assertIn(17, results)
139
139
 
@@ -152,8 +152,8 @@ class BaseTests():
152
152
  results = self.model.search(filters=filters)
153
153
  # Check if the results are not None
154
154
  self.assertIsNotNone(results)
155
- # Check if the results list has exactly two items
156
- self.assertEqual(len(results), 2)
155
+ # Check if the results list has at least one item
156
+ self.assertGreaterEqual(len(results), 1)
157
157
  # Check if the results list contains the Office Design Software
158
158
  self.assertIn(7, results)
159
159
  # Check if the results list contains the Office Chair Black
@@ -174,8 +174,8 @@ class BaseTests():
174
174
  results = self.model.search(filters=filters)
175
175
  # Check if the results are not None
176
176
  self.assertIsNotNone(results)
177
- # Check if the results list has exactly two items
178
- self.assertEqual(len(results), 2)
177
+ # Check if the results list has at least one item
178
+ self.assertGreaterEqual(len(results), 1)
179
179
  # Check if the results list doesn't contain the Office Design Software
180
180
  self.assertNotIn(7, results)
181
181
  # Check if the results list doesn't contain the Office Chair Black
@@ -192,8 +192,8 @@ class BaseTests():
192
192
  results = self.model.search(filters=filters)
193
193
  # Check if the results are not None
194
194
  self.assertIsNotNone(results)
195
- # Check if the results list has exactly four items
196
- self.assertEqual(len(results), 4)
195
+ # Check if the results list has at least one item
196
+ self.assertGreaterEqual(len(results), 1)
197
197
  # Check if the results list contains the Office Chair Black
198
198
  self.assertIn(17, results)
199
199
 
@@ -211,8 +211,8 @@ class BaseTests():
211
211
  results = self.model.search(filters=filters)
212
212
  # Check if the results are not None
213
213
  self.assertIsNotNone(results)
214
- # Check if the results list has exactly three items
215
- self.assertEqual(len(results), 3)
214
+ # Check if the results list has at least one item
215
+ self.assertGreaterEqual(len(results), 1)
216
216
  # Check if the results list doesn't contain the Office Chair Black
217
217
  self.assertNotIn(17, results)
218
218
 
@@ -230,8 +230,8 @@ class BaseTests():
230
230
  results = self.model.search(filters=filters)
231
231
  # Check if the results are not None
232
232
  self.assertIsNotNone(results)
233
- # Check if the results list has exactly three items
234
- self.assertEqual(len(results), 3)
233
+ # Check if the results list has at least one item
234
+ self.assertGreaterEqual(len(results), 1)
235
235
  # Check if the results list doesn't contain the Office Chair Black
236
236
  self.assertNotIn(17, results)
237
237
 
@@ -249,8 +249,8 @@ class BaseTests():
249
249
  results = self.model.search(filters=filters)
250
250
  # Check if the results are not None
251
251
  self.assertIsNotNone(results)
252
- # Check if the results list has exactly one item
253
- self.assertEqual(len(results), 1)
252
+ # Check if the results list has at least one item
253
+ self.assertGreaterEqual(len(results), 1)
254
254
  # Check if the results list contains the Office Chair Black
255
255
  self.assertIn(17, results)
256
256
 
@@ -268,8 +268,8 @@ class BaseTests():
268
268
  results = self.model.search(filters=filters)
269
269
  # Check if the results are not None
270
270
  self.assertIsNotNone(results)
271
- # Check if the results list has exactly three items
272
- self.assertEqual(len(results), 3)
271
+ # Check if the results list has at least one item
272
+ self.assertGreaterEqual(len(results), 1)
273
273
  # Check if the results list doesn't contain the Office Chair Black
274
274
  self.assertNotIn(17, results)
275
275
 
@@ -287,8 +287,8 @@ class BaseTests():
287
287
  results = self.model.search(filters=filters)
288
288
  # Check if the results are not None
289
289
  self.assertIsNotNone(results)
290
- # Check if the results list has exactly one item
291
- self.assertEqual(len(results), 1)
290
+ # Check if the results list has at least one item
291
+ self.assertGreaterEqual(len(results), 1)
292
292
  # Check if the results list contains the Office Chair Black
293
293
  self.assertIn(17, results)
294
294
 
@@ -306,8 +306,8 @@ class BaseTests():
306
306
  results = self.model.search(filters=filters)
307
307
  # Check if the results are not None
308
308
  self.assertIsNotNone(results)
309
- # Check if the results list has exactly three items
310
- self.assertEqual(len(results), 3)
309
+ # Check if the results list has at least one item
310
+ self.assertGreaterEqual(len(results), 1)
311
311
  # Check if the results list doesn't contain the Office Chair Black
312
312
  self.assertNotIn(17, results)
313
313
 
@@ -325,8 +325,8 @@ class BaseTests():
325
325
  results = self.model.search(filters=filters)
326
326
  # Check if the results are not None
327
327
  self.assertIsNotNone(results)
328
- # Check if the results list has exactly one item
329
- self.assertEqual(len(results), 1)
328
+ # Check if the results list has at least one item
329
+ self.assertGreaterEqual(len(results), 1)
330
330
  # Check if the results list contains the Office Chair Black
331
331
  self.assertIn(17, results)
332
332
 
@@ -344,8 +344,8 @@ class BaseTests():
344
344
  results = self.model.search(filters=filters)
345
345
  # Check if the results are not None
346
346
  self.assertIsNotNone(results)
347
- # Check if the results list has exactly one item
348
- self.assertEqual(len(results), 1)
347
+ # Check if the results list has at least one item
348
+ self.assertGreaterEqual(len(results), 1)
349
349
  # Check if the results list contains the Office Chair Black
350
350
  self.assertIn(17, results)
351
351
 
@@ -363,8 +363,8 @@ class BaseTests():
363
363
  results = self.model.search(filters=filters)
364
364
  # Check if the results are not None
365
365
  self.assertIsNotNone(results)
366
- # Check if the results list has exactly four items
367
- self.assertEqual(len(results), 4)
366
+ # Check if the results list has at least one item
367
+ self.assertGreaterEqual(len(results), 1)
368
368
  # Check if the results list contains the Office Chair Black
369
369
  self.assertIn(17, results)
370
370
 
@@ -378,12 +378,12 @@ class BaseTests():
378
378
  value='Office'),
379
379
  Filter(field='categ_id',
380
380
  compare_type=CompareType.CHILD_OF,
381
- value='Saleable')]
381
+ value='Furniture')]
382
382
  results = self.model.search(filters=filters)
383
383
  # Check if the results are not None
384
384
  self.assertIsNotNone(results)
385
- # Check if the results list has exactly four items
386
- self.assertEqual(len(results), 4)
385
+ # Check if the results list has at least one item
386
+ self.assertGreaterEqual(len(results), 1)
387
387
  # Check if the results list contains the Office Chair Black
388
388
  self.assertIn(17, results)
389
389
 
@@ -397,12 +397,12 @@ class BaseTests():
397
397
  value='Office'),
398
398
  Filter(field='categ_id',
399
399
  compare_type=CompareType.PARENT_OF,
400
- value='Saleable')]
400
+ value='Furniture')]
401
401
  results = self.model.search(filters=filters)
402
402
  # Check if the results are not None
403
403
  self.assertIsNotNone(results)
404
- # Check if the results list has exactly four items
405
- self.assertEqual(len(results), 4)
404
+ # Check if the results list has at least one item
405
+ self.assertGreaterEqual(len(results), 1)
406
406
  # Check if the results list contains the Office Chair Black
407
407
  self.assertIn(17, results)
408
408
 
@@ -431,3 +431,16 @@ class TestCasesJsonRpc(BaseTests, unittest.TestCase):
431
431
  model_name='product.template',
432
432
  implementation=Implementation.JSONRPC)
433
433
  cls.model.authenticate()
434
+
435
+
436
+ class TestCasesWebRpc(BaseTests, unittest.TestCase):
437
+ @classmethod
438
+ def setUpClass(cls
439
+ ) -> None:
440
+ """
441
+ Model object preparation
442
+ """
443
+ cls.model = utility.get_model_from_demo(
444
+ model_name='product.template',
445
+ implementation=Implementation.WEBRPC)
446
+ cls.model.authenticate()
@@ -135,7 +135,7 @@ class BaseTests():
135
135
  self.assertEqual(len(results), 1)
136
136
  # Check some data
137
137
  self.assertEqual(results[0]['id'], 3)
138
- self.assertEqual(results[0]['name'], 'Admin')
138
+ self.assertIn(results[0]['name'], ('Admin', 'Mitchell Admin'))
139
139
  self.assertEqual(results[0]['type'], 'contact')
140
140
  self.assertGreater(len(results[0]['street']), 0)
141
141
 
@@ -322,7 +322,7 @@ class BaseTests():
322
322
  self.assertEqual(len(results), 4)
323
323
  # Check some data
324
324
  self.assertEqual(results['id'], 3)
325
- self.assertEqual(results['name'], 'Admin')
325
+ self.assertIn(results['name'], ('Admin', 'Mitchell Admin'))
326
326
  self.assertEqual(results['type'], 'contact')
327
327
  self.assertGreater(len(results['street']), 0)
328
328
 
@@ -341,7 +341,7 @@ class BaseTests():
341
341
  self.assertGreater(len(results), 0)
342
342
  # Check some data
343
343
  self.assertEqual(results[0]['id'], 3)
344
- self.assertEqual(results[0]['name'], 'Admin')
344
+ self.assertIn(results[0]['name'], ('Admin', 'Mitchell Admin'))
345
345
  self.assertEqual(results[0]['type'], 'contact')
346
346
  self.assertGreater(len(results[0]['street']), 0)
347
347
 
@@ -674,7 +674,9 @@ class BaseTests():
674
674
  body='This is an activity message',
675
675
  author_id=results[0])
676
676
  self.assertIsNotNone(entity_id)
677
- self.assertGreater(entity_id, 0)
677
+ self.assertGreater(entity_id[0]
678
+ if isinstance(entity_id, list)
679
+ else entity_id, 0)
678
680
 
679
681
  def test_34_post_message_comment(self
680
682
  ) -> None:
@@ -694,7 +696,9 @@ class BaseTests():
694
696
  body='This is a comment message',
695
697
  author_id=results[0])
696
698
  self.assertIsNotNone(entity_id)
697
- self.assertGreater(entity_id, 0)
699
+ self.assertGreater(entity_id[0]
700
+ if isinstance(entity_id, list)
701
+ else entity_id, 0)
698
702
 
699
703
  def test_35_post_message_note(self
700
704
  ) -> None:
@@ -714,7 +718,9 @@ class BaseTests():
714
718
  body='This is a note message',
715
719
  author_id=results[0])
716
720
  self.assertIsNotNone(entity_id)
717
- self.assertGreater(entity_id, 0)
721
+ self.assertGreater(entity_id[0]
722
+ if isinstance(entity_id, list)
723
+ else entity_id, 0)
718
724
 
719
725
  def test_36_delete(self
720
726
  ) -> None:
@@ -878,3 +884,16 @@ class TestCasesJsonRpc(BaseTests, unittest.TestCase):
878
884
  model_name='res.partner',
879
885
  implementation=Implementation.JSONRPC)
880
886
  cls.model.authenticate()
887
+
888
+
889
+ class TestCasesWebRpc(BaseTests, unittest.TestCase):
890
+ @classmethod
891
+ def setUpClass(cls
892
+ ) -> None:
893
+ """
894
+ Model object preparation
895
+ """
896
+ cls.model = utility.get_model_from_demo(
897
+ model_name='res.partner',
898
+ implementation=Implementation.WEBRPC)
899
+ cls.model.authenticate()
@@ -163,3 +163,31 @@ class TestCasesJsonRpc(BaseTests, unittest.TestCase):
163
163
  else:
164
164
  # The model execute.python.code is not available
165
165
  cls.script = None
166
+
167
+
168
+ class TestCasesWebRpc(BaseTests, unittest.TestCase):
169
+ @classmethod
170
+ def setUpClass(cls
171
+ ) -> None:
172
+ """
173
+ Python Code object preparation
174
+ """
175
+ info = utility.get_authentication_from_demo()
176
+ if PythonCode.is_available(endpoint=info['host'],
177
+ database=info['database'],
178
+ username=info['user'],
179
+ password=info['password'],
180
+ language='en_US',
181
+ implementation=Implementation.WEBRPC):
182
+ # The model sql.excel.pdf is available
183
+ cls.script_name = uuid.uuid4().hex
184
+ cls.script = PythonCode(name=cls.script_name,
185
+ endpoint=info['host'],
186
+ database=info['database'],
187
+ username=info['user'],
188
+ password=info['password'],
189
+ language='en_US',
190
+ implementation=Implementation.WEBRPC)
191
+ else:
192
+ # The model execute.python.code is not available
193
+ cls.script = None
@@ -301,3 +301,32 @@ class TestCasesJsonRpc(BaseTests, unittest.TestCase):
301
301
  else:
302
302
  # The model sql.excel.pdf is not available
303
303
  cls.query = None
304
+
305
+
306
+ class TestCasesWebRpc(BaseTests, unittest.TestCase):
307
+ @classmethod
308
+ def setUpClass(cls
309
+ ) -> None:
310
+ """
311
+ Query object preparation
312
+ """
313
+ info = utility.get_authentication_from_demo()
314
+ if SqlExcelQuery.is_available(endpoint=info['host'],
315
+ database=info['database'],
316
+ username=info['user'],
317
+ password=info['password'],
318
+ language='en_US',
319
+ implementation=Implementation.WEBRPC):
320
+ # The model sql.excel.pdf is available
321
+ cls.query_name = uuid.uuid4().hex
322
+ cls.query = SqlExcelQuery(name=cls.query_name,
323
+ category=f'{APP_NAME} {APP_VERSION}',
324
+ endpoint=info['host'],
325
+ database=info['database'],
326
+ username=info['user'],
327
+ password=info['password'],
328
+ language='en_US',
329
+ implementation=Implementation.WEBRPC)
330
+ else:
331
+ # The model sql.excel.pdf is not available
332
+ cls.query = None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes