Skip to content

Commit e00db78

Browse files
Merge branch 'development'
2 parents 346838d + 0704703 commit e00db78

File tree

8 files changed

+98
-79
lines changed

8 files changed

+98
-79
lines changed

OneSila/sales_channels/integrations/mirakl/factories/feeds/build.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ def _has_pending_feed_for_group(self, *, feed: MiraklSalesChannelFeed) -> bool:
5858
type=MiraklSalesChannelFeed.TYPE_COMBINED,
5959
status__in=[
6060
MiraklSalesChannelFeed.STATUS_PENDING,
61-
MiraklSalesChannelFeed.STATUS_SUBMITTED,
6261
MiraklSalesChannelFeed.STATUS_PROCESSING,
6362
],
6463
).exclude(id=feed.id).exists()

OneSila/sales_channels/integrations/mirakl/factories/feeds/product_payloads.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
MiraklSalesChannelFeed,
4242
MiraklSalesChannelFeedItem,
4343
)
44+
from sales_channels.integrations.mirakl.utils.helpers import (
45+
build_remote_property_mapping_label,
46+
get_local_instance_label,
47+
)
4448
from sales_channels.integrations.mirakl.utils.type_parameters import get_mirakl_type_parameter_value
4549
from sales_channels.models import SalesChannelFeedItem, SalesChannelIntegrationPricelist
4650
from sales_prices.models import SalesPriceListItem
@@ -86,8 +90,10 @@ def get_select_value(self, *, product_property=None, remote_property=None, langu
8690

8791
mapped_value = self.remote_select_lookup.get((remote_property.id, local_select_value.id))
8892
if mapped_value is None:
93+
local_value_label = get_local_instance_label(local_instance=local_select_value)
8994
raise MissingMappingError(
90-
f"Map the OneSila select value for Mirakl field '{remote_property.code}' before pushing."
95+
f"Map the OneSila select value '{local_value_label}' for "
96+
f"{build_remote_property_mapping_label(remote_property=remote_property)} before pushing."
9197
)
9298
return self._serialize_remote_select_value(select_value=mapped_value)
9399

@@ -110,8 +116,10 @@ def get_select_value_multiple(self, *, product_property=None, remote_property=No
110116
for local_select_value in product_property.value_multi_select.all():
111117
mapped_value = self.remote_select_lookup.get((remote_property.id, local_select_value.id))
112118
if mapped_value is None:
119+
local_value_label = get_local_instance_label(local_instance=local_select_value)
113120
raise MissingMappingError(
114-
f"Map all OneSila multiselect values for Mirakl field '{remote_property.code}' before pushing."
121+
f"Map the OneSila multiselect value '{local_value_label}' for "
122+
f"{build_remote_property_mapping_label(remote_property=remote_property)} before pushing."
115123
)
116124
mapped_values.append(self._serialize_remote_select_value(select_value=mapped_value))
117125

@@ -771,7 +779,7 @@ def _resolve_property_value(
771779
representation_type=representation_type,
772780
):
773781
raise MissingMappingError(
774-
f"Map Mirakl field '{remote_property.code}'"
782+
f"Map {build_remote_property_mapping_label(remote_property=remote_property)} before pushing."
775783
)
776784
raise PreFlightCheckError(
777785
self._build_missing_required_value_message(
@@ -795,11 +803,7 @@ def _build_missing_required_value_message(
795803
f"Mirakl required field '{remote_property.code}' is missing for product {product_label}."
796804
)
797805

798-
local_property_label = (
799-
getattr(local_instance, "internal_name", None)
800-
or getattr(local_instance, "name", None)
801-
or str(local_instance.id)
802-
)
806+
local_property_label = get_local_instance_label(local_instance=local_instance)
803807
parent_product = product_context.get("parent_product")
804808
if product is not None and parent_product is not None and getattr(product, "id", None) != getattr(parent_product, "id", None):
805809
return (

OneSila/sales_channels/integrations/mirakl/factories/mixins.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class GetMiraklAPIMixin:
1717
default_timeout = 30
1818
default_page_size = 100
1919

20+
def _build_retry_delay(self, *, attempt: int) -> int:
21+
return max(1, 2 ** max(0, attempt - 1))
22+
2023
def get_api(self):
2124
return self
2225

@@ -82,17 +85,37 @@ def _request(
8285
headers.setdefault("Content-Type", "application/json")
8386

8487
for attempt in range(1, max_attempts + 1):
85-
response = self.get_mirakl_session().request(
86-
method=method,
87-
url=url,
88-
params=request_params,
89-
json=None if files else payload,
90-
data=None if not files else payload,
91-
files=files,
92-
headers=headers,
93-
timeout=request_timeout,
94-
verify=self.sales_channel.verify_ssl,
95-
)
88+
try:
89+
response = self.get_mirakl_session().request(
90+
method=method,
91+
url=url,
92+
params=request_params,
93+
json=None if files else payload,
94+
data=None if not files else payload,
95+
files=files,
96+
headers=headers,
97+
timeout=request_timeout,
98+
verify=self.sales_channel.verify_ssl,
99+
)
100+
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc:
101+
if attempt < max_attempts:
102+
sleep_for = self._build_retry_delay(attempt=attempt)
103+
logger.warning(
104+
"Mirakl request failed for %s %s (%s/%s). Retrying in %s seconds.",
105+
method,
106+
path,
107+
attempt,
108+
max_attempts,
109+
sleep_for,
110+
exc_info=exc,
111+
)
112+
time.sleep(sleep_for)
113+
continue
114+
raise ValueError(
115+
f"Mirakl request timed out for {method} {path} after {max_attempts} attempts: {exc}"
116+
) from exc
117+
except requests.exceptions.RequestException as exc:
118+
raise ValueError(f"Mirakl request failed for {method} {path}: {exc}") from exc
96119

97120
if response.status_code == 429 and attempt < max_attempts:
98121
retry_after = response.headers.get("Retry-After", "1")

OneSila/sales_channels/integrations/mirakl/factories/sales_channels/full_schema.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
class MiraklFullSchemaSyncFactory(GetMiraklAPIMixin):
3333
"""Fetch and mirror the Mirakl schema metadata."""
3434

35+
schema_timeout = 120
36+
3537
def __init__(self, *, sales_channel, import_process=None):
3638
self.sales_channel = sales_channel
3739
self.import_process = import_process
@@ -125,33 +127,35 @@ def run(self) -> dict[str, int]:
125127
return self.summary_data
126128

127129
def _get_document_types(self) -> list[dict[str, Any]]:
128-
response = self.mirakl_get(path="/api/documents")
130+
response = self.mirakl_get(path="/api/documents", timeout=self.schema_timeout)
129131
return self._normalize_records(response.get("document_types"))
130132

131133
def _get_offer_states(self) -> list[dict[str, Any]]:
132134
response = self.mirakl_get(
133135
path="/api/offers/states",
134136
params={"active": "true"},
137+
timeout=self.schema_timeout,
135138
)
136139
return self._normalize_records(response.get("offer_states"))
137140

138141
def _get_logistic_classes(self) -> list[dict[str, Any]]:
139-
response = self.mirakl_get(path="/api/shipping/logistic_classes")
142+
response = self.mirakl_get(path="/api/shipping/logistic_classes", timeout=self.schema_timeout)
140143
return self._normalize_records(response.get("logistic_classes"))
141144

142145
def _get_hierarchies(self) -> list[dict[str, Any]]:
143-
response = self.mirakl_get(path="/api/hierarchies")
146+
response = self.mirakl_get(path="/api/hierarchies", timeout=self.schema_timeout)
144147
return self._normalize_records(response.get("hierarchies"))
145148

146149
def _get_attributes(self) -> list[dict[str, Any]]:
147150
response = self.mirakl_get(
148151
path="/api/products/attributes",
149152
params={"all_operator_attributes": True},
153+
timeout=self.schema_timeout,
150154
)
151155
return self._normalize_records(response.get("attributes"))
152156

153157
def _get_value_lists(self) -> list[dict[str, Any]]:
154-
response = self.mirakl_get(path="/api/values_lists")
158+
response = self.mirakl_get(path="/api/values_lists", timeout=self.schema_timeout)
155159
return self._normalize_records(response.get("values_lists"))
156160

157161
def _prepare_progress(

OneSila/sales_channels/integrations/mirakl/tests/tests_factories/test_product_payloads.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from media.models import Media, MediaProductThrough
1010
from products.models import ConfigurableVariation
1111
from properties.models import Property, ProductProperty, PropertySelectValue
12-
from properties.models import ProductPropertiesRule, PropertySelectValueTranslation
12+
from properties.models import ProductPropertiesRule, PropertySelectValueTranslation, PropertyTranslation
1313
from properties.models import ProductPropertyTextTranslation
1414
from products.models import ProductTranslation, ProductTranslationBulletPoint
1515
from sales_channels.exceptions import MiraklPayloadValidationError, MissingMappingError, PreFlightCheckError
@@ -272,6 +272,12 @@ def test_select_field_with_unmapped_local_value_still_raises_missing_mapping(sel
272272
multi_tenant_company=self.multi_tenant_company,
273273
type=Property.TYPES.SELECT,
274274
)
275+
PropertyTranslation.objects.create(
276+
multi_tenant_company=self.multi_tenant_company,
277+
property=local_property,
278+
language="en",
279+
name="Colour",
280+
)
275281
builder, _, product = self._build_builder(
276282
remote_code="colour",
277283
local_property=local_property,
@@ -283,6 +289,12 @@ def test_select_field_with_unmapped_local_value_still_raises_missing_mapping(sel
283289
multi_tenant_company=self.multi_tenant_company,
284290
property=local_property,
285291
)
292+
PropertySelectValueTranslation.objects.create(
293+
multi_tenant_company=self.multi_tenant_company,
294+
propertyselectvalue=local_select_value,
295+
language="en",
296+
value="Purple",
297+
)
286298
baker.make(
287299
ProductProperty,
288300
multi_tenant_company=self.multi_tenant_company,
@@ -293,7 +305,7 @@ def test_select_field_with_unmapped_local_value_still_raises_missing_mapping(sel
293305

294306
with self.assertRaisesMessage(
295307
MissingMappingError,
296-
"Missing Mirakl mappings:\n- Map the OneSila select value for Mirakl field 'colour' before pushing.",
308+
"Missing Mirakl mappings:\n- Map the OneSila select value 'Purple' for Mirakl field 'colour' (local 'Colour') before pushing.",
297309
):
298310
builder.build()
299311

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from __future__ import annotations
2+
3+
4+
def get_local_instance_label(*, local_instance=None) -> str:
5+
if local_instance is None:
6+
return ""
7+
8+
for attr_name in ("name", "internal_name", "value"):
9+
label = getattr(local_instance, attr_name, None)
10+
if label not in (None, "", "No Name Set", "No Value Set"):
11+
return str(label)
12+
13+
local_instance_id = getattr(local_instance, "id", None)
14+
if local_instance_id is None:
15+
return ""
16+
return str(local_instance_id)
17+
18+
19+
def build_remote_property_mapping_label(*, remote_property=None) -> str:
20+
remote_property_code = getattr(remote_property, "code", "") or ""
21+
local_property_label = get_local_instance_label(
22+
local_instance=getattr(remote_property, "local_instance", None),
23+
)
24+
if not local_property_label:
25+
return f"Mirakl field '{remote_property_code}'"
26+
return f"Mirakl field '{remote_property_code}' (local '{local_property_label}')"

OneSila/sales_channels/integrations/shopify/factories/sales_channels/oauth.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ def __init__(self, sales_channel: ShopifySalesChannel):
1919
self.redirect_url = None
2020

2121
shopify.Session.setup(
22-
api_key=self.sales_channel.api_key,
23-
secret=self.sales_channel.api_secret
22+
api_key=settings.SHOPIFY_API_KEY,
23+
secret=settings.SHOPIFY_API_SECRET
2424
)
2525

2626
def clean_shop_hostname(self, hostname: str) -> str:
@@ -73,10 +73,7 @@ def __init__(self, sales_channel: ShopifySalesChannel, shop: str, code: str, hma
7373
self.host = host
7474

7575
def exchange_token(self):
76-
shopify.Session.setup(
77-
api_key=self.sales_channel.api_key,
78-
secret=self.sales_channel.api_secret,
79-
)
76+
shopify.Session.setup(api_key=settings.SHOPIFY_API_KEY, secret=settings.SHOPIFY_API_SECRET)
8077
session = shopify.Session(self.shop, settings.SHOPIFY_API_VERSION)
8178

8279
try:

OneSila/sales_channels/schema/mutations/mutation_type.py

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -35,53 +35,7 @@
3535

3636
@type(name='Mutation')
3737
class SalesChannelsMutation:
38-
@strawberry_django.mutation(handle_django_errors=True, extensions=default_extensions)
39-
def create_sales_import_process(
40-
self,
41-
data: SalesChannelImportInput,
42-
info: Info,
43-
) -> SalesChannelImportType:
44-
multi_tenant_company = get_multi_tenant_company(info, fail_silently=False)
45-
sales_channel = SalesChannel.objects.get(
46-
id=data.sales_channel.id.node_id,
47-
multi_tenant_company=multi_tenant_company,
48-
).get_real_instance()
49-
50-
import_name = str(getattr(data, "name", "") or "").strip()
51-
import_status = getattr(data, "status", SalesChannelImport.STATUS_NEW)
52-
53-
from sales_channels.integrations.mirakl.models import MiraklSalesChannel, MiraklSalesChannelImport
54-
55-
if isinstance(sales_channel, MiraklSalesChannel):
56-
normalized_name = import_name.lower()
57-
import_type = (
58-
MiraklSalesChannelImport.TYPE_PRODUCTS
59-
if normalized_name == MiraklSalesChannelImport.TYPE_PRODUCTS
60-
else MiraklSalesChannelImport.TYPE_SCHEMA
61-
)
62-
return MiraklSalesChannelImport.objects.create(
63-
multi_tenant_company=multi_tenant_company,
64-
sales_channel=sales_channel,
65-
name=import_name,
66-
status=import_status,
67-
create_only=getattr(data, "create_only", False),
68-
update_only=getattr(data, "update_only", False),
69-
override_only=getattr(data, "override_only", False),
70-
skip_broken_records=getattr(data, "skip_broken_records", False),
71-
type=import_type,
72-
)
73-
74-
return SalesChannelImport.objects.create(
75-
multi_tenant_company=multi_tenant_company,
76-
sales_channel=sales_channel,
77-
name=import_name,
78-
status=import_status,
79-
create_only=getattr(data, "create_only", False),
80-
update_only=getattr(data, "update_only", False),
81-
override_only=getattr(data, "override_only", False),
82-
skip_broken_records=getattr(data, "skip_broken_records", False),
83-
)
84-
38+
create_sales_import_process: SalesChannelImportType = create(SalesChannelImportInput)
8539
create_sales_import_processes: List[SalesChannelImportType] = create(SalesChannelImportInput)
8640
update_sales_import_process: SalesChannelImportType = update(SalesChannelImportPartialInput)
8741
delete_sales_import_process: SalesChannelImportType = delete()

0 commit comments

Comments
 (0)