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.
@@ -0,0 +1,152 @@
1
+ from typing import List, Optional, Union
2
+ from pydantic import BaseModel, Field
3
+ import logging
4
+
5
+ _logger = logging.getLogger(__name__)
6
+
7
+
8
+ class SalesOrderProduct(BaseModel):
9
+ id: int
10
+ name: str
11
+ code: Optional[str] = None
12
+ brand: Optional[str] = None
13
+
14
+
15
+ class SalesOrderVariationAttribute(BaseModel):
16
+ key: Optional[str] = None
17
+ value: Optional[str] = None
18
+
19
+
20
+ class SalesOrderVariation(BaseModel):
21
+ id: int
22
+ attributes: Optional[List[SalesOrderVariationAttribute]] = None
23
+ sku: Optional[str] = None
24
+ thumbnail: Optional[str] = None
25
+
26
+
27
+ class SalesOrderLine(BaseModel):
28
+ product: SalesOrderProduct
29
+ variation: Optional[SalesOrderVariation] = None
30
+ quantity: int
31
+ price: float
32
+
33
+
34
+ class SalesOrderCard(BaseModel):
35
+ payment_network: str = Field(alias="paymentNetwork")
36
+ first_six_digits: int = Field(alias="firstSixDigits")
37
+ last_four_digits: int = Field(alias="lastFourDigits")
38
+ cardholder_identification_number: str = Field(alias="cardholderIdentificationNumber")
39
+ cardholder_identification_type: str = Field(alias="cardholderIdentificationType")
40
+ cardholder_name: str = Field(alias="cardholderName")
41
+
42
+
43
+ class SalesOrderPaymentIntegration(BaseModel):
44
+ integration_id: str = Field(alias="integrationId")
45
+ app: int
46
+
47
+
48
+ class SalesOrderPayment(BaseModel):
49
+ date: str
50
+ amount: float
51
+ coupon_amount: Optional[float] = Field(default=None, alias="couponAmount")
52
+ status: Optional[str] = None
53
+ method: str
54
+ integration: Optional[SalesOrderPaymentIntegration] = None
55
+ transaction_fee: Optional[float] = Field(default=None, alias="transactionFee")
56
+ installments: Optional[int] = None
57
+ card: Optional[SalesOrderCard] = None
58
+ notes: Optional[str] = None
59
+ has_cancelable_status: Optional[bool] = Field(default=None, alias="hasCancelableStatus")
60
+ id: int
61
+
62
+
63
+ class SalesOrderIntegration(BaseModel):
64
+ alternate_id: Optional[str] = Field(default=None, alias="alternateId")
65
+ integration_id: Union[str, int] = Field(alias="integrationId")
66
+ app: int
67
+
68
+
69
+ class SalesOrderShipmentProduct(BaseModel):
70
+ product: int
71
+ variation: int
72
+ quantity: int
73
+
74
+
75
+ class SalesOrderShipmentMethod(BaseModel):
76
+ tracking_number: Optional[str] = Field(alias="trackingNumber")
77
+ tracking_url: Optional[str] = Field(alias="trackingUrl")
78
+ courier: Optional[str] = None
79
+ mode: Optional[str] = None
80
+ cost: Optional[float] = None
81
+ type: Optional[str] = None
82
+ eta: Optional[Union[int, str]] = Field(None)
83
+ status: Optional[str] = None
84
+
85
+
86
+ class SalesOrderShipmentIntegration(BaseModel):
87
+ id: int
88
+ integration_id: str = Field(alias="integrationId")
89
+ app: int
90
+ status: str
91
+
92
+
93
+ class SalesOrderShipment(BaseModel):
94
+ date: str
95
+ products: List[SalesOrderShipmentProduct]
96
+ method: SalesOrderShipmentMethod
97
+ integration: Optional[SalesOrderShipmentIntegration] = None
98
+
99
+
100
+ class SalesOrderResultItem(BaseModel):
101
+ codes: List[str]
102
+ contact_id: Optional[int] = Field(default=None, alias="contactId")
103
+ currency: str
104
+ date: str
105
+ delivery_method: str = Field(alias="deliveryMethod")
106
+ delivery_status: str = Field(alias="deliveryStatus")
107
+ id: str
108
+ integration_ids: List[str] = Field(alias="integrationIds")
109
+ integrations: List[SalesOrderIntegration]
110
+ invoice_integration_app: Optional[int] = Field(default=None, alias="invoiceIntegrationApp")
111
+ invoice_integration_id: Optional[str] = Field(default=None, alias="invoiceIntegrationId")
112
+ lines: List[SalesOrderLine]
113
+ payments: Optional[List[SalesOrderPayment]] = None
114
+ payment_status: str = Field(alias="paymentStatus")
115
+ payment_term: str = Field(alias="paymentTerm")
116
+ product_names: List[str] = Field(alias="productNames")
117
+ reserving_product_ids: Union[str, List[str]] = Field(alias="reservingProductIds")
118
+ sales_channel: int = Field(alias="salesChannel")
119
+ shipments: Optional[List[SalesOrderShipment]] = None
120
+ tracking_number: Optional[str] = Field(alias="trackingNumber")
121
+ skus: List[str]
122
+ status: str
123
+ tags: List[str]
124
+ warehouse: str
125
+ company_id: int = Field(alias="companyId")
126
+ shipping_cost: float = Field(alias="shippingCost")
127
+ contact_phone: Optional[str] = Field(default=None, alias="contactPhone")
128
+ brands: List[str]
129
+ courier: Optional[str] = None
130
+ order_id: int = Field(alias="orderId")
131
+ updated_at: str = Field(alias="updatedAt")
132
+ invoice_integration_created_at: Optional[str] = Field(default=None, alias="invoiceIntegrationCreatedAt")
133
+ invoice_integration_document_url: Optional[str] = Field(default=None, alias="invoiceIntegrationDocumentUrl")
134
+ has_document_url: bool = Field(alias="hasDocumentUrl")
135
+ integration_alternate_ids: Union[str, List[str]] = Field(alias="integrationAlternateIds")
136
+ cart_id: Optional[str] = Field(default=None, alias="cartId")
137
+ amount: float
138
+ has_any_shipments: bool = Field(alias="hasAnyShipments")
139
+
140
+
141
+ class SearchSalesOrder(BaseModel):
142
+ count: int
143
+ results: List[SalesOrderResultItem]
144
+
145
+
146
+ class SearchSalesOrderParams(BaseModel):
147
+ top: Optional[int]
148
+ skip: Optional[int]
149
+ filter: Optional[str] = Field(default=None, alias="$filter")
150
+
151
+ class Config:
152
+ validate_by_name = True
@@ -0,0 +1,137 @@
1
+ {
2
+ "count": 0,
3
+ "results": [
4
+ {
5
+ "codes": [
6
+ "string"
7
+ ],
8
+ "contactId": 0,
9
+ "currency": "Local",
10
+ "date": "string",
11
+ "deliveryMethod": "ToBeConfirmed",
12
+ "deliveryStatus": "ToBeConfirmed",
13
+ "id": "string",
14
+ "integrationIds": [
15
+ "string"
16
+ ],
17
+ "integrations": [
18
+ {
19
+ "alternateId": "string",
20
+ "integrationId": 0,
21
+ "app": 0
22
+ }
23
+ ],
24
+ "invoiceIntegrationApp": 0,
25
+ "invoiceIntegrationId": "string",
26
+ "lines": [
27
+ {
28
+ "product": {
29
+ "id": 0,
30
+ "name": "string",
31
+ "code": "string",
32
+ "brand": "string"
33
+ },
34
+ "variation": {
35
+ "id": 0,
36
+ "attributes": [
37
+ {
38
+ "key": "string",
39
+ "value": "string"
40
+ }
41
+ ],
42
+ "sku": "string",
43
+ "thumbnail": "string"
44
+ },
45
+ "quantity": 0,
46
+ "price": 0
47
+ }
48
+ ],
49
+ "payments": [
50
+ {
51
+ "date": "string",
52
+ "amount": 0,
53
+ "couponAmount": 0,
54
+ "status": "Pending",
55
+ "method": "Cash",
56
+ "integration": {
57
+ "integrationId": "string",
58
+ "app": 249
59
+ },
60
+ "transactionFee": 0,
61
+ "installments": 0,
62
+ "card": {
63
+ "paymentNetwork": "string",
64
+ "firstSixDigits": 0,
65
+ "lastFourDigits": 0,
66
+ "cardholderIdentificationNumber": "string",
67
+ "cardholderIdentificationType": "string",
68
+ "cardholderName": "string"
69
+ },
70
+ "notes": "string",
71
+ "hasCancelableStatus": true,
72
+ "id": 0
73
+ }
74
+ ],
75
+ "paymentStatus": "Pending",
76
+ "paymentTerm": "Advance",
77
+ "productNames": [
78
+ "string"
79
+ ],
80
+ "reservingProductIds": "string",
81
+ "salesChannel": 0,
82
+ "shipments": [
83
+ {
84
+ "date": "string",
85
+ "products": [
86
+ {
87
+ "product": 0,
88
+ "variation": 0,
89
+ "quantity": 0
90
+ }
91
+ ],
92
+ "method": {
93
+ "trackingNumber": "string",
94
+ "trackingUrl": "string",
95
+ "courier": "string",
96
+ "mode": "string",
97
+ "cost": 0,
98
+ "type": "Ship",
99
+ "eta": "string",
100
+ "status": "PickingPending"
101
+ },
102
+ "integration": {
103
+ "id": 0,
104
+ "integrationId": "string",
105
+ "app": 0,
106
+ "status": "NotAvailable"
107
+ }
108
+ }
109
+ ],
110
+ "trackingNumber": "string",
111
+ "skus": [
112
+ "string"
113
+ ],
114
+ "status": "Pending",
115
+ "tags": [
116
+ "string"
117
+ ],
118
+ "warehouse": "string",
119
+ "companyId": 0,
120
+ "shippingCost": 0,
121
+ "contactPhone": "string",
122
+ "brands": [
123
+ "string"
124
+ ],
125
+ "courier": "string",
126
+ "orderId": 0,
127
+ "updatedAt": "string",
128
+ "invoiceIntegrationCreatedAt": "string",
129
+ "invoiceIntegrationDocumentUrl": "string",
130
+ "hasDocumentUrl": true,
131
+ "integrationAlternateIds": "string",
132
+ "cartId": "string",
133
+ "amount": 0,
134
+ "hasAnyShipments": true
135
+ }
136
+ ]
137
+ }
@@ -1,17 +1,25 @@
1
1
  import unittest
2
2
  from unittest.mock import patch, Mock
3
- from producteca.config.config import ConfigProducteca
4
- from producteca.sales_orders.sales_orders import SaleOrder, SaleOrderInvoiceIntegration
3
+ from producteca.sales_orders.sales_orders import SaleOrder
4
+ from producteca.client import ProductecaClient
5
5
 
6
6
 
7
7
  class TestSaleOrder(unittest.TestCase):
8
+
8
9
  def setUp(self):
9
- self.config = ConfigProducteca(token="test_client", api_key="test_secret")
10
+ self.client = ProductecaClient(token="test_client", api_key="test_secret")
10
11
  self.sale_order_id = 123
11
12
  self.mock_response = {
12
13
  "id": self.sale_order_id,
13
14
  "contact": {"id": 1, "name": "Test Contact"},
14
- "lines": []
15
+ "lines": [],
16
+ "invoiceIntegration": {
17
+ 'id': 1,
18
+ 'integrationId': 'test-integration',
19
+ 'app': 1,
20
+ 'createdAt': '2023-01-01',
21
+ 'decreaseStock': True
22
+ }
15
23
  }
16
24
 
17
25
  @patch('requests.get')
@@ -21,7 +29,7 @@ class TestSaleOrder(unittest.TestCase):
21
29
  json=lambda: self.mock_response
22
30
  )
23
31
 
24
- sale_order = SaleOrder.get(self.config, self.sale_order_id)
32
+ sale_order = self.client.SalesOrder.get(self.sale_order_id)
25
33
  self.assertEqual(sale_order.id, self.sale_order_id)
26
34
  mock_get.assert_called_once()
27
35
 
@@ -33,20 +41,33 @@ class TestSaleOrder(unittest.TestCase):
33
41
  json=lambda: mock_labels
34
42
  )
35
43
 
36
- labels = SaleOrder.get_shipping_labels(self.config, self.sale_order_id)
44
+ labels = self.client.SalesOrder(id=1234, invoiceIntegration={
45
+ 'id': 1,
46
+ 'integrationId': 'test-integration',
47
+ 'app': 1,
48
+ 'createdAt': '2023-01-01',
49
+ 'decreaseStock': True,
50
+ "documentUrl": "https://aallala.copm",
51
+ "xmlUrl": "https://aallala.copm",
52
+ }).get_shipping_labels()
37
53
  self.assertEqual(labels, mock_labels)
38
54
  mock_get.assert_called_once()
39
55
 
40
56
  @patch('requests.post')
41
57
  def test_close_sale_order(self, mock_post):
42
58
  mock_post.return_value = Mock(
43
- status_code=200,
44
- json=lambda: {"status": "closed"}
59
+ status_code=200
45
60
  )
46
61
 
47
- status_code, response = SaleOrder.close(self.config, self.sale_order_id)
48
- self.assertEqual(status_code, 200)
49
- self.assertEqual(response, {"status": "closed"})
62
+ self.client.SalesOrder(id=1234, invoiceIntegration={
63
+ 'id': 1,
64
+ 'integrationId': 'test-integration',
65
+ 'app': 1,
66
+ 'createdAt': '2023-01-01',
67
+ 'decreaseStock': True,
68
+ "documentUrl": "https://aallala.copm",
69
+ "xmlUrl": "https://aallala.copm",
70
+ }).close()
50
71
  mock_post.assert_called_once()
51
72
 
52
73
  @patch('requests.post')
@@ -56,9 +77,15 @@ class TestSaleOrder(unittest.TestCase):
56
77
  json=lambda: {"status": "cancelled"}
57
78
  )
58
79
 
59
- status_code, response = SaleOrder.cancel(self.config, self.sale_order_id)
60
- self.assertEqual(status_code, 200)
61
- self.assertEqual(response, {"status": "cancelled"})
80
+ self.client.SalesOrder(id=1234, invoiceIntegration={
81
+ 'id': 1,
82
+ 'integrationId': 'test-integration',
83
+ 'app': 1,
84
+ 'createdAt': '2023-01-01',
85
+ 'decreaseStock': True,
86
+ "documentUrl": "https://aallala.copm",
87
+ "xmlUrl": "https://aallala.copm",
88
+ }).cancel()
62
89
  mock_post.assert_called_once()
63
90
 
64
91
  @patch('requests.post')
@@ -69,30 +96,32 @@ class TestSaleOrder(unittest.TestCase):
69
96
  json=lambda: self.mock_response
70
97
  )
71
98
 
72
- status_code, response = SaleOrder.synchronize(self.config, sale_order)
73
- self.assertEqual(status_code, 200)
99
+ response = self.client.SalesOrder(**sale_order.model_dump(by_alias=True)).synchronize()
74
100
  self.assertEqual(response.id, self.sale_order_id)
75
101
  mock_post.assert_called_once()
76
102
 
77
103
  @patch('requests.put')
78
104
  def test_invoice_integration(self, mock_put):
79
105
  invoice_data = {
80
- "id": 1,
81
- "integrationId": "test123",
82
- "app": 1
83
- }
84
- invoice_integration = SaleOrderInvoiceIntegration(**invoice_data)
85
- sale_order = SaleOrder(id=self.sale_order_id, invoiceIntegration=invoice_integration)
106
+ 'id': 1,
107
+ 'integrationId': 'test-integration',
108
+ 'app': 1,
109
+ 'createdAt': '2023-01-01',
110
+ 'decreaseStock': True,
111
+ "documentUrl": "https://aallala.copm",
112
+ "xmlUrl": "https://aallala.copm",
113
+ }
86
114
 
87
115
  mock_put.return_value = Mock(
88
116
  status_code=200,
89
- json=lambda: {}
117
+ json=lambda: invoice_data,
118
+ ok=True
90
119
  )
91
120
 
92
- status_code, response = SaleOrder.invoice_integration(self.config, self.sale_order_id, sale_order)
93
- self.assertEqual(status_code, 200)
94
- self.assertEqual(response, {})
121
+ response = self.client.SalesOrder(id=self.sale_order_id, invoiceIntegration=invoice_data).invoice_integration()
122
+ self.assertTrue(response)
95
123
  mock_put.assert_called_once()
96
124
 
125
+
97
126
  if __name__ == '__main__':
98
127
  unittest.main()
@@ -0,0 +1,46 @@
1
+ import unittest
2
+ import json
3
+ from unittest.mock import patch, Mock
4
+ from producteca.sales_orders.search_sale_orders import SearchSalesOrderParams, SalesOrderResultItem
5
+ from producteca.client import ProductecaClient
6
+
7
+
8
+ class TestSearchSalesOrder(unittest.TestCase):
9
+
10
+ def setUp(self):
11
+ self.client = ProductecaClient(token="test_client_id", api_key="test_client_secret")
12
+ self.params = SearchSalesOrderParams(
13
+ top=10,
14
+ skip=0,
15
+ filter="status eq 'confirmed'"
16
+ )
17
+
18
+ @patch('requests.get')
19
+ def test_search_saleorder_success(self, mock_get):
20
+ # Mock successful response
21
+ mock_response = Mock()
22
+ with open('producteca/sales_orders/tests/search.json', 'r') as f:
23
+ results = json.loads(f.read())
24
+ mock_response.json.return_value = results
25
+ mock_response.status_code = 200
26
+ mock_get.return_value = mock_response
27
+
28
+ response = self.client.SalesOrder.search(self.params)
29
+
30
+ self.assertEqual(response.count, 0)
31
+ self.assertEqual(len(response.results), 1)
32
+ self.assertEqual(response.results[0].id, "string")
33
+ self.assertIsInstance(response.results[0], SalesOrderResultItem)
34
+
35
+ @patch('requests.get')
36
+ def test_search_saleorder_error(self, mock_get):
37
+ mock_response = Mock()
38
+ mock_response.json.return_value = {"error": "Invalid request"}
39
+ mock_response.status_code = 400
40
+ mock_get.return_value = mock_response
41
+ with self.assertRaises(Exception):
42
+ self.client.SalesOrder.search(self.params)
43
+
44
+
45
+ if __name__ == '__main__':
46
+ unittest.main()
@@ -1,7 +1,5 @@
1
1
  from typing import List, Optional
2
2
  from pydantic import BaseModel
3
- import requests
4
- from ..config.config import ConfigProducteca
5
3
 
6
4
 
7
5
  class ShipmentProduct(BaseModel):
@@ -33,15 +31,3 @@ class Shipment(BaseModel):
33
31
  products: Optional[List[ShipmentProduct]] = None
34
32
  method: Optional[ShipmentMethod] = None
35
33
  integration: Optional[ShipmentIntegration] = None
36
-
37
- @classmethod
38
- def create(cls, config: ConfigProducteca, sale_order_id: int, payload: "Shipment") -> "Shipment":
39
- url = config.get_endpoint(f"salesorders/{sale_order_id}/shipments")
40
- res = requests.post(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
41
- return res.status_code, res.json()
42
-
43
- @classmethod
44
- def update(cls, config: ConfigProducteca, sale_order_id: int, shipment_id: str, payload: "Shipment") -> "Shipment":
45
- url = config.get_endpoint(f"salesorders/{sale_order_id}/shipments/{shipment_id}")
46
- res = requests.put(url, data=payload.model_dump_json(exclude_none=True), headers=config.headers)
47
- return res.status_code, res.json()
@@ -1,57 +1,62 @@
1
1
  import unittest
2
2
  from unittest.mock import patch, MagicMock
3
- from producteca.shipments.shipment import Shipment, ShipmentProduct, ShipmentMethod, ShipmentIntegration, ConfigProducteca
3
+ from producteca.shipments.shipment import Shipment, ShipmentProduct, ShipmentMethod, ShipmentIntegration
4
+ from producteca.client import ProductecaClient
4
5
 
5
6
 
6
7
  class TestShipment(unittest.TestCase):
7
-
8
+ def setUp(self):
9
+ self.client = ProductecaClient(token="test_id", api_key="test_secret")
10
+
8
11
  @patch('requests.post')
9
12
  def test_create_shipment(self, mock_post):
10
13
  # Arrange
11
- config = ConfigProducteca(token="test_token", api_key="as")
12
- sale_order_id = 123
13
14
  products = [ShipmentProduct(product=1, variation=2, quantity=3)]
14
15
  method = ShipmentMethod(trackingNumber="TN123", trackingUrl="http://track.url", courier="DHL", mode="air", cost=10.5, type="express", eta=5, status="shipped")
15
16
  integration = ShipmentIntegration(id=1, integrationId="int123", app=10, status="active")
16
- payload = Shipment(date="2023-01-01", products=products, method=method, integration=integration)
17
+ payload = Shipment(date="2023-01-01", products=products, method=method, integration=integration).model_dump(by_alias=True)
17
18
 
18
19
  mock_response = MagicMock()
19
20
  mock_response.status_code = 201
20
- mock_response.json.return_value = {'success': True}
21
+ mock_response.json.return_value = payload
21
22
  mock_post.return_value = mock_response
22
-
23
23
  # Act
24
- status_code, response_json = Shipment.create(config, sale_order_id, payload)
25
-
26
- # Assert
27
- self.assertEqual(status_code, 201)
28
- self.assertEqual(response_json, {'success': True})
24
+ shipment = self.client.SalesOrder(id=1234, invoiceIntegration={
25
+ 'id': 1,
26
+ 'integrationId': 'test-integration',
27
+ 'app': 1,
28
+ 'createdAt': '2023-01-01',
29
+ 'decreaseStock': True
30
+ }).add_shipment(payload)
31
+
32
+ self.assertIsInstance(shipment, Shipment)
29
33
  mock_post.assert_called_once()
30
34
 
31
35
  @patch('requests.put')
32
36
  def test_update_shipment(self, mock_put):
33
37
  # Arrange
34
- config = ConfigProducteca(token="test_token", api_key="as")
35
- sale_order_id = 123
36
38
  shipment_id = 'abc'
37
39
  products = [ShipmentProduct(product=4, quantity=7)]
38
40
  method = ShipmentMethod(courier="FedEx", cost=15.0)
39
41
  integration = ShipmentIntegration(status="pending")
40
- payload = Shipment(date="2023-02-02", products=products, method=method, integration=integration)
42
+ payload = Shipment(date="2023-02-02", products=products, method=method, integration=integration).model_dump(by_alias=True)
41
43
 
42
44
  mock_response = MagicMock()
43
45
  mock_response.status_code = 200
44
- mock_response.json.return_value = {'updated': True}
46
+ mock_response.json.return_value = payload
45
47
  mock_put.return_value = mock_response
46
-
47
48
  # Act
48
- status_code, response_json = Shipment.update(config, sale_order_id, shipment_id, payload)
49
-
50
- # Assert
51
- self.assertEqual(status_code, 200)
52
- self.assertEqual(response_json, {'updated': True})
49
+ shipment = self.client.SalesOrder(id=1234, invoiceIntegration={
50
+ 'id': 1,
51
+ 'integrationId': 'test-integration',
52
+ 'app': 1,
53
+ 'createdAt': '2023-01-01',
54
+ 'decreaseStock': True
55
+ }).update_shipment(shipment_id, payload)
56
+
57
+ self.assertIsInstance(shipment, Shipment)
53
58
  mock_put.assert_called_once()
54
59
 
55
60
 
56
61
  if __name__ == '__main__':
57
- unittest.main()
62
+ unittest.main()
producteca/utils.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ Utility functions for the producteca package
3
+ """
4
+ from typing import Any, Dict, List, Union
5
+ from pydantic import BaseModel
6
+
7
+
8
+ def exclude_empty_values(obj: Any) -> Any:
9
+ """Recursively remove None, empty lists, empty strings, and empty dicts
10
+
11
+ Note: Preserves 0 values for numeric fields as they are valid prices/quantities
12
+ Special case: Preserves empty list [] for 'updatableProperties' to allow explicit updates
13
+ """
14
+ if isinstance(obj, dict):
15
+ filtered_dict = {}
16
+ for k, v in obj.items():
17
+ # Special case: preserve empty list for updatableProperties
18
+ # This allows explicitly sending [] to clear/update the field
19
+ if k == 'updatableProperties' and v == []:
20
+ filtered_dict[k] = v
21
+ continue
22
+
23
+ # Skip None, empty lists, empty strings, empty dicts
24
+ if v is None or v == [] or v == "" or v == {}:
25
+ continue
26
+ # Keep numeric 0 values - they are valid for prices, quantities, etc.
27
+ filtered_dict[k] = exclude_empty_values(v)
28
+ return filtered_dict
29
+ elif isinstance(obj, list):
30
+ filtered_list = [exclude_empty_values(item) for item in obj if item is not None]
31
+ return [item for item in filtered_list if item != [] and item != "" and item != {}]
32
+ else:
33
+ return obj
34
+
35
+
36
+ def clean_model_dump(model: BaseModel, by_alias: bool = True, exclude_none: bool = True, **kwargs) -> Dict[str, Any]:
37
+ """
38
+ Enhanced model_dump that automatically cleans empty values
39
+
40
+ Args:
41
+ model: Pydantic model instance
42
+ by_alias: Use field aliases in output
43
+ exclude_none: Exclude None values
44
+ **kwargs: Additional arguments to pass to model_dump
45
+
46
+ Returns:
47
+ Clean dictionary with empty values removed
48
+ """
49
+ raw_data = model.model_dump(by_alias=by_alias, exclude_none=exclude_none, **kwargs)
50
+ return exclude_empty_values(raw_data)