producteca 1.0.14__py3-none-any.whl → 2.0.57__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.
producteca/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ # flake8: noqa
2
+
3
+ from .client import ProductecaClient
@@ -1,23 +1,25 @@
1
- from pydantic import BaseModel, PrivateAttr
2
- from abc import ABC, abstractmethod
3
- from ..config.config import ConfigProducteca, APIConfig
1
+ from abc import ABC
2
+ from ..config.config import ConfigProducteca
3
+ from ..utils import clean_model_dump
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Any
4
6
 
5
7
 
6
- class AbstractProductecaModel(BaseModel, ABC):
7
- _config: APIConfig = PrivateAttr()
8
+ @dataclass
9
+ class BaseService(ABC):
10
+ config: ConfigProducteca
11
+ endpoint: str
12
+ _record: Optional[Any] = None
13
+
14
+ def __repr__(self):
15
+ return repr(self._record)
8
16
 
9
- @property
10
- @abstractmethod
11
- def endpoint(self) -> str:
12
- pass
17
+ def to_dict(self):
18
+ return clean_model_dump(self._record)
13
19
 
14
- def dict(self, *args, **kwargs):
15
- return super().dict(*args, exclude_none=True, **kwargs)
20
+ def to_json(self):
21
+ import json
22
+ return json.dumps(clean_model_dump(self._record))
16
23
 
17
-
18
- class AbstractProductecaV1Model(AbstractProductecaModel):
19
- _config: ConfigProducteca = PrivateAttr()
20
-
21
- @property
22
- def endpoint_url(self) -> str:
23
- return self._config.get_endpoint(self.endpoint)
24
+ def __getattr__(self, key):
25
+ return getattr(self._record, key)
producteca/client.py CHANGED
@@ -1,37 +1,24 @@
1
1
  from producteca.config.config import ConfigProducteca
2
- from producteca.products.products import Product
3
- from producteca.sales_orders.sales_orders import SalesOrder
4
- from producteca.search.search import Search
5
- from producteca.shipments.shipment import Shipment
6
- from producteca.payments.payments import Payment
2
+ from producteca.products.products import ProductService
3
+ from producteca.sales_orders.sales_orders import SaleOrderService
4
+
7
5
  import os
8
6
 
9
7
 
10
8
  class ProductecaClient:
11
9
 
12
- def __init__(self, token: str = os.environ['PRODUCTECA_TOKEN'], api_key: str = os.environ['PRODUCTECA_API_KEY']):
10
+ def __init__(self, token: str = os.environ.get('PRODUCTECA_TOKEN', ''), api_key: str = os.environ.get('PRODUCTECA_API_KEY', '')):
13
11
  if not token:
14
12
  raise ValueError('PRODUCTECA_TOKEN environment variable not set')
15
13
  if not api_key:
16
14
  raise ValueError('PRODUCTECA_API_KEY environment variable not set')
17
15
  self.config = ConfigProducteca(token=token, api_key=api_key)
18
-
19
- @property
20
- def Product(self):
21
- return lambda *args: Product(config=self.config, *args)
22
-
23
- @property
24
- def SalesOrder(self):
25
- return lambda *args: SalesOrder(config=self.config, *args)
26
16
 
27
17
  @property
28
- def Search(self):
29
- return lambda *args: Search(config=self.config, *args)
18
+ def Product(self):
19
+ return ProductService(self.config)
30
20
 
31
21
  @property
32
- def Shipment(self):
33
- return lambda *args: Shipment(config=self.config, *args)
22
+ def SalesOrder(self):
23
+ return SaleOrderService(self.config)
34
24
 
35
- @property
36
- def Payment(self):
37
- return lambda *args: Payment(config=self.config, *args)
@@ -1 +0,0 @@
1
- from .config import ConfigProducteca
@@ -1,7 +1,5 @@
1
1
  from pydantic import BaseModel
2
2
  from typing import Optional
3
- import requests
4
- from ..config.config import ConfigProducteca
5
3
 
6
4
 
7
5
  class PaymentCard(BaseModel):
@@ -22,24 +20,14 @@ class Payment(BaseModel):
22
20
  date: str
23
21
  amount: float
24
22
  couponAmount: Optional[float] = None
25
- status: str
26
- method: str
23
+ status: Optional[str] = None
24
+ method: Optional[str] = None
27
25
  integration: Optional[PaymentIntegration] = None
28
26
  transactionFee: Optional[float] = None
29
27
  installments: Optional[int] = None
30
28
  card: Optional[PaymentCard] = None
31
29
  notes: Optional[str] = None
32
- hasCancelableStatus: bool
30
+ hasCancelableStatus: Optional[bool] = None
33
31
  id: Optional[int] = None
34
32
 
35
- @classmethod
36
- def create(cls, config: ConfigProducteca, sale_order_id: int, payload: "Payment") -> "Payment":
37
- url = config.get_endpoint(f"salesorders/{sale_order_id}/payments")
38
- res = requests.post(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
39
- return cls(**res.json())
40
-
41
- @classmethod
42
- def update(cls, config: ConfigProducteca, sale_order_id: int, payment_id: int, payload: "Payment") -> "Payment":
43
- url = config.get_endpoint(f"salesorders/{sale_order_id}/payments/{payment_id}")
44
- res = requests.put(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
45
- return cls(**res.json())
33
+
@@ -1,13 +1,13 @@
1
1
  import unittest
2
2
  from unittest.mock import patch, Mock
3
3
  from datetime import datetime
4
- from producteca.config.config import ConfigProducteca
5
- from producteca.payments.payments import Payment, PaymentCard, PaymentIntegration
4
+ from producteca.client import ProductecaClient
5
+ from producteca.payments.payments import Payment
6
6
 
7
7
 
8
8
  class TestPayments(unittest.TestCase):
9
9
  def setUp(self):
10
- self.config = ConfigProducteca(token="asd", api_key="test_key")
10
+ self.client = ProductecaClient(token="asd", api_key="test_key")
11
11
  self.sale_order_id = 123
12
12
  self.payment_id = 456
13
13
 
@@ -35,7 +35,15 @@ class TestPayments(unittest.TestCase):
35
35
  payment = Payment(**self.payment_data)
36
36
 
37
37
  # Test create method
38
- result = Payment.create(self.config, self.sale_order_id, payment)
38
+ result = self.client.SalesOrder(id=self.sale_order_id, invoiceIntegration={
39
+ 'id': 1,
40
+ 'integrationId': 'test-integration',
41
+ 'app': 1,
42
+ 'createdAt': '2023-01-01',
43
+ 'decreaseStock': True,
44
+ "documentUrl": "https://aallala.copm",
45
+ "xmlUrl": "https://aallala.copm",
46
+ }).add_payment(payment.model_dump(by_alias=True))
39
47
 
40
48
  # Assertions
41
49
  mock_post.assert_called_once()
@@ -55,7 +63,15 @@ class TestPayments(unittest.TestCase):
55
63
  payment = Payment(**self.payment_data)
56
64
 
57
65
  # Test update method
58
- result = Payment.update(self.config, self.sale_order_id, self.payment_id, payment)
66
+ result = self.client.SalesOrder(id=self.sale_order_id, invoiceIntegration={
67
+ 'id': 1,
68
+ 'integrationId': 'test-integration',
69
+ 'app': 1,
70
+ 'createdAt': '2023-01-01',
71
+ 'decreaseStock': True,
72
+ "documentUrl": "https://aallala.copm",
73
+ "xmlUrl": "https://aallala.copm",
74
+ }).update_payment(self.payment_id, payment.model_dump(by_alias=True))
59
75
 
60
76
  # Assertions
61
77
  mock_put.assert_called_once()
@@ -1 +0,0 @@
1
- from . import products
@@ -1,20 +1,24 @@
1
1
  from typing import List, Optional, Union
2
- from pydantic import BaseModel, Field
3
- import requests
4
- from ..config.config import ConfigProducteca
2
+ from pydantic import BaseModel, Field, ValidationError
3
+ from dataclasses import dataclass
4
+ from producteca.abstract.abstract_dataclass import BaseService
5
+ from producteca.products.search_products import SearchProduct, SearchProductParams
6
+ from producteca.utils import clean_model_dump
5
7
  import logging
8
+ import requests
6
9
 
7
10
  _logger = logging.getLogger(__name__)
8
11
 
9
- # Models for nested structures
10
12
 
11
13
  class Attribute(BaseModel):
12
14
  key: str
13
15
  value: str
14
16
 
17
+
15
18
  class Tag(BaseModel):
16
19
  tag: str
17
20
 
21
+
18
22
  class Dimensions(BaseModel):
19
23
  weight: Optional[float] = None
20
24
  width: Optional[float] = None
@@ -22,11 +26,13 @@ class Dimensions(BaseModel):
22
26
  length: Optional[float] = None
23
27
  pieces: Optional[int] = None
24
28
 
29
+
25
30
  class Deal(BaseModel):
26
31
  campaign: str
27
32
  regular_price: Optional[float] = Field(default=None, alias='regularPrice')
28
33
  deal_price: Optional[float] = Field(default=None, alias='dealPrice')
29
34
 
35
+
30
36
  class Stock(BaseModel):
31
37
  quantity: Optional[int] = None
32
38
  available_quantity: Optional[int] = Field(default=None, alias='availableQuantity')
@@ -35,15 +41,18 @@ class Stock(BaseModel):
35
41
  reserved: Optional[int] = None
36
42
  available: Optional[int] = None
37
43
 
44
+
38
45
  class Price(BaseModel):
39
46
  amount: Optional[float] = None
40
47
  currency: str
41
48
  price_list: str = Field(alias='priceList')
42
49
  price_list_id: Optional[int] = Field(default=None, alias='priceListId')
43
50
 
51
+
44
52
  class Picture(BaseModel):
45
53
  url: str
46
54
 
55
+
47
56
  class Integration(BaseModel):
48
57
  app: Optional[int] = None
49
58
  integration_id: Optional[str] = Field(default=None, alias='integrationId')
@@ -57,96 +66,96 @@ class Integration(BaseModel):
57
66
  id: Optional[int] = None
58
67
  parent_integration: Optional[str] = Field(default=None, alias='parentIntegration')
59
68
 
69
+
60
70
  class Variation(BaseModel):
61
71
  variation_id: Optional[int] = Field(default=None, alias='variationId')
62
72
  components: Optional[List] = None
63
- pictures: Optional[List[Picture]] = None
64
- stocks: Optional[List[Stock]] = None
73
+ pictures: Optional[Union[List[Picture], List]] = None
74
+ stocks: Optional[Union[List[Stock], List]] = None
65
75
  attributes_hash: Optional[str] = Field(default=None, alias='attributesHash')
66
76
  primary_color: Optional[str] = Field(default=None, alias='primaryColor')
67
77
  thumbnail: Optional[str] = None
68
- attributes: Optional[List[Attribute]] = None
69
- integrations: Optional[List[Integration]] = None
78
+ attributes: Optional[Union[List[Attribute], List]] = None
79
+ integrations: Optional[Union[List[Integration], List]] = None
70
80
  id: Optional[int] = None
71
81
  sku: Optional[str] = None
72
82
  barcode: Optional[str] = None
73
83
 
74
- # Model base para los productos
84
+
85
+ class MeliCategory(BaseModel):
86
+ meli_id: Optional[str] = Field(default=None, alias='meliId')
87
+ accepts_mercadoenvios: Optional[bool] = Field(default=None, alias='acceptsMercadoenvios')
88
+ suggest: Optional[bool] = None
89
+ fixed: Optional[bool] = None
90
+
91
+
92
+ class BundleComponent(BaseModel):
93
+ quantity: int
94
+ variation_id: int = Field(alias='variationId')
95
+ product_id: int = Field(alias='productId')
96
+
97
+
98
+ class BundleVariation(BaseModel):
99
+ variation_id: int = Field(alias='variationId')
100
+ components: Union[List[BundleComponent], List]
101
+
102
+
103
+ class BundleResult(BaseModel):
104
+ company_id: int = Field(alias='companyId')
105
+ product_id: int = Field(alias='productId')
106
+ variations: Union[List[BundleVariation], List]
107
+ id: str
108
+
109
+
110
+ class BundleResponse(BaseModel):
111
+ results: Union[List[BundleResult], List]
112
+ count: int
113
+
114
+
75
115
  class Product(BaseModel):
76
- config: Optional[ConfigProducteca] = Field(default=None, exclude=True)
77
- endpoint: str = Field(default='products', exclude=True)
78
- create_if_it_doesnt_exist: bool = Field(default=False, exclude=True)
79
- sku: Optional[str] = None
80
- variation_id: Optional[int] = None
116
+ updatable_properties: Optional[List[str]] = Field(default=None, alias='$updatableProperties')
117
+ integrations: Optional[Union[List[Integration], List]] = None
118
+ variations: Optional[Union[List[Variation], List]] = None
119
+ is_simple: Optional[bool] = Field(default=None, alias='isSimple')
120
+ has_variations: Optional[bool] = Field(default=None, alias='hasVariations')
121
+ thumbnail: Optional[str] = None
122
+ category: Optional[str] = None
123
+ notes: Optional[str] = None
124
+ prices: Optional[Union[List[Price], List]] = None
125
+ buying_price: Optional[float] = Field(default=None, alias='buyingPrice')
126
+ is_archived: Optional[bool] = Field(default=None, alias='isArchived')
127
+ dimensions: Optional[Union[Dimensions, dict]] = None
128
+ attributes: Optional[Union[List[Attribute], List]] = None
129
+ metadata: Optional[List[str]] = None
130
+ is_original: Optional[bool] = Field(default=None, alias='isOriginal')
131
+ name: str
81
132
  code: Optional[str] = None
82
- name: Optional[str] = None
83
- barcode: Optional[str] = None
84
- attributes: Optional[List[Attribute]] = None
85
- tags: Optional[List[str]] = None
86
- buying_price: Optional[float] = None
87
- dimensions: Optional[Dimensions] = None
88
- category: Optional[Union[str, dict]] = None # Puede ser string en POST o dict en GET Meli
133
+ sku: Optional[str] = None
89
134
  brand: Optional[str] = None
90
- notes: Optional[str] = None
91
- deals: Optional[List[Deal]] = None
92
- stocks: Optional[List[Stock]] = None
93
- prices: Optional[List[Price]] = None
94
- pictures: Optional[List[Picture]] = None
135
+ id: Optional[int] = None
95
136
 
96
137
 
97
- def create(self):
98
- endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
99
- headers = self.config.headers.copy()
100
- headers.update({"createifitdoesntexist": str(self.create_if_it_doesnt_exist).lower()})
101
- data = self.model_dump_json(by_alias=True, exclude_none=True)
102
- response = requests.post(endpoint_url, data=data, headers=headers)
103
- if response.status_code == 204:
104
- final_response = {"Message":"Product does not exist and the request cant create if it does not exist"}
105
- else:
106
- final_response = response.json()
107
- return final_response, response.status_code
108
-
109
- def update(self):
110
- endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
111
- headers = self.config.headers.copy()
112
- data = self.model_dump_json(by_alias=True, exclude_none=True)
113
- if not self.code and not self.sku:
114
- return {"Message":"Sku or code should be provided to update the product"}, 204
115
- response = requests.post(endpoint_url, data=data, headers=headers)
116
- if response.status_code == 204:
117
- final_response = {"Message":"Product does not exist and the request cant create if it does not exist"}
118
- else:
119
- final_response = response.json()
120
- return final_response, response.status_code
121
-
122
- @classmethod
123
- def get(cls, config: ConfigProducteca, product_id: int):
124
- endpoint_url = config.get_endpoint(f'{cls().endpoint}/{product_id}')
125
- headers = config.headers
126
- response = requests.get(endpoint_url, headers=headers)
127
- response_data = response.json()
128
- return response_data, response.status_code
138
+ class ProductVariationBase(BaseModel):
139
+ sku: str
140
+ variation_id: Optional[int] = Field(default=None, alias='variationId')
141
+ code: Optional[str] = None
142
+ barcode: Optional[str] = None
143
+ attributes: Union[List[Attribute], List] = []
144
+ tags: Optional[List[str]] = []
145
+ buying_price: Optional[float] = Field(default=None, alias='buyingPrice')
146
+ dimensions: Optional[Union[Dimensions, dict]] = Field(default_factory=Dimensions)
147
+ brand: Optional[str] = ''
148
+ notes: Optional[str] = ''
149
+ deals: Optional[Union[List[Deal], List]] = []
150
+ stocks: Optional[Union[List[Stock], List]] = []
151
+ prices: Optional[Union[List[Price], List]] = []
152
+ pictures: Optional[Union[List[Picture], List]] = []
129
153
 
130
- @classmethod
131
- def get_bundle(cls, config: ConfigProducteca, product_id: int):
132
- endpoint_url = config.get_endpoint(f'{cls().endpoint}/{product_id}/bundles')
133
- headers = config.headers
134
- response = requests.get(endpoint_url, headers=headers)
135
- return cls(config=config, **response.json()), response.status_code
136
154
 
137
- @classmethod
138
- def get_ml_integration(cls, config: ConfigProducteca, product_id: int):
139
- endpoint_url = config.get_endpoint(f'{cls().endpoint}/{product_id}/listintegration')
140
- headers = config.headers
141
- response = requests.get(endpoint_url, headers=headers)
142
- return cls(config=config, **response.json()), response.status_code
155
+ class ProductVariation(ProductVariationBase):
156
+ category: Optional[str] = Field(default=None)
157
+ name: Optional[str] = None
143
158
 
144
- # Modelo con campos extra de la vista Meli
145
- class MeliCategory(BaseModel):
146
- meli_id: Optional[str] = Field(default=None, alias='meliId')
147
- accepts_mercadoenvios: Optional[bool] = Field(default=None, alias='acceptsMercadoenvios')
148
- suggest: Optional[bool] = None
149
- fixed: Optional[bool] = None
150
159
 
151
160
  class Shipping(BaseModel):
152
161
  local_pickup: Optional[bool] = Field(default=None, alias='localPickup')
@@ -156,9 +165,11 @@ class Shipping(BaseModel):
156
165
  mandatory_free_shipping: Optional[bool] = Field(default=None, alias='mandatoryFreeShipping')
157
166
  free_shipping_method: Optional[str] = Field(default=None, alias='freeShippingMethod')
158
167
 
168
+
159
169
  class MShopsShipping(BaseModel):
160
170
  enabled: Optional[bool] = None
161
171
 
172
+
162
173
  class AttributeCompletion(BaseModel):
163
174
  product_identifier_status: Optional[str] = Field(default=None, alias='productIdentifierStatus')
164
175
  data_sheet_status: Optional[str] = Field(default=None, alias='dataSheetStatus')
@@ -166,17 +177,162 @@ class AttributeCompletion(BaseModel):
166
177
  count: Optional[int] = None
167
178
  total: Optional[int] = None
168
179
 
169
- class MeliProduct(Product):
180
+
181
+ class MeliProduct(BaseModel):
170
182
  product_id: Optional[int] = Field(default=None, alias='productId')
183
+ tags: Optional[List[str]] = Field(default=None)
171
184
  has_custom_shipping_costs: Optional[bool] = Field(default=None, alias='hasCustomShippingCosts')
172
- shipping: Optional[Shipping] = None
173
- mshops_shipping: Optional[MShopsShipping] = Field(default=None, alias='mShopsShipping')
185
+ shipping: Optional[Union[Shipping, dict]] = None
186
+ mshops_shipping: Optional[Union[MShopsShipping, dict]] = Field(default=None, alias='mShopsShipping')
174
187
  add_free_shipping_cost_to_price: Optional[bool] = Field(default=None, alias='addFreeShippingCostToPrice')
175
- category: Optional[MeliCategory] = None
176
- attribute_completion: Optional[AttributeCompletion] = Field(default=None, alias='attributeCompletion')
188
+ category: Union[MeliCategory, dict]
189
+ attribute_completion: Optional[Union[AttributeCompletion, dict]] = Field(default=None, alias='attributeCompletion')
177
190
  catalog_products: Optional[List[str]] = Field(default=None, alias='catalogProducts')
178
191
  warranty: Optional[str] = None
179
192
  domain: Optional[str] = None
180
193
  listing_type_id: Optional[str] = Field(default=None, alias='listingTypeId')
181
194
  catalog_products_status: Optional[str] = Field(default=None, alias='catalogProductsStatus')
182
195
 
196
+
197
+ class ErrorMessage(BaseModel):
198
+ en: str
199
+ es: str
200
+ pt: str
201
+
202
+
203
+ class ErrorReason(BaseModel):
204
+ code: str
205
+ error: str
206
+ message: ErrorMessage
207
+ data: Optional[dict] = None
208
+
209
+
210
+ class ResolvedValue(BaseModel):
211
+ updated: bool
212
+
213
+
214
+ class ResolvedError(BaseModel):
215
+ resolved: Optional[bool] = None
216
+ reason: Optional[Union[ErrorReason, dict]] = None
217
+ value: Optional[Union[ResolvedValue, dict]] = None
218
+ statusCode: Optional[int] = None
219
+
220
+
221
+ class ErrorContext(BaseModel):
222
+ _ns_name: str
223
+ id: int
224
+ requestId: str
225
+ tokenAppId: str
226
+ appId: str
227
+ bearer: str
228
+ eventId: str
229
+
230
+
231
+ class SynchronizeResponse(BaseModel):
232
+ product: Optional[Union[ResolvedError, dict]] = None
233
+ variation: Optional[Union[ResolvedError, dict]] = None
234
+ deals: Optional[Union[ResolvedError, dict]] = None
235
+ bundles: Optional[Union[ResolvedError, dict]] = None
236
+ taxes: Optional[Union[ResolvedError, dict]] = None
237
+ meliProductListingIntegrations: Optional[Union[ResolvedError, dict]] = None
238
+ tags: Optional[Union[ResolvedError, dict]] = None
239
+ productIntegrations: Optional[Union[ResolvedError, dict]] = None
240
+ statusCode: Optional[int] = None
241
+ error_context: Optional[Union[ErrorContext, dict]] = Field(None, alias='error@context')
242
+
243
+
244
+ class ListedSynchronizeResponse(BaseModel):
245
+ results: Union[List[SynchronizeResponse], List]
246
+
247
+
248
+ @dataclass
249
+ class ProductService(BaseService):
250
+ endpoint: str = 'products'
251
+ create_if_it_doesnt_exist: bool = Field(default=False, exclude=True)
252
+
253
+ def __call__(self, **payload):
254
+ self._record = Product(**payload)
255
+ return self
256
+
257
+ def synchronize(self, payload) -> Union[Product, SynchronizeResponse]:
258
+ endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
259
+ headers = self.config.headers.copy()
260
+ headers.update({"createifitdoesntexist": str(self.create_if_it_doesnt_exist).lower()})
261
+ product_variation = ProductVariation(**payload)
262
+ if not product_variation.code and not product_variation.sku:
263
+ raise Exception("Sku or code should be provided to update the product")
264
+ # Hacer model_dump con limpieza automática de valores vacíos
265
+ data = clean_model_dump(product_variation)
266
+ _logger.info(f"Synchronizing product: {data}")
267
+ _logger.info(f"POST {endpoint_url} - Headers: {headers} - Data: {data}")
268
+ response = requests.post(endpoint_url, json=data, headers=headers)
269
+ if not response.ok:
270
+ raise Exception(f"Error getting product {product_variation.sku} - {product_variation.code}\n {response.text}")
271
+ if response.status_code == 204:
272
+ _logger.info("Status code is 204 (No Content), product synchronized successfully but no changes were made")
273
+ return None
274
+
275
+ _logger.info(f"response text: {response.text}")
276
+ response_data = response.json()
277
+ _logger.debug(f"Response data: {response_data}")
278
+ if isinstance(response_data, list):
279
+ res = ListedSynchronizeResponse(results=response_data)
280
+ if res.results and hasattr(res.results[0], 'error_context') and res.results[0].error_context:
281
+ raise Exception(f"Errored while updating {res.results[0].error_context} {res.model_dump_json()}")
282
+ return res.results[0] if res.results else None
283
+
284
+ if isinstance(response_data, dict) and 'name' in response_data:
285
+ return Product(**response_data)
286
+
287
+ if isinstance(response_data, dict) and any(key in response_data for key in ['product', 'variation', 'statusCode']):
288
+ sync_resp = SynchronizeResponse(**response_data)
289
+ if sync_resp.error_context:
290
+ raise Exception(f"Errored while updating {sync_resp.error_context} - {sync_resp.model_dump_json()}")
291
+ return sync_resp
292
+
293
+ if isinstance(response_data, dict) and 'message' in response_data:
294
+ error_res = ErrorReason(**response_data)
295
+ raise Exception(f"Errored with the following message {error_res.message} - {error_res.model_dump_json()}")
296
+
297
+ raise Exception(f"Unhandled response format, check response {response.text}")
298
+
299
+ def get(self, product_id: int) -> "ProductService":
300
+ endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}')
301
+ headers = self.config.headers
302
+ _logger.info(f"GET {endpoint_url} - Headers: {headers}")
303
+ response = requests.get(endpoint_url, headers=headers)
304
+ if not response.ok:
305
+ raise Exception(f"Error getting product {product_id}\n {response.text}")
306
+ response_data = response.json()
307
+ return self(**response_data)
308
+
309
+ def get_bundle(self, product_id: int) -> BundleResponse:
310
+ endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/bundles')
311
+ headers = self.config.headers
312
+ _logger.info(f"GET {endpoint_url} - Headers: {headers}")
313
+ response = requests.get(endpoint_url, headers=headers)
314
+ if not response.ok:
315
+ raise Exception(f"Error getting bundle {product_id}\n {response.text}")
316
+ return BundleResponse(**response.json())
317
+
318
+ def get_ml_integration(self, product_id: int) -> MeliProduct:
319
+ endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/listingintegration')
320
+ headers = self.config.headers
321
+ _logger.info(f"GET {endpoint_url} - Headers: {headers}")
322
+ response = requests.get(endpoint_url, headers=headers)
323
+ if not response.ok:
324
+ raise Exception(f"Error getting ml integration {product_id}\n {response.text}")
325
+ response_data = response.json()
326
+ return MeliProduct(**response_data)
327
+
328
+ def search(self, params: SearchProductParams) -> SearchProduct:
329
+ endpoint: str = f'search/{self.endpoint}'
330
+ headers = self.config.headers
331
+ url = self.config.get_endpoint(endpoint)
332
+ params_dict = clean_model_dump(params)
333
+ _logger.info(f"GET {url} - Headers: {headers} - Params: {params_dict}")
334
+ response = requests.get(url, headers=headers, params=params_dict)
335
+ _logger.info(f"Response status: {response.status_code} - Response text: {response.text}")
336
+ if not response.ok:
337
+ raise Exception(f"error in searching products {response.text}")
338
+ return SearchProduct(**response.json())