biszx-odoo-mcp 1.1.0__py3-none-any.whl → 1.1.2__py3-none-any.whl
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.
- biszx_odoo_mcp/server/__init__.py +0 -0
- biszx_odoo_mcp/server/context.py +19 -0
- biszx_odoo_mcp/server/resources.py +230 -0
- biszx_odoo_mcp/server/response.py +36 -0
- biszx_odoo_mcp/server/tools.py +432 -0
- biszx_odoo_mcp/tools/__init__.py +0 -0
- biszx_odoo_mcp/tools/config.py +98 -0
- biszx_odoo_mcp/tools/odoo_client.py +632 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/METADATA +1 -1
- biszx_odoo_mcp-1.1.2.dist-info/RECORD +18 -0
- biszx_odoo_mcp-1.1.0.dist-info/RECORD +0 -10
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/WHEEL +0 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/entry_points.txt +0 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {biszx_odoo_mcp-1.1.0.dist-info → biszx_odoo_mcp-1.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Tools for Odoo integration
|
|
3
|
+
|
|
4
|
+
This module contains all the MCP tool functions for interacting with Odoo.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
from biszx_odoo_mcp.exceptions import OdooMCPError, ToolError
|
|
10
|
+
from biszx_odoo_mcp.server.context import AppContext
|
|
11
|
+
from biszx_odoo_mcp.server.response import Response
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
async def search_models(mcp: Any, query: str) -> dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Get a list of all available models in the Odoo system.
|
|
17
|
+
Args:
|
|
18
|
+
query: Search term to find models (searches in model name and display name)
|
|
19
|
+
Returns:
|
|
20
|
+
JSON string with matching models
|
|
21
|
+
"""
|
|
22
|
+
# Access lifespan context to get the Odoo client
|
|
23
|
+
ctx = mcp.get_context()
|
|
24
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
data = app_context.odoo.search_models(query)
|
|
28
|
+
return Response(data=data).to_dict()
|
|
29
|
+
except OdooMCPError as e:
|
|
30
|
+
return Response(error=e.to_dict()).to_dict()
|
|
31
|
+
except Exception as e:
|
|
32
|
+
tool_error = ToolError(
|
|
33
|
+
f"Unexpected error getting models: {str(e)}",
|
|
34
|
+
tool_name="search_models",
|
|
35
|
+
original_error=e,
|
|
36
|
+
)
|
|
37
|
+
return Response(error=tool_error.to_dict()).to_dict()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def get_model_info(mcp: Any, model_name: str) -> dict[str, Any]:
|
|
41
|
+
"""
|
|
42
|
+
Get information about a specific Odoo model.
|
|
43
|
+
Args:
|
|
44
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
45
|
+
Returns:
|
|
46
|
+
Dictionary with model information
|
|
47
|
+
"""
|
|
48
|
+
# Access lifespan context to get the Odoo client
|
|
49
|
+
ctx = mcp.get_context()
|
|
50
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
data = app_context.odoo.get_model_info(model_name)
|
|
54
|
+
return Response(data=data).to_dict()
|
|
55
|
+
except OdooMCPError as e:
|
|
56
|
+
return Response(error=e.to_dict()).to_dict()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
tool_error = ToolError(
|
|
59
|
+
f"Unexpected error getting model info: {str(e)}",
|
|
60
|
+
tool_name="get_model_info",
|
|
61
|
+
details={"model_name": model_name},
|
|
62
|
+
original_error=e,
|
|
63
|
+
)
|
|
64
|
+
return Response(error=tool_error.to_dict()).to_dict()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def get_model_fields(
|
|
68
|
+
mcp: Any, model_name: str, query_field: str
|
|
69
|
+
) -> dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Get field definitions for a specific Odoo model.
|
|
72
|
+
Args:
|
|
73
|
+
model_name: Name of the model (e.g., 'res.partner')
|
|
74
|
+
query_field: Search term to find fields (searches in field name and string)
|
|
75
|
+
Returns:
|
|
76
|
+
Dictionary with field definitions
|
|
77
|
+
"""
|
|
78
|
+
# Access lifespan context to get the Odoo client
|
|
79
|
+
ctx = mcp.get_context()
|
|
80
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
data = app_context.odoo.get_model_fields(model_name, query_field)
|
|
84
|
+
return Response(data=data).to_dict()
|
|
85
|
+
except OdooMCPError as e:
|
|
86
|
+
return Response(error=e.to_dict()).to_dict()
|
|
87
|
+
except Exception as e:
|
|
88
|
+
tool_error = ToolError(
|
|
89
|
+
f"Unexpected error getting model fields: {str(e)}",
|
|
90
|
+
tool_name="get_model_fields",
|
|
91
|
+
details={"model_name": model_name},
|
|
92
|
+
original_error=e,
|
|
93
|
+
)
|
|
94
|
+
return Response(error=tool_error.to_dict()).to_dict()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def search_records(
|
|
98
|
+
mcp: Any,
|
|
99
|
+
model_name: str,
|
|
100
|
+
domain: list[Any],
|
|
101
|
+
fields: list[str] | None = None,
|
|
102
|
+
limit: int | None = None,
|
|
103
|
+
offset: int | None = None,
|
|
104
|
+
order: str | None = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""
|
|
107
|
+
Search for records in an Odoo model.
|
|
108
|
+
Args:
|
|
109
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
110
|
+
domain: Search domain as list of tuples e.g., [['is_company', '=', true]]
|
|
111
|
+
fields: List of field names to return, None for all fields (default: None)
|
|
112
|
+
limit: Maximum number of records to return (default: None)
|
|
113
|
+
offset: Number of records to skip (default: None)
|
|
114
|
+
order: Sorting criteria e.g., 'name ASC, id DESC' (default: None)
|
|
115
|
+
Returns:
|
|
116
|
+
Dictionary with search results
|
|
117
|
+
"""
|
|
118
|
+
# Access lifespan context to get the Odoo client
|
|
119
|
+
ctx = mcp.get_context()
|
|
120
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
data = app_context.odoo.search_read(
|
|
124
|
+
model_name, domain, fields=fields, limit=limit, offset=offset, order=order
|
|
125
|
+
)
|
|
126
|
+
return Response(data=data).to_dict()
|
|
127
|
+
except OdooMCPError as e:
|
|
128
|
+
return Response(error=e.to_dict()).to_dict()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def read_records(
|
|
132
|
+
mcp: Any,
|
|
133
|
+
model_name: str,
|
|
134
|
+
ids: list[int],
|
|
135
|
+
fields: list[str] | None = None,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
"""
|
|
138
|
+
Read specific records by their IDs.
|
|
139
|
+
Args:
|
|
140
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
141
|
+
ids: List of record IDs to read
|
|
142
|
+
fields: List of field names to return, None for all fields (default: None)
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary with record data
|
|
145
|
+
"""
|
|
146
|
+
# Access lifespan context to get the Odoo client
|
|
147
|
+
ctx = mcp.get_context()
|
|
148
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
data = app_context.odoo.read_records(model_name, ids, fields=fields)
|
|
152
|
+
return Response(data=data).to_dict()
|
|
153
|
+
except OdooMCPError as e:
|
|
154
|
+
return Response(error=e.to_dict()).to_dict()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def create_record(
|
|
158
|
+
mcp: Any,
|
|
159
|
+
model_name: str,
|
|
160
|
+
values: dict[str, Any],
|
|
161
|
+
) -> dict[str, Any]:
|
|
162
|
+
"""
|
|
163
|
+
Create a new record in an Odoo model.
|
|
164
|
+
Args:
|
|
165
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
166
|
+
values: Dictionary with field values for the new record
|
|
167
|
+
Returns:
|
|
168
|
+
Dictionary with the created record ID
|
|
169
|
+
"""
|
|
170
|
+
# Access lifespan context to get the Odoo client
|
|
171
|
+
ctx = mcp.get_context()
|
|
172
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
record_id = app_context.odoo.create_records(model_name, [values])
|
|
176
|
+
return Response(data={"id": record_id}).to_dict()
|
|
177
|
+
except OdooMCPError as e:
|
|
178
|
+
return Response(error=e.to_dict()).to_dict()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def create_records(
|
|
182
|
+
mcp: Any,
|
|
183
|
+
model_name: str,
|
|
184
|
+
values_list: list[dict[str, Any]],
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
"""
|
|
187
|
+
Create multiple records in an Odoo model.
|
|
188
|
+
Args:
|
|
189
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
190
|
+
values_list: List of dictionaries with field values for the new records
|
|
191
|
+
Returns:
|
|
192
|
+
Dictionary with the created record IDs
|
|
193
|
+
"""
|
|
194
|
+
# Access lifespan context to get the Odoo client
|
|
195
|
+
ctx = mcp.get_context()
|
|
196
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
record_ids = app_context.odoo.create_records(model_name, values_list)
|
|
200
|
+
return Response(data={"ids": record_ids}).to_dict()
|
|
201
|
+
except OdooMCPError as e:
|
|
202
|
+
return Response(error=e.to_dict()).to_dict()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def write_record(
|
|
206
|
+
mcp: Any,
|
|
207
|
+
model_name: str,
|
|
208
|
+
record_id: int,
|
|
209
|
+
values: dict[str, Any],
|
|
210
|
+
) -> dict[str, Any]:
|
|
211
|
+
"""
|
|
212
|
+
Update a single record in an Odoo model.
|
|
213
|
+
Args:
|
|
214
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
215
|
+
record_id: ID of the record to update
|
|
216
|
+
values: Dictionary with field values to update
|
|
217
|
+
Returns:
|
|
218
|
+
Dictionary with operation result
|
|
219
|
+
"""
|
|
220
|
+
# Access lifespan context to get the Odoo client
|
|
221
|
+
ctx = mcp.get_context()
|
|
222
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
result = app_context.odoo.write_records(model_name, [record_id], values)
|
|
226
|
+
return Response(data={"success": result}).to_dict()
|
|
227
|
+
except OdooMCPError as e:
|
|
228
|
+
return Response(error=e.to_dict()).to_dict()
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def write_records(
|
|
232
|
+
mcp: Any,
|
|
233
|
+
model_name: str,
|
|
234
|
+
record_ids: list[int],
|
|
235
|
+
values: dict[str, Any],
|
|
236
|
+
) -> dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
Update multiple records in an Odoo model.
|
|
239
|
+
Args:
|
|
240
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
241
|
+
record_ids: List of record IDs to update
|
|
242
|
+
values: Dictionary with field values to update
|
|
243
|
+
Returns:
|
|
244
|
+
Dictionary with operation result
|
|
245
|
+
"""
|
|
246
|
+
# Access lifespan context to get the Odoo client
|
|
247
|
+
ctx = mcp.get_context()
|
|
248
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
result = app_context.odoo.write_records(model_name, record_ids, values)
|
|
252
|
+
return Response(data={"success": result}).to_dict()
|
|
253
|
+
except OdooMCPError as e:
|
|
254
|
+
return Response(error=e.to_dict()).to_dict()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def unlink_record(
|
|
258
|
+
mcp: Any,
|
|
259
|
+
model_name: str,
|
|
260
|
+
record_id: int,
|
|
261
|
+
) -> dict[str, Any]:
|
|
262
|
+
"""
|
|
263
|
+
Delete a single record from an Odoo model.
|
|
264
|
+
Args:
|
|
265
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
266
|
+
record_id: ID of the record to delete
|
|
267
|
+
Returns:
|
|
268
|
+
Dictionary with operation result
|
|
269
|
+
"""
|
|
270
|
+
# Access lifespan context to get the Odoo client
|
|
271
|
+
ctx = mcp.get_context()
|
|
272
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
result = app_context.odoo.unlink_records(model_name, [record_id])
|
|
276
|
+
return Response(data={"success": result}).to_dict()
|
|
277
|
+
except OdooMCPError as e:
|
|
278
|
+
return Response(error=e.to_dict()).to_dict()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
async def unlink_records(
|
|
282
|
+
mcp: Any,
|
|
283
|
+
model_name: str,
|
|
284
|
+
record_ids: list[int],
|
|
285
|
+
) -> dict[str, Any]:
|
|
286
|
+
"""
|
|
287
|
+
Delete multiple records from an Odoo model.
|
|
288
|
+
Args:
|
|
289
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
290
|
+
record_ids: List of record IDs to delete
|
|
291
|
+
Returns:
|
|
292
|
+
Dictionary with operation result
|
|
293
|
+
"""
|
|
294
|
+
# Access lifespan context to get the Odoo client
|
|
295
|
+
ctx = mcp.get_context()
|
|
296
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
result = app_context.odoo.unlink_records(model_name, record_ids)
|
|
300
|
+
return Response(data={"success": result}).to_dict()
|
|
301
|
+
except OdooMCPError as e:
|
|
302
|
+
return Response(error=e.to_dict()).to_dict()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
async def search_count(
|
|
306
|
+
mcp: Any,
|
|
307
|
+
model_name: str,
|
|
308
|
+
domain: list[Any],
|
|
309
|
+
) -> dict[str, Any]:
|
|
310
|
+
"""
|
|
311
|
+
Count records that match a search domain.
|
|
312
|
+
Args:
|
|
313
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
314
|
+
domain: Search domain as list of tuples e.g., [['is_company', '=', True]]
|
|
315
|
+
Returns:
|
|
316
|
+
Dictionary with the count of matching records
|
|
317
|
+
"""
|
|
318
|
+
# Access lifespan context to get the Odoo client
|
|
319
|
+
ctx = mcp.get_context()
|
|
320
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
count = app_context.odoo.search_count(model_name, domain)
|
|
324
|
+
return Response(data={"count": count}).to_dict()
|
|
325
|
+
except OdooMCPError as e:
|
|
326
|
+
return Response(error=e.to_dict()).to_dict()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
async def search_ids(
|
|
330
|
+
mcp: Any,
|
|
331
|
+
model_name: str,
|
|
332
|
+
domain: list[Any],
|
|
333
|
+
offset: int | None = None,
|
|
334
|
+
limit: int | None = None,
|
|
335
|
+
order: str | None = None,
|
|
336
|
+
) -> dict[str, Any]:
|
|
337
|
+
"""
|
|
338
|
+
Search for record IDs that match a domain.
|
|
339
|
+
Args:
|
|
340
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
341
|
+
domain: Search domain as list of tuples e.g., [['is_company', '=', True]]
|
|
342
|
+
offset: Number of records to skip (default: None)
|
|
343
|
+
limit: Maximum number of records to return (default: None)
|
|
344
|
+
order: Sorting criteria e.g., 'name ASC, id DESC' (default: None)
|
|
345
|
+
Returns:
|
|
346
|
+
Dictionary with list of matching record IDs
|
|
347
|
+
"""
|
|
348
|
+
# Access lifespan context to get the Odoo client
|
|
349
|
+
ctx = mcp.get_context()
|
|
350
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
ids = app_context.odoo.search_ids(
|
|
354
|
+
model_name, domain, offset=offset, limit=limit, order=order
|
|
355
|
+
)
|
|
356
|
+
return Response(data={"ids": ids}).to_dict()
|
|
357
|
+
except OdooMCPError as e:
|
|
358
|
+
return Response(error=e.to_dict()).to_dict()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
async def call_method(
|
|
362
|
+
mcp: Any,
|
|
363
|
+
model_name: str,
|
|
364
|
+
method_name: str,
|
|
365
|
+
args: list[Any] | None = None,
|
|
366
|
+
kwargs: dict[str, Any] | None = None,
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
"""
|
|
369
|
+
Call a custom method on an Odoo model.
|
|
370
|
+
Args:
|
|
371
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
372
|
+
method_name: Name of the method to call
|
|
373
|
+
args: Positional arguments to pass to the method (default: None)
|
|
374
|
+
kwargs: Keyword arguments to pass to the method (default: None)
|
|
375
|
+
Returns:
|
|
376
|
+
Dictionary with method result
|
|
377
|
+
"""
|
|
378
|
+
# Access lifespan context to get the Odoo client
|
|
379
|
+
ctx = mcp.get_context()
|
|
380
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
381
|
+
|
|
382
|
+
if args is None:
|
|
383
|
+
args = []
|
|
384
|
+
if kwargs is None:
|
|
385
|
+
kwargs = {}
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
result = app_context.odoo.call_method(model_name, method_name, args, kwargs)
|
|
389
|
+
return Response(data=result).to_dict()
|
|
390
|
+
except OdooMCPError as e:
|
|
391
|
+
return Response(error=e.to_dict()).to_dict()
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
async def search_and_update(
|
|
395
|
+
mcp: Any,
|
|
396
|
+
model_name: str,
|
|
397
|
+
domain: list[Any],
|
|
398
|
+
values: dict[str, Any],
|
|
399
|
+
) -> dict[str, Any]:
|
|
400
|
+
"""
|
|
401
|
+
Search for records and update them in one operation.
|
|
402
|
+
Args:
|
|
403
|
+
model_name: Name of the model e.g., 'res.partner'
|
|
404
|
+
domain: Search domain to find records to update
|
|
405
|
+
values: Dictionary with field values to update
|
|
406
|
+
Returns:
|
|
407
|
+
Dictionary with operation results including affected record count
|
|
408
|
+
"""
|
|
409
|
+
# Access lifespan context to get the Odoo client
|
|
410
|
+
ctx = mcp.get_context()
|
|
411
|
+
app_context = cast(AppContext, ctx.request_context.lifespan_context)
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
# First search for IDs
|
|
415
|
+
record_ids = app_context.odoo.search_ids(model_name, domain)
|
|
416
|
+
if not record_ids:
|
|
417
|
+
return Response(
|
|
418
|
+
data={"affected_records": 0, "message": "No records found"}
|
|
419
|
+
).to_dict()
|
|
420
|
+
|
|
421
|
+
# Then update the found records
|
|
422
|
+
result = app_context.odoo.write_records(model_name, record_ids, values)
|
|
423
|
+
return Response(
|
|
424
|
+
data={
|
|
425
|
+
"affected_records": len(record_ids),
|
|
426
|
+
"record_ids": record_ids,
|
|
427
|
+
"updated": result,
|
|
428
|
+
"update_result": result, # For backward compatibility
|
|
429
|
+
}
|
|
430
|
+
).to_dict()
|
|
431
|
+
except OdooMCPError as e:
|
|
432
|
+
return Response(error=e.to_dict()).to_dict()
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Odoo Configuration for MCP server integration
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
"""
|
|
14
|
+
Odoo client configuration
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
url: str
|
|
18
|
+
db: str
|
|
19
|
+
username: str
|
|
20
|
+
password: str
|
|
21
|
+
timeout: int
|
|
22
|
+
verify_ssl: bool
|
|
23
|
+
|
|
24
|
+
def __init__(self) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Initialize the Config object and load configuration
|
|
27
|
+
"""
|
|
28
|
+
self.__dict__.update(self.load_config())
|
|
29
|
+
self.url = self._prepare_url(self.url)
|
|
30
|
+
|
|
31
|
+
logger.info("🔧 Odoo client configuration:")
|
|
32
|
+
logger.info(f" URL: {self.url}")
|
|
33
|
+
logger.info(f" Database: {self.db}")
|
|
34
|
+
logger.info(f" Username: {self.username}")
|
|
35
|
+
logger.info(f" Timeout: {self.timeout}s")
|
|
36
|
+
logger.info(f" Verify SSL: {self.verify_ssl}")
|
|
37
|
+
|
|
38
|
+
def load_config(self) -> dict[str, Any]:
|
|
39
|
+
"""
|
|
40
|
+
Load Odoo configuration from environment variables or config file
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
OSError: If required environment variables are missing
|
|
44
|
+
Returns:
|
|
45
|
+
dict: Configuration parameters for Odoo client
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
self._validate_config()
|
|
49
|
+
return {
|
|
50
|
+
"url": os.environ["ODOO_URL"],
|
|
51
|
+
"db": os.environ["ODOO_DB"],
|
|
52
|
+
"username": os.environ["ODOO_USERNAME"],
|
|
53
|
+
"password": os.environ["ODOO_PASSWORD"],
|
|
54
|
+
"timeout": int(os.environ.get("ODOO_TIMEOUT", "30")),
|
|
55
|
+
"verify_ssl": os.environ.get("ODOO_VERIFY_SSL", "1").lower()
|
|
56
|
+
in ("1", "true", "yes"),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def _validate_config(self) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Validate the loaded configuration parameters
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If any required parameter is missing or invalid
|
|
64
|
+
"""
|
|
65
|
+
required_env = {"ODOO_URL", "ODOO_DB", "ODOO_USERNAME", "ODOO_PASSWORD"}
|
|
66
|
+
for var in required_env:
|
|
67
|
+
if var not in os.environ:
|
|
68
|
+
raise OSError(f"Missing required environment variable: {var}")
|
|
69
|
+
if "ODOO_TIMEOUT" in os.environ:
|
|
70
|
+
try:
|
|
71
|
+
int(os.environ["ODOO_TIMEOUT"])
|
|
72
|
+
except ValueError:
|
|
73
|
+
raise ValueError("ODOO_TIMEOUT must be an integer") from None
|
|
74
|
+
if "ODOO_VERIFY_SSL" in os.environ and os.environ[
|
|
75
|
+
"ODOO_VERIFY_SSL"
|
|
76
|
+
].lower() not in (
|
|
77
|
+
"1",
|
|
78
|
+
"true",
|
|
79
|
+
"yes",
|
|
80
|
+
"0",
|
|
81
|
+
"false",
|
|
82
|
+
"no",
|
|
83
|
+
):
|
|
84
|
+
raise ValueError("ODOO_VERIFY_SSL must be 1, true, yes, 0, false, or no")
|
|
85
|
+
|
|
86
|
+
def _prepare_url(self, url: str) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Prepare the URL by ensuring it has a protocol and no trailing slash
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
url: The URL to prepare
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str: The prepared URL
|
|
95
|
+
"""
|
|
96
|
+
if not re.match(r"^https?://", url):
|
|
97
|
+
url = f"http://{url}"
|
|
98
|
+
return url.rstrip("/")
|