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 +3 -0
- producteca/abstract/abstract_dataclass.py +20 -18
- producteca/client.py +8 -21
- producteca/config/__init__.py +0 -1
- producteca/payments/payments.py +4 -16
- producteca/payments/tests/test_payments.py +21 -5
- producteca/products/__init__.py +0 -1
- producteca/products/products.py +237 -81
- producteca/products/search_products.py +136 -0
- producteca/products/tests/test_products.py +111 -38
- producteca/{search/tests/test_search.py → products/tests/test_search_products.py} +15 -86
- producteca/sales_orders/__init__.py +0 -1
- producteca/sales_orders/sales_orders.py +292 -118
- producteca/sales_orders/search_sale_orders.py +152 -0
- producteca/sales_orders/tests/search.json +137 -0
- producteca/sales_orders/tests/test_sales_orders.py +55 -26
- producteca/sales_orders/tests/test_search_so.py +46 -0
- producteca/shipments/shipment.py +0 -14
- producteca/shipments/tests/test_shipment.py +28 -23
- producteca/utils.py +50 -0
- producteca-2.0.57.dist-info/LICENSE +661 -0
- {producteca-1.0.14.dist-info → producteca-2.0.57.dist-info}/METADATA +33 -2
- producteca-2.0.57.dist-info/RECORD +33 -0
- producteca/search/__init__.py +0 -0
- producteca/search/search.py +0 -149
- producteca/search/search_sale_orders.py +0 -170
- producteca/search/tests/__init__.py +0 -0
- producteca-1.0.14.dist-info/RECORD +0 -31
- {producteca-1.0.14.dist-info → producteca-2.0.57.dist-info}/WHEEL +0 -0
- {producteca-1.0.14.dist-info → producteca-2.0.57.dist-info}/entry_points.txt +0 -0
producteca/__init__.py
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
from ..
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
def endpoint(self) -> str:
|
|
12
|
-
pass
|
|
17
|
+
def to_dict(self):
|
|
18
|
+
return clean_model_dump(self._record)
|
|
13
19
|
|
|
14
|
-
def
|
|
15
|
-
|
|
20
|
+
def to_json(self):
|
|
21
|
+
import json
|
|
22
|
+
return json.dumps(clean_model_dump(self._record))
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
3
|
-
from producteca.sales_orders.sales_orders import
|
|
4
|
-
|
|
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
|
|
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
|
|
29
|
-
return
|
|
18
|
+
def Product(self):
|
|
19
|
+
return ProductService(self.config)
|
|
30
20
|
|
|
31
21
|
@property
|
|
32
|
-
def
|
|
33
|
-
return
|
|
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)
|
producteca/config/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .config import ConfigProducteca
|
producteca/payments/payments.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
5
|
-
from producteca.payments.payments import Payment
|
|
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.
|
|
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 =
|
|
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 =
|
|
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()
|
producteca/products/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from . import products
|
producteca/products/products.py
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
from typing import List, Optional, Union
|
|
2
|
-
from pydantic import BaseModel, Field
|
|
3
|
-
import
|
|
4
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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:
|
|
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())
|