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.
@@ -0,0 +1,632 @@
1
+ """
2
+ Odoo client for interacting with Odoo via JSON-RPC using OdooRPC library.
3
+
4
+ TABLE OF CONTENTS:
5
+ =================
6
+
7
+ 1. INITIALIZATION AND CONNECTION MANAGEMENT
8
+ - __init__()
9
+ - _connect()
10
+ - _ensure_connected()
11
+
12
+ 2. MODEL INTROSPECTION
13
+ - get_models()
14
+ - get_model_info()
15
+ - get_model_fields()
16
+ 3. SEARCH AND READ OPERATIONS
17
+ - search_ids()
18
+ - search_count()
19
+ - search_read()
20
+ - read_records()
21
+
22
+ 4. CRUD OPERATIONS
23
+ - create_record()
24
+ - create_records()
25
+ - write_records()
26
+ - unlink_records()
27
+ - copy_record()
28
+
29
+ 5. ACCESS CONTROL
30
+ - check_access_rights()
31
+
32
+ 6. GENERIC METHOD EXECUTION
33
+ - execute_method()
34
+ - call_method()
35
+ """
36
+
37
+ import urllib.parse
38
+ from typing import Any, Protocol, cast
39
+
40
+ import odoorpc # type: ignore
41
+ from loguru import logger
42
+ from odoorpc.error import InternalError, RPCError # type: ignore
43
+ from odoorpc.rpc.error import ConnectorError # type: ignore
44
+
45
+ from biszx_odoo_mcp.exceptions import (
46
+ AuthenticationError,
47
+ ConnectionTimeoutError,
48
+ InternalServerError,
49
+ ModelNotFoundError,
50
+ OdooRPCError,
51
+ )
52
+ from biszx_odoo_mcp.tools.config import Config
53
+
54
+
55
+ class OdooModelProtocol(Protocol):
56
+ """Protocol defining the interface for Odoo model proxy objects"""
57
+
58
+ def search(self, domain: list[Any], **kwargs: Any) -> list[int]:
59
+ """Search for record IDs"""
60
+ ...
61
+
62
+ def search_count(self, domain: list[Any]) -> int:
63
+ """Count records matching domain"""
64
+ ...
65
+
66
+ def search_read(
67
+ self, domain: list[Any], fields: list[str] | None = None, **kwargs: Any
68
+ ) -> list[dict[str, Any]]:
69
+ """Search and read records"""
70
+ ...
71
+
72
+ def browse(self, ids: list[int]) -> Any:
73
+ """Browse records by IDs"""
74
+ ...
75
+
76
+ def create(self, values: list[dict[str, Any]]) -> Any:
77
+ """Create records"""
78
+ ...
79
+
80
+ def fields_get(self) -> dict[str, Any]:
81
+ """Get field definitions"""
82
+ ...
83
+
84
+ def check_access_rights(self, operation: str, raise_exception: bool = True) -> bool:
85
+ """Check access rights for the given operation"""
86
+ ...
87
+
88
+
89
+ class OdooClient:
90
+ """
91
+ Client for interacting with Odoo via JSON-RPC
92
+ """
93
+
94
+ # ============================================================================
95
+ # INITIALIZATION AND CONNECTION MANAGEMENT
96
+ # ============================================================================
97
+
98
+ def __init__(
99
+ self,
100
+ config: Config,
101
+ ) -> None:
102
+ """
103
+ Initialize the Odoo client with connection parameters
104
+
105
+ Args:
106
+ config: Odoo client configuration
107
+ """
108
+ self.config = config
109
+ parsed_url = urllib.parse.urlparse(self.config.url)
110
+ self.hostname = parsed_url.netloc
111
+ self.odoo: odoorpc.ODOO | None = None # Will be initialized in _connect
112
+ self.uid: int | None = None # Will be set after login
113
+ self._connect()
114
+
115
+ def _ensure_connected(self) -> Any:
116
+ """Ensure we have a valid connection"""
117
+ if self.odoo is None:
118
+ raise InternalServerError("Not connected to Odoo")
119
+ return self.odoo
120
+
121
+ def _get_model(self, model_name: str) -> OdooModelProtocol:
122
+ """Get a model proxy with proper typing"""
123
+ odoo_conn = self._ensure_connected()
124
+ return cast(OdooModelProtocol, odoo_conn.env[model_name])
125
+
126
+ def _connect(self) -> None:
127
+ """Initialize the OdooRPC connection and authenticate"""
128
+ logger.debug(f"Connecting to Odoo at: {self.config.url}")
129
+ logger.debug(f"Database: {self.config.db}, User: {self.config.username}")
130
+
131
+ try:
132
+ # Determine protocol and port based on URL scheme
133
+ is_https = self.config.url.startswith("https://")
134
+ protocol = "jsonrpc+ssl" if is_https else "jsonrpc"
135
+ port = 443 if is_https else 80
136
+
137
+ self.odoo = odoorpc.ODOO(
138
+ self.hostname,
139
+ protocol=protocol,
140
+ port=port,
141
+ timeout=self.config.timeout,
142
+ version=None,
143
+ )
144
+ self.odoo.login(self.config.db, self.config.username, self.config.password)
145
+
146
+ # Get user ID for later use
147
+ user_model = self._get_model("res.users")
148
+ user_records = user_model.search([("login", "=", self.config.username)])
149
+ self.uid = user_records[0] if user_records else None
150
+
151
+ logger.info("✅ Successfully connected to Odoo")
152
+
153
+ except (RPCError, InternalError, ConnectorError) as e:
154
+ # Log connection errors as they're important for debugging
155
+ logger.error(f"🔴 Failed to connect to Odoo: {str(e)}")
156
+
157
+ # Check specific error types and raise appropriate custom exceptions
158
+ error_msg = str(e).lower()
159
+ if isinstance(e, RPCError):
160
+ if "access" in error_msg or "denied" in error_msg:
161
+ raise AuthenticationError(
162
+ f"Authentication failed: {str(e)}",
163
+ username=self.config.username,
164
+ database=self.config.db,
165
+ ) from e
166
+ raise OdooRPCError(error=e, method="connect") from e
167
+ if isinstance(e, ConnectorError):
168
+ raise ConnectionTimeoutError(
169
+ f"Connection failed: {str(e)}", timeout=self.config.timeout
170
+ ) from e
171
+ raise InternalServerError(f"Internal error: {str(e)}") from e
172
+
173
+ # ============================================================================
174
+ # MODEL INTROSPECTION
175
+ # ============================================================================
176
+
177
+ def search_models(self, query: str) -> dict[str, Any]:
178
+ """
179
+ Search for models that match a query term
180
+
181
+ This searches through model names and display names to find models that
182
+ match the given query term.
183
+
184
+ Args:
185
+ query: Search term to find models (searches in model name and display name)
186
+
187
+ Returns:
188
+ Dictionary with search results
189
+
190
+ Examples:
191
+ >>> client = OdooClient(url, db, username, password)
192
+ >>> results = client.search_models('partner')
193
+ >>> print(results['length'])
194
+ 3
195
+ >>> print([m['model'] for m in results['models']])
196
+ ['res.partner', 'res.partner.bank', 'res.partner.category']
197
+ """
198
+ try:
199
+ IrModel = self._get_model("ir.model")
200
+ IrModel.check_access_rights("read")
201
+ domain = [
202
+ "&",
203
+ ("transient", "=", False),
204
+ "&",
205
+ "|",
206
+ ("model", "like", query),
207
+ ("name", "like", query),
208
+ "|",
209
+ "&",
210
+ ("model", "not like", "base.%"),
211
+ ("model", "not like", "ir.%"),
212
+ (
213
+ "model",
214
+ "in",
215
+ [
216
+ "ir.attachment",
217
+ "ir.model",
218
+ "ir.model.fields",
219
+ ],
220
+ ),
221
+ ]
222
+ matching_models = IrModel.search_read(domain, ["model", "name"])
223
+ return {
224
+ "query": query,
225
+ "length": len(matching_models),
226
+ "models": [
227
+ {
228
+ "model": model["model"],
229
+ "name": model["name"],
230
+ }
231
+ for model in matching_models
232
+ ],
233
+ }
234
+ except RPCError as e:
235
+ raise OdooRPCError(e, method="search_models") from e
236
+
237
+ def get_model_info(self, model_name: str) -> dict[str, Any]:
238
+ """
239
+ Get information about a specific model
240
+
241
+ Args:
242
+ model_name: Name of the model (e.g., 'res.partner')
243
+
244
+ Returns:
245
+ Dictionary with model information
246
+
247
+ Examples:
248
+ >>> client = OdooClient(url, db, username, password)
249
+ >>> info = client.get_model_info('res.partner')
250
+ >>> print(info['name'])
251
+ 'Contact'
252
+ """
253
+ try:
254
+ IrModel = self._get_model("ir.model")
255
+ IrModel.check_access_rights("read")
256
+ result = IrModel.search_read(
257
+ [("model", "=", model_name)], ["model", "name"]
258
+ )
259
+ if not result:
260
+ raise ModelNotFoundError(model_name)
261
+ return result[0]
262
+ except RPCError as e:
263
+ raise OdooRPCError(e, method="get_model_info") from e
264
+
265
+ def get_model_fields(
266
+ self, model_name: str, query: str | None = None
267
+ ) -> dict[str, Any]:
268
+ """
269
+ Get field definitions for a specific model
270
+
271
+ Args:
272
+ model_name: Name of the model (e.g., 'res.partner')
273
+
274
+ Returns:
275
+ Dictionary mapping field names to their definitions
276
+
277
+ Examples:
278
+ >>> client = OdooClient(url, db, username, password)
279
+ >>> fields = client.get_model_fields('res.partner')
280
+ >>> print(fields['name']['type'])
281
+ 'char'
282
+ """
283
+ try:
284
+ Model = self._get_model(model_name)
285
+ data: dict[str, Any] = Model.fields_get()
286
+ result: dict[str, Any] = {
287
+ "length": 0,
288
+ "fields": {},
289
+ }
290
+ if query is not None:
291
+ for field, value in data.items():
292
+ if "related" in value:
293
+ continue
294
+
295
+ if all(
296
+ {
297
+ query.lower() in field.lower()
298
+ or query.lower() in value["string"].lower(),
299
+ }
300
+ ):
301
+ result["fields"][field] = {
302
+ "name": field,
303
+ "string": value["string"],
304
+ "type": value["type"],
305
+ "required": value.get("required", False),
306
+ "readonly": value.get("readonly", False),
307
+ "searchable": value.get("searchable", False),
308
+ "relation": value.get("relation", False),
309
+ }
310
+ else:
311
+ result["fields"] = data
312
+ result["length"] = len(result["fields"])
313
+ return result
314
+ except RPCError as e:
315
+ raise OdooRPCError(e, method="get_model_fields") from e
316
+
317
+ # ============================================================================
318
+ # SEARCH AND READ OPERATIONS
319
+ # ============================================================================
320
+
321
+ def search_ids(
322
+ self,
323
+ model_name: str,
324
+ domain: list[Any],
325
+ offset: int | None = None,
326
+ limit: int | None = None,
327
+ order: str | None = None,
328
+ ) -> list[int]:
329
+ """
330
+ Search for record IDs that match a domain
331
+
332
+ Args:
333
+ model_name: Name of the model (e.g., 'res.partner')
334
+ domain: Search domain (e.g., [('is_company', '=', True)])
335
+ offset: Number of records to skip
336
+ limit: Maximum number of records to return
337
+ order: Sorting criteria (e.g., 'name ASC, id DESC')
338
+
339
+ Returns:
340
+ List of matching record IDs
341
+
342
+ Examples:
343
+ >>> client = OdooClient(url, db, username, password)
344
+ >>> ids = client.search_ids(
345
+ ... 'res.partner', [('is_company', '=', True)], limit=5
346
+ ... )
347
+ >>> print(ids)
348
+ [1, 2, 3, 4, 5]
349
+ """
350
+ try:
351
+ Model = self._get_model(model_name)
352
+
353
+ # Build search kwargs
354
+ search_kwargs: dict[str, Any] = {}
355
+ if offset is not None:
356
+ search_kwargs["offset"] = offset
357
+ if limit is not None:
358
+ search_kwargs["limit"] = limit
359
+ if order is not None:
360
+ search_kwargs["order"] = order
361
+
362
+ return Model.search(domain, **search_kwargs)
363
+ except RPCError as e:
364
+ raise OdooRPCError(e, method="search_ids") from e
365
+
366
+ def search_count(self, model_name: str, domain: list[Any]) -> int:
367
+ """
368
+ Count records that match a search domain
369
+
370
+ Args:
371
+ model_name: Name of the model (e.g., 'res.partner')
372
+ domain: Search domain (e.g., [('is_company', '=', True)])
373
+
374
+ Returns:
375
+ Integer count of matching records
376
+
377
+ Examples:
378
+ >>> client = OdooClient(url, db, username, password)
379
+ >>> count = client.search_count('res.partner', [('is_company', '=', True)])
380
+ >>> print(count)
381
+ 25
382
+ """
383
+ try:
384
+ Model = self._get_model(model_name)
385
+ return Model.search_count(domain)
386
+ except RPCError as e:
387
+ raise OdooRPCError(e, method="search_count") from e
388
+
389
+ def search_read(
390
+ self,
391
+ model_name: str,
392
+ domain: list[Any],
393
+ fields: list[str] | None = None,
394
+ offset: int | None = None,
395
+ limit: int | None = None,
396
+ order: str | None = None,
397
+ ) -> list[dict[str, Any]]:
398
+ """
399
+ Search for records and read their data in a single call
400
+
401
+ Args:
402
+ model_name: Name of the model (e.g., 'res.partner')
403
+ domain: Search domain (e.g., [('is_company', '=', True)])
404
+ fields: List of field names to return (None for all)
405
+ offset: Number of records to skip
406
+ limit: Maximum number of records to return
407
+ order: Sorting criteria (e.g., 'name ASC, id DESC')
408
+
409
+ Returns:
410
+ List of dictionaries with the matching records
411
+
412
+ Examples:
413
+ >>> client = OdooClient(url, db, username, password)
414
+ >>> records = client.search_read('res.partner', [
415
+ ('is_company', '=', True)
416
+ ], limit=5)
417
+ >>> print(len(records))
418
+ 5
419
+ """
420
+ try:
421
+ Model = self._get_model(model_name)
422
+
423
+ # Build search_read arguments
424
+ search_kwargs: dict[str, Any] = {}
425
+ if offset is not None:
426
+ search_kwargs["offset"] = offset
427
+ if fields is not None:
428
+ search_kwargs["fields"] = fields
429
+ if limit is not None:
430
+ search_kwargs["limit"] = limit
431
+ if order is not None:
432
+ search_kwargs["order"] = order
433
+
434
+ result = Model.search_read(domain, **search_kwargs)
435
+ return result
436
+ except RPCError as e:
437
+ raise OdooRPCError(e, method="search_read") from e
438
+
439
+ def read_records(
440
+ self,
441
+ model_name: str,
442
+ ids: list[int],
443
+ fields: list[str] | None = None,
444
+ ) -> list[dict[str, Any]]:
445
+ """
446
+ Read data of records by IDs
447
+
448
+ Args:
449
+ model_name: Name of the model (e.g., 'res.partner')
450
+ ids: List of record IDs to read
451
+ fields: List of field names to return (None for all)
452
+
453
+ Returns:
454
+ List of dictionaries with the requested records
455
+
456
+ Examples:
457
+ >>> client = OdooClient(url, db, username, password)
458
+ >>> records = client.read_records('res.partner', [1])
459
+ >>> print(records[0]['name'])
460
+ 'YourCompany'
461
+ """
462
+ try:
463
+ Model = self._get_model(model_name)
464
+
465
+ if fields is not None:
466
+ result = Model.browse(ids).read(fields)
467
+ else:
468
+ result = Model.browse(ids).read()
469
+
470
+ return cast(list[dict[str, Any]], result)
471
+ except RPCError as e:
472
+ raise OdooRPCError(e, method="read_records") from e
473
+
474
+ # ============================================================================
475
+ # CRUD OPERATIONS
476
+ # ============================================================================
477
+
478
+ def create_records(
479
+ self, model_name: str, values_list: list[dict[str, Any]]
480
+ ) -> int | list[int]:
481
+ """
482
+ Create records in an Odoo model
483
+
484
+ Args:
485
+ model_name: Name of the model (e.g., 'res.partner')
486
+ values_list: List of dictionaries with field values for the new records
487
+
488
+ Returns:
489
+ List of created record IDs
490
+
491
+ Examples:
492
+ >>> client = OdooClient(url, db, username, password)
493
+ >>> record_ids = client.create_records('res.partner', [
494
+ {'name': 'Company 1'}, {'name': 'Company 2'}
495
+ ])
496
+ >>> print(record_ids)
497
+ [42, 43]
498
+ """
499
+ try:
500
+ Model = self._get_model(model_name)
501
+ return cast(int | list[int], Model.create(values_list))
502
+ except RPCError as e:
503
+ raise OdooRPCError(e, method="create_records") from e
504
+
505
+ def write_records(
506
+ self,
507
+ model_name: str,
508
+ record_ids: list[int],
509
+ values: dict[str, Any],
510
+ ) -> bool:
511
+ """
512
+ Update records in an Odoo model
513
+
514
+ Args:
515
+ model_name: Name of the model (e.g., 'res.partner')
516
+ record_ids: List of record IDs to update
517
+ values: Dictionary with field values to update
518
+
519
+ Returns:
520
+ Boolean indicating success
521
+
522
+ Examples:
523
+ >>> client = OdooClient(url, db, username, password)
524
+ >>> success = client.write_records(
525
+ ... 'res.partner', [1], {'name': 'Updated Name'}
526
+ ... )
527
+ >>> print(success)
528
+ True
529
+ """
530
+ try:
531
+ Model = self._get_model(model_name)
532
+ records = Model.browse(record_ids)
533
+ return cast(bool, records.write(values))
534
+ except RPCError as e:
535
+ raise OdooRPCError(e, method="write_records") from e
536
+
537
+ def unlink_records(self, model_name: str, record_ids: list[int]) -> bool:
538
+ """
539
+ Delete records from an Odoo model
540
+
541
+ Args:
542
+ model_name: Name of the model (e.g., 'res.partner')
543
+ record_ids: List of record IDs to delete
544
+
545
+ Returns:
546
+ Boolean indicating success
547
+
548
+ Examples:
549
+ >>> client = OdooClient(url, db, username, password)
550
+ >>> success = client.unlink_records('res.partner', [42])
551
+ >>> print(success)
552
+ True
553
+ """
554
+ try:
555
+ Model = self._get_model(model_name)
556
+ records = Model.browse(record_ids)
557
+ return cast(bool, records.unlink())
558
+ except RPCError as e:
559
+ raise OdooRPCError(e, method="unlink_records") from e
560
+
561
+ # ============================================================================
562
+ # GENERIC METHOD EXECUTION
563
+ # ============================================================================
564
+
565
+ def execute_method(self, model: str, method: str, *args: Any, **kwargs: Any) -> Any:
566
+ """
567
+ Execute an arbitrary method on a model
568
+
569
+ Args:
570
+ model: The model name (e.g., 'res.partner')
571
+ method: Method name to execute
572
+ *args: Positional arguments to pass to the method
573
+ **kwargs: Keyword arguments to pass to the method
574
+
575
+ Returns:
576
+ Result of the method execution
577
+ """
578
+ try:
579
+ model_proxy = self._get_model(model)
580
+ # Use getattr to dynamically call the method on the model proxy
581
+ method_func = getattr(model_proxy, method)
582
+ return method_func(*args, **kwargs)
583
+ except RPCError as e:
584
+ raise OdooRPCError(e, method=f"execute_method: {model}.{method}") from e
585
+
586
+ def call_method(
587
+ self,
588
+ model_name: str,
589
+ method_name: str,
590
+ args: list[Any],
591
+ kwargs: dict[str, Any],
592
+ ) -> Any:
593
+ """
594
+ Call a custom method on an Odoo model
595
+
596
+ Args:
597
+ model_name: Name of the model (e.g., 'res.partner')
598
+ method_name: Name of the method to call
599
+ args: Positional arguments to pass to the method
600
+ kwargs: Keyword arguments to pass to the method
601
+
602
+ Returns:
603
+ Method result
604
+
605
+ Examples:
606
+ >>> client = OdooClient(url, db, username, password)
607
+ >>> result = client.call_method('res.partner', 'name_get', [1], {})
608
+ >>> print(result)
609
+ [(1, 'YourCompany')]
610
+ """
611
+ try:
612
+ model_proxy = self._get_model(model_name)
613
+ # Use getattr to dynamically call the method on the model proxy
614
+ method_func = getattr(model_proxy, method_name)
615
+ result = method_func(*args, **kwargs)
616
+ if result is None:
617
+ raise ValueError(f"Failed to call method {method_name}")
618
+ return result
619
+ except RPCError as e:
620
+ raise OdooRPCError(
621
+ e, method=f"call_method: {model_name}.{method_name}"
622
+ ) from e
623
+
624
+
625
+ def get_odoo_client() -> OdooClient:
626
+ """
627
+ Get a configured Odoo client instance
628
+
629
+ Returns:
630
+ OdooClient: A configured Odoo client instance
631
+ """
632
+ return OdooClient(config=Config())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: biszx-odoo-mcp
3
- Version: 1.1.0
3
+ Version: 1.1.2
4
4
  Summary: MCP Server for Odoo Integration
5
5
  Author-email: Biszx <isares.br@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,18 @@
1
+ biszx_odoo_mcp/__init__.py,sha256=J-K5_2GapE12EHcbhrotvjxKDYFS2aGa_eIpcsnHjIg,58
2
+ biszx_odoo_mcp/__main__.py,sha256=b7dX--Aowgjuvgr8Sh4aXa63TCwxPa_vLjh7JYReFz0,1256
3
+ biszx_odoo_mcp/exceptions.py,sha256=vVMyZekFmZIg2SsH53zcefWp_swqFkE4lxQin1OVgB0,6611
4
+ biszx_odoo_mcp/main.py,sha256=_HW_Pblf8BFIOhfZ7-uuT4uKNn3uZ269zsK8Lk7Rt5o,3914
5
+ biszx_odoo_mcp/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ biszx_odoo_mcp/server/context.py,sha256=PxyFxFXL3GDpUynm4j63RiUjjIagDfy1oLh1cjPfvVA,378
7
+ biszx_odoo_mcp/server/resources.py,sha256=EliQzuWDKibpd5MPWb2FyM65acll7EkvjYcxdLBFmsw,8389
8
+ biszx_odoo_mcp/server/response.py,sha256=VY85S0JPpR7F1lxweZV_lqB1cqhGtGRbsTkkBmPXimo,877
9
+ biszx_odoo_mcp/server/tools.py,sha256=XfFSg8_5AM-HsXUrqHSRh5_eBO6ExRUGbU3JAQk_0BM,14019
10
+ biszx_odoo_mcp/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ biszx_odoo_mcp/tools/config.py,sha256=xdV6L9CATO1PDSk6nenh1fCimONHGo25UAVuoulGFbo,2869
12
+ biszx_odoo_mcp/tools/odoo_client.py,sha256=LfXKHkkjgwONuOulR0JpojGcb0_OkiUAa2qnaGtZkGI,21115
13
+ biszx_odoo_mcp-1.1.2.dist-info/licenses/LICENSE,sha256=_FwB4PGxcQWsToTqX33moF0HqnB45Ox6Fr0VPJiLyBI,1071
14
+ biszx_odoo_mcp-1.1.2.dist-info/METADATA,sha256=E45-DbNR4g0817nX5GAeODhydoKtlShhRxxhOVoE2zM,5394
15
+ biszx_odoo_mcp-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ biszx_odoo_mcp-1.1.2.dist-info/entry_points.txt,sha256=vdIkuiVsddBLNV--8Wgaf8xHu6Pdi2DAia-ISGNdxP0,64
17
+ biszx_odoo_mcp-1.1.2.dist-info/top_level.txt,sha256=l6PxKyczED68V9LsRlWYClo1GfjSJaP9V-SDMnAUJbU,15
18
+ biszx_odoo_mcp-1.1.2.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- biszx_odoo_mcp/__init__.py,sha256=J-K5_2GapE12EHcbhrotvjxKDYFS2aGa_eIpcsnHjIg,58
2
- biszx_odoo_mcp/__main__.py,sha256=b7dX--Aowgjuvgr8Sh4aXa63TCwxPa_vLjh7JYReFz0,1256
3
- biszx_odoo_mcp/exceptions.py,sha256=vVMyZekFmZIg2SsH53zcefWp_swqFkE4lxQin1OVgB0,6611
4
- biszx_odoo_mcp/main.py,sha256=_HW_Pblf8BFIOhfZ7-uuT4uKNn3uZ269zsK8Lk7Rt5o,3914
5
- biszx_odoo_mcp-1.1.0.dist-info/licenses/LICENSE,sha256=_FwB4PGxcQWsToTqX33moF0HqnB45Ox6Fr0VPJiLyBI,1071
6
- biszx_odoo_mcp-1.1.0.dist-info/METADATA,sha256=zok0QuLLRKeeTpC0W9T1-UCqhTSM8HEX4ZkxVp8qC-4,5394
7
- biszx_odoo_mcp-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- biszx_odoo_mcp-1.1.0.dist-info/entry_points.txt,sha256=vdIkuiVsddBLNV--8Wgaf8xHu6Pdi2DAia-ISGNdxP0,64
9
- biszx_odoo_mcp-1.1.0.dist-info/top_level.txt,sha256=l6PxKyczED68V9LsRlWYClo1GfjSJaP9V-SDMnAUJbU,15
10
- biszx_odoo_mcp-1.1.0.dist-info/RECORD,,