producteca 1.0.13__py3-none-any.whl → 2.0.14__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/abstract/abstract_dataclass.py +9 -21
- producteca/client.py +7 -22
- producteca/config/__init__.py +0 -1
- producteca/payments/payments.py +1 -13
- producteca/payments/tests/test_payments.py +5 -5
- producteca/products/__init__.py +0 -1
- producteca/products/products.py +88 -63
- producteca/{search/search.py → products/search_products.py} +1 -11
- producteca/products/tests/test_products.py +25 -27
- producteca/{search/tests/test_search.py → products/tests/test_search_products.py} +5 -76
- producteca/sales_orders/__init__.py +0 -1
- producteca/sales_orders/sales_orders.py +135 -42
- 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 +17 -18
- producteca/sales_orders/tests/test_search_so.py +48 -0
- producteca/shipments/shipment.py +0 -14
- producteca/shipments/tests/test_shipment.py +10 -17
- {producteca-1.0.13.dist-info → producteca-2.0.14.dist-info}/METADATA +31 -1
- producteca-2.0.14.dist-info/RECORD +31 -0
- producteca/search/__init__.py +0 -0
- producteca/search/search_sale_orders.py +0 -170
- producteca/search/tests/__init__.py +0 -0
- producteca-1.0.13.dist-info/RECORD +0 -31
- {producteca-1.0.13.dist-info → producteca-2.0.14.dist-info}/WHEEL +0 -0
- {producteca-1.0.13.dist-info → producteca-2.0.14.dist-info}/entry_points.txt +0 -0
@@ -1,23 +1,11 @@
|
|
1
|
-
from
|
2
|
-
from
|
3
|
-
from
|
1
|
+
from abc import ABC
|
2
|
+
from ..config.config import ConfigProducteca
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Optional
|
4
5
|
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def endpoint(self) -> str:
|
12
|
-
pass
|
13
|
-
|
14
|
-
def dict(self, *args, **kwargs):
|
15
|
-
return super().dict(*args, exclude_none=True, **kwargs)
|
16
|
-
|
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)
|
7
|
+
@dataclass
|
8
|
+
class BaseService[T](ABC):
|
9
|
+
config: ConfigProducteca
|
10
|
+
endpoint: str
|
11
|
+
_record: Optional[T] = None
|
producteca/client.py
CHANGED
@@ -1,37 +1,22 @@
|
|
1
1
|
from producteca.config.config import ConfigProducteca
|
2
|
-
from producteca.products.products import
|
3
|
-
from producteca.sales_orders.sales_orders import
|
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
|
7
4
|
import os
|
8
5
|
|
9
6
|
|
10
7
|
class ProductecaClient:
|
11
8
|
|
12
|
-
def __init__(self, token: str = os.environ
|
9
|
+
def __init__(self, token: str = os.environ.get('PRODUCTECA_TOKEN', ''), api_key: str = os.environ.get('PRODUCTECA_API_KEY', '')):
|
13
10
|
if not token:
|
14
11
|
raise ValueError('PRODUCTECA_TOKEN environment variable not set')
|
15
12
|
if not api_key:
|
16
13
|
raise ValueError('PRODUCTECA_API_KEY environment variable not set')
|
17
14
|
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
15
|
|
27
16
|
@property
|
28
|
-
def
|
29
|
-
return
|
30
|
-
|
31
|
-
@property
|
32
|
-
def Shipment(self):
|
33
|
-
return lambda *args: Shipment(config=self.config, *args)
|
17
|
+
def Product(self):
|
18
|
+
return ProductService(self.config)
|
34
19
|
|
35
20
|
@property
|
36
|
-
def
|
37
|
-
return
|
21
|
+
def SalesOrder(self):
|
22
|
+
return SaleOrderService(self.config)
|
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):
|
@@ -32,14 +30,4 @@ class Payment(BaseModel):
|
|
32
30
|
hasCancelableStatus: bool
|
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,7 @@ 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).add_payment(payment)
|
39
39
|
|
40
40
|
# Assertions
|
41
41
|
mock_post.assert_called_once()
|
@@ -55,7 +55,7 @@ class TestPayments(unittest.TestCase):
|
|
55
55
|
payment = Payment(**self.payment_data)
|
56
56
|
|
57
57
|
# Test update method
|
58
|
-
result =
|
58
|
+
result = self.client.SalesOrder(id=self.sale_order_id).update_payment(self.payment_id, payment)
|
59
59
|
|
60
60
|
# Assertions
|
61
61
|
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,23 @@
|
|
1
1
|
from typing import List, Optional, Union
|
2
2
|
from pydantic import BaseModel, Field
|
3
|
-
import
|
4
|
-
from
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from producteca.abstract.abstract_dataclass import BaseService
|
5
|
+
from producteca.products.search_products import SearchProduct, SearchProductParams
|
5
6
|
import logging
|
7
|
+
import requests
|
6
8
|
|
7
9
|
_logger = logging.getLogger(__name__)
|
8
10
|
|
9
|
-
# Models for nested structures
|
10
11
|
|
11
12
|
class Attribute(BaseModel):
|
12
13
|
key: str
|
13
14
|
value: str
|
14
15
|
|
16
|
+
|
15
17
|
class Tag(BaseModel):
|
16
18
|
tag: str
|
17
19
|
|
20
|
+
|
18
21
|
class Dimensions(BaseModel):
|
19
22
|
weight: Optional[float] = None
|
20
23
|
width: Optional[float] = None
|
@@ -22,11 +25,13 @@ class Dimensions(BaseModel):
|
|
22
25
|
length: Optional[float] = None
|
23
26
|
pieces: Optional[int] = None
|
24
27
|
|
28
|
+
|
25
29
|
class Deal(BaseModel):
|
26
30
|
campaign: str
|
27
31
|
regular_price: Optional[float] = Field(default=None, alias='regularPrice')
|
28
32
|
deal_price: Optional[float] = Field(default=None, alias='dealPrice')
|
29
33
|
|
34
|
+
|
30
35
|
class Stock(BaseModel):
|
31
36
|
quantity: Optional[int] = None
|
32
37
|
available_quantity: Optional[int] = Field(default=None, alias='availableQuantity')
|
@@ -35,15 +40,18 @@ class Stock(BaseModel):
|
|
35
40
|
reserved: Optional[int] = None
|
36
41
|
available: Optional[int] = None
|
37
42
|
|
43
|
+
|
38
44
|
class Price(BaseModel):
|
39
45
|
amount: Optional[float] = None
|
40
46
|
currency: str
|
41
47
|
price_list: str = Field(alias='priceList')
|
42
48
|
price_list_id: Optional[int] = Field(default=None, alias='priceListId')
|
43
49
|
|
50
|
+
|
44
51
|
class Picture(BaseModel):
|
45
52
|
url: str
|
46
53
|
|
54
|
+
|
47
55
|
class Integration(BaseModel):
|
48
56
|
app: Optional[int] = None
|
49
57
|
integration_id: Optional[str] = Field(default=None, alias='integrationId')
|
@@ -57,6 +65,7 @@ class Integration(BaseModel):
|
|
57
65
|
id: Optional[int] = None
|
58
66
|
parent_integration: Optional[str] = Field(default=None, alias='parentIntegration')
|
59
67
|
|
68
|
+
|
60
69
|
class Variation(BaseModel):
|
61
70
|
variation_id: Optional[int] = Field(default=None, alias='variationId')
|
62
71
|
components: Optional[List] = None
|
@@ -71,11 +80,15 @@ class Variation(BaseModel):
|
|
71
80
|
sku: Optional[str] = None
|
72
81
|
barcode: Optional[str] = None
|
73
82
|
|
74
|
-
|
83
|
+
|
84
|
+
class MeliCategory(BaseModel):
|
85
|
+
meli_id: Optional[str] = Field(default=None, alias='meliId')
|
86
|
+
accepts_mercadoenvios: Optional[bool] = Field(default=None, alias='acceptsMercadoenvios')
|
87
|
+
suggest: Optional[bool] = None
|
88
|
+
fixed: Optional[bool] = None
|
89
|
+
|
90
|
+
|
75
91
|
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
92
|
sku: Optional[str] = None
|
80
93
|
variation_id: Optional[int] = None
|
81
94
|
code: Optional[str] = None
|
@@ -85,7 +98,7 @@ class Product(BaseModel):
|
|
85
98
|
tags: Optional[List[str]] = None
|
86
99
|
buying_price: Optional[float] = None
|
87
100
|
dimensions: Optional[Dimensions] = None
|
88
|
-
category: Optional[Union[str,
|
101
|
+
category: Optional[Union[str, MeliCategory]]
|
89
102
|
brand: Optional[str] = None
|
90
103
|
notes: Optional[str] = None
|
91
104
|
deals: Optional[List[Deal]] = None
|
@@ -94,60 +107,6 @@ class Product(BaseModel):
|
|
94
107
|
pictures: Optional[List[Picture]] = None
|
95
108
|
|
96
109
|
|
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
|
129
|
-
|
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
|
-
|
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
|
143
|
-
|
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
|
-
|
151
110
|
class Shipping(BaseModel):
|
152
111
|
local_pickup: Optional[bool] = Field(default=None, alias='localPickup')
|
153
112
|
mode: Optional[str] = None
|
@@ -156,9 +115,11 @@ class Shipping(BaseModel):
|
|
156
115
|
mandatory_free_shipping: Optional[bool] = Field(default=None, alias='mandatoryFreeShipping')
|
157
116
|
free_shipping_method: Optional[str] = Field(default=None, alias='freeShippingMethod')
|
158
117
|
|
118
|
+
|
159
119
|
class MShopsShipping(BaseModel):
|
160
120
|
enabled: Optional[bool] = None
|
161
121
|
|
122
|
+
|
162
123
|
class AttributeCompletion(BaseModel):
|
163
124
|
product_identifier_status: Optional[str] = Field(default=None, alias='productIdentifierStatus')
|
164
125
|
data_sheet_status: Optional[str] = Field(default=None, alias='dataSheetStatus')
|
@@ -166,13 +127,14 @@ class AttributeCompletion(BaseModel):
|
|
166
127
|
count: Optional[int] = None
|
167
128
|
total: Optional[int] = None
|
168
129
|
|
130
|
+
|
169
131
|
class MeliProduct(Product):
|
170
132
|
product_id: Optional[int] = Field(default=None, alias='productId')
|
171
133
|
has_custom_shipping_costs: Optional[bool] = Field(default=None, alias='hasCustomShippingCosts')
|
172
134
|
shipping: Optional[Shipping] = None
|
173
135
|
mshops_shipping: Optional[MShopsShipping] = Field(default=None, alias='mShopsShipping')
|
174
136
|
add_free_shipping_cost_to_price: Optional[bool] = Field(default=None, alias='addFreeShippingCostToPrice')
|
175
|
-
category: Optional[MeliCategory]
|
137
|
+
category: Optional[Union[str, MeliCategory]] # will never be str, but needs compat with super class
|
176
138
|
attribute_completion: Optional[AttributeCompletion] = Field(default=None, alias='attributeCompletion')
|
177
139
|
catalog_products: Optional[List[str]] = Field(default=None, alias='catalogProducts')
|
178
140
|
warranty: Optional[str] = None
|
@@ -180,3 +142,66 @@ class MeliProduct(Product):
|
|
180
142
|
listing_type_id: Optional[str] = Field(default=None, alias='listingTypeId')
|
181
143
|
catalog_products_status: Optional[str] = Field(default=None, alias='catalogProductsStatus')
|
182
144
|
|
145
|
+
|
146
|
+
@dataclass
|
147
|
+
class ProductService(BaseService[Product]):
|
148
|
+
endpoint: str = Field(default='products', exclude=True)
|
149
|
+
create_if_it_doesnt_exist: bool = Field(default=False, exclude=True)
|
150
|
+
|
151
|
+
def __call__(self, **payload):
|
152
|
+
self._record = Product(**payload)
|
153
|
+
return self
|
154
|
+
|
155
|
+
def create(self):
|
156
|
+
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
|
157
|
+
headers = self.config.headers.copy()
|
158
|
+
headers.update({"createifitdoesntexist": str(self.create_if_it_doesnt_exist).lower()})
|
159
|
+
data = self._record.model_dump_json(by_alias=True, exclude_none=True)
|
160
|
+
response = requests.post(endpoint_url, data=data, headers=headers)
|
161
|
+
if response.status_code == 204:
|
162
|
+
raise Exception("Product does not exist and the request cant create if it does not exist")
|
163
|
+
return Product(**response.json())
|
164
|
+
|
165
|
+
def update(self):
|
166
|
+
# TODO: Change name to synchronize
|
167
|
+
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/synchronize')
|
168
|
+
headers = self.config.headers.copy()
|
169
|
+
data = self._record.model_dump_json(by_alias=True, exclude_none=True)
|
170
|
+
if not self._record.code and not self._record.sku:
|
171
|
+
raise "Sku or code should be provided to update the product"
|
172
|
+
response = requests.post(endpoint_url, data=data, headers=headers)
|
173
|
+
if response.status_code == 204:
|
174
|
+
raise Exception("Product does not exist and the request cant create if it does not exist")
|
175
|
+
return Product(**response.json())
|
176
|
+
|
177
|
+
def get(self, product_id: int) -> "Product":
|
178
|
+
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}')
|
179
|
+
headers = self.config.headers
|
180
|
+
response = requests.get(endpoint_url, headers=headers)
|
181
|
+
if not response.ok:
|
182
|
+
raise Exception(f"Error getting product {product_id}\n {response.text}")
|
183
|
+
response_data = response.json()
|
184
|
+
return Product(**response_data)
|
185
|
+
|
186
|
+
def get_bundle(self, product_id: int) -> "Product":
|
187
|
+
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/bundles')
|
188
|
+
headers = self.config.headers
|
189
|
+
response = requests.get(endpoint_url, headers=headers)
|
190
|
+
if not response.ok:
|
191
|
+
raise Exception(f"Error getting bundle {product_id}\n {response.text}")
|
192
|
+
return Product(**response.json())
|
193
|
+
|
194
|
+
def get_ml_integration(self, product_id: int) -> "MeliProduct":
|
195
|
+
endpoint_url = self.config.get_endpoint(f'{self.endpoint}/{product_id}/listingintegration')
|
196
|
+
headers = self.config.headers
|
197
|
+
response = requests.get(endpoint_url, headers=headers)
|
198
|
+
if not response.ok:
|
199
|
+
raise Exception(f"Error getting ml integration {product_id}\n {response.text}")
|
200
|
+
return MeliProduct(**response.json())
|
201
|
+
|
202
|
+
def search(self, params: SearchProductParams) -> SearchProduct:
|
203
|
+
endpoint: str = f'search/{self.endpoint}'
|
204
|
+
headers = self.config.headers
|
205
|
+
url = self.config.get_endpoint(endpoint)
|
206
|
+
response = requests.get(url, headers=headers, params=params.model_dump(by_alias=True, exclude_none=True))
|
207
|
+
return SearchProduct(**response.json())
|
@@ -1,7 +1,5 @@
|
|
1
1
|
from typing import List, Optional
|
2
2
|
from pydantic import BaseModel, Field
|
3
|
-
import requests
|
4
|
-
from ..config.config import ConfigProducteca
|
5
3
|
|
6
4
|
|
7
5
|
class FacetValue(BaseModel):
|
@@ -124,7 +122,7 @@ class SearchResultItem(BaseModel):
|
|
124
122
|
channel_pictures_templates_apps: Optional[List[str]]
|
125
123
|
|
126
124
|
|
127
|
-
class
|
125
|
+
class SearchProduct(BaseModel):
|
128
126
|
count: int
|
129
127
|
facets: List[Facet]
|
130
128
|
results: List[SearchResultItem]
|
@@ -138,12 +136,4 @@ class SearchProductParams(BaseModel):
|
|
138
136
|
sales_channel: Optional[str] = Field(default='2', alias='salesChannel')
|
139
137
|
|
140
138
|
|
141
|
-
class SearchProduct:
|
142
|
-
endpoint: str = 'search/products'
|
143
139
|
|
144
|
-
@classmethod
|
145
|
-
def search_product(cls, config: ConfigProducteca, params: SearchProductParams) -> SearchProductResponse:
|
146
|
-
headers = config.headers
|
147
|
-
url = config.get_endpoint(cls.endpoint)
|
148
|
-
response = requests.get(url, headers=headers, params=params.model_dump(by_alias=True, exclude_none=True))
|
149
|
-
return SearchProductResponse(**response.json())
|
@@ -1,17 +1,17 @@
|
|
1
1
|
import unittest
|
2
2
|
from unittest.mock import patch, Mock
|
3
|
-
from producteca.
|
4
|
-
from producteca.
|
3
|
+
from producteca.products.products import Product
|
4
|
+
from producteca.client import ProductecaClient
|
5
5
|
|
6
6
|
|
7
7
|
class TestProduct(unittest.TestCase):
|
8
8
|
def setUp(self):
|
9
|
-
self.
|
9
|
+
self.client = ProductecaClient(token="test_client_id", api_key="test_client_secret")
|
10
10
|
self.test_product = Product(
|
11
|
-
config=self.config,
|
12
11
|
sku="TEST001",
|
13
12
|
name="Test Product",
|
14
|
-
code="TEST001"
|
13
|
+
code="TEST001",
|
14
|
+
category="Test"
|
15
15
|
)
|
16
16
|
|
17
17
|
@patch('requests.post')
|
@@ -19,13 +19,12 @@ class TestProduct(unittest.TestCase):
|
|
19
19
|
# Mock successful response
|
20
20
|
mock_response = Mock()
|
21
21
|
mock_response.status_code = 200
|
22
|
-
mock_response.json.return_value =
|
22
|
+
mock_response.json.return_value = self.test_product.model_dump()
|
23
23
|
mock_post.return_value = mock_response
|
24
24
|
|
25
|
-
response
|
25
|
+
response = self.client.Product(**self.test_product.model_dump()).create()
|
26
26
|
|
27
|
-
self.assertEqual(
|
28
|
-
self.assertEqual(response["sku"], "TEST001")
|
27
|
+
self.assertEqual(response.sku, "TEST001")
|
29
28
|
|
30
29
|
@patch('requests.post')
|
31
30
|
def test_create_product_not_exist(self, mock_post):
|
@@ -34,48 +33,45 @@ class TestProduct(unittest.TestCase):
|
|
34
33
|
mock_response.status_code = 204
|
35
34
|
mock_post.return_value = mock_response
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
self.assertEqual(status_code, 204)
|
40
|
-
self.assertEqual(response["Message"], "Product does not exist and the request cant create if it does not exist")
|
36
|
+
with self.assertRaises(Exception):
|
37
|
+
self.client.Product.create()
|
41
38
|
|
42
39
|
@patch('requests.post')
|
43
40
|
def test_update_product_success(self, mock_post):
|
44
41
|
# Mock successful update
|
45
42
|
mock_response = Mock()
|
46
43
|
mock_response.status_code = 200
|
47
|
-
mock_response.json.return_value =
|
44
|
+
mock_response.json.return_value = self.test_product.model_dump()
|
48
45
|
mock_post.return_value = mock_response
|
49
46
|
|
50
|
-
response
|
47
|
+
response = self.client.Product(**self.test_product.model_dump()).update()
|
51
48
|
|
52
|
-
self.assertEqual(
|
53
|
-
self.assertEqual(response["name"], "Updated Product")
|
49
|
+
self.assertEqual(response.name, "Test Product")
|
54
50
|
|
55
51
|
@patch('requests.get')
|
56
52
|
def test_get_product(self, mock_get):
|
57
53
|
# Mock get product response
|
58
54
|
mock_response = Mock()
|
59
55
|
mock_response.status_code = 200
|
60
|
-
mock_response.json.return_value =
|
56
|
+
mock_response.json.return_value = self.test_product.model_dump()
|
61
57
|
mock_get.return_value = mock_response
|
62
58
|
|
63
|
-
response
|
59
|
+
response = self.client.Product.get(1)
|
64
60
|
|
65
|
-
self.assertEqual(
|
66
|
-
self.assertEqual(response["sku"], "TEST001")
|
61
|
+
self.assertEqual(response.sku, "TEST001")
|
67
62
|
|
68
63
|
@patch('requests.get')
|
69
64
|
def test_get_bundle(self, mock_get):
|
70
65
|
# Mock get bundle response
|
71
66
|
mock_response = Mock()
|
72
67
|
mock_response.status_code = 200
|
73
|
-
|
68
|
+
test_prod = self.test_product.model_dump()
|
69
|
+
test_prod.update({"sku": "TEST001", "bundles": []})
|
70
|
+
mock_response.json.return_value = test_prod
|
74
71
|
mock_get.return_value = mock_response
|
75
72
|
|
76
|
-
product
|
73
|
+
product = self.client.Product.get_bundle(1)
|
77
74
|
|
78
|
-
self.assertEqual(status_code, 200)
|
79
75
|
self.assertEqual(product.sku, "TEST001")
|
80
76
|
|
81
77
|
@patch('requests.get')
|
@@ -83,13 +79,15 @@ class TestProduct(unittest.TestCase):
|
|
83
79
|
# Mock ML integration response
|
84
80
|
mock_response = Mock()
|
85
81
|
mock_response.status_code = 200
|
86
|
-
|
82
|
+
test_prod = self.test_product.model_dump()
|
83
|
+
test_prod.update({"sku": "TEST001", "integrations": []})
|
84
|
+
mock_response.json.return_value = test_prod
|
87
85
|
mock_get.return_value = mock_response
|
88
86
|
|
89
|
-
product
|
87
|
+
product = self.client.Product.get_ml_integration(1)
|
90
88
|
|
91
|
-
self.assertEqual(status_code, 200)
|
92
89
|
self.assertEqual(product.sku, "TEST001")
|
93
90
|
|
91
|
+
|
94
92
|
if __name__ == '__main__':
|
95
93
|
unittest.main()
|
@@ -1,81 +1,12 @@
|
|
1
1
|
import unittest
|
2
2
|
from unittest.mock import patch, Mock
|
3
|
-
from producteca.
|
4
|
-
from producteca.
|
5
|
-
from producteca.search.search import SearchProduct, SearchProductParams
|
6
|
-
|
7
|
-
class TestSearchSalesOrder(unittest.TestCase):
|
8
|
-
def setUp(self):
|
9
|
-
self.config = ConfigProducteca(
|
10
|
-
token="test_client_id",
|
11
|
-
api_key="test_client_secret",
|
12
|
-
)
|
13
|
-
self.params = SearchSalesOrderParams(
|
14
|
-
top=10,
|
15
|
-
skip=0,
|
16
|
-
filter="status eq 'confirmed'"
|
17
|
-
)
|
18
|
-
|
19
|
-
@patch('requests.get')
|
20
|
-
def test_search_saleorder_success(self, mock_get):
|
21
|
-
# Mock successful response
|
22
|
-
mock_response = Mock()
|
23
|
-
mock_response.json.return_value = {
|
24
|
-
"count": 1,
|
25
|
-
"results": [{
|
26
|
-
"id": "123",
|
27
|
-
"status": "confirmed",
|
28
|
-
"lines": [],
|
29
|
-
"payments": [],
|
30
|
-
"shipments": [],
|
31
|
-
"integrations": [],
|
32
|
-
"codes": [],
|
33
|
-
"integration_ids": [],
|
34
|
-
"product_names": [],
|
35
|
-
"skus": [],
|
36
|
-
"tags": [],
|
37
|
-
"brands": []
|
38
|
-
}]
|
39
|
-
}
|
40
|
-
mock_response.status_code = 200
|
41
|
-
mock_get.return_value = mock_response
|
42
|
-
|
43
|
-
response, status_code = SearchSalesOrder.search_saleorder(self.config, self.params)
|
44
|
-
|
45
|
-
# Validate response
|
46
|
-
self.assertEqual(status_code, 200)
|
47
|
-
self.assertEqual(response["count"], 1)
|
48
|
-
self.assertEqual(len(response["results"]), 1)
|
49
|
-
self.assertEqual(response["results"][0]["id"], "123")
|
50
|
-
|
51
|
-
# Verify the request was made with correct parameters
|
52
|
-
expected_url = f"{self.config.get_endpoint(SearchSalesOrder.endpoint)}?$filter={self.params.filter}&top={self.params.top}&skip={self.params.skip}"
|
53
|
-
mock_get.assert_called_once_with(
|
54
|
-
expected_url,
|
55
|
-
headers=self.config.headers
|
56
|
-
)
|
57
|
-
|
58
|
-
@patch('requests.get')
|
59
|
-
def test_search_saleorder_error(self, mock_get):
|
60
|
-
# Mock error response
|
61
|
-
mock_response = Mock()
|
62
|
-
mock_response.json.return_value = {"error": "Invalid request"}
|
63
|
-
mock_response.status_code = 400
|
64
|
-
mock_get.return_value = mock_response
|
65
|
-
|
66
|
-
response, status_code = SearchSalesOrder.search_saleorder(self.config, self.params)
|
67
|
-
|
68
|
-
# Validate error response
|
69
|
-
self.assertEqual(status_code, 400)
|
70
|
-
self.assertEqual(response["error"], "Invalid request")
|
3
|
+
from producteca.products.search_products import SearchProductParams
|
4
|
+
from producteca.client import ProductecaClient
|
71
5
|
|
72
6
|
|
73
7
|
class TestSearchProduct(unittest.TestCase):
|
74
8
|
def setUp(self):
|
75
|
-
self.
|
76
|
-
token="test_client_id",
|
77
|
-
api_key="test_client_secret",
|
78
|
-
)
|
9
|
+
self.client = ProductecaClient(token="test_client_id", api_key="test_client_secret")
|
79
10
|
self.params = SearchProductParams(
|
80
11
|
top=10,
|
81
12
|
skip=0,
|
@@ -181,15 +112,13 @@ class TestSearchProduct(unittest.TestCase):
|
|
181
112
|
mock_response.status_code = 200
|
182
113
|
mock_get.return_value = mock_response
|
183
114
|
|
184
|
-
response =
|
115
|
+
response = self.client.Product.search(self.params)
|
185
116
|
|
186
117
|
# Validate response
|
187
118
|
self.assertEqual(response.count, 1)
|
188
119
|
self.assertEqual(len(response.results), 1)
|
189
120
|
self.assertEqual(response.results[0].id, 123)
|
190
121
|
self.assertEqual(response.results[0].name, "Test Product")
|
191
|
-
|
192
|
-
|
193
122
|
|
194
123
|
@patch('requests.get')
|
195
124
|
def test_search_product_error(self, mock_get):
|
@@ -200,7 +129,7 @@ class TestSearchProduct(unittest.TestCase):
|
|
200
129
|
mock_get.return_value = mock_response
|
201
130
|
# TODO: Fix this
|
202
131
|
# with self.assertRaises(Exception):
|
203
|
-
#
|
132
|
+
# self.client.Product.search(self.params)
|
204
133
|
|
205
134
|
|
206
135
|
if __name__ == '__main__':
|
@@ -1 +0,0 @@
|
|
1
|
-
from . import sales_orders
|