refactor(vendors): standardize method visibility and improve clarity
- Prefixed internal helper methods with "_" to comply with naming conventions. - Enhanced docstrings for improved readability and developer guidance. - Organized the `AbstractVendor` class API into distinct categories (abstract, public, and protected methods). This refactor improves maintainability and code clarity while preserving functionality.
This commit is contained in:
parent
0db5b9b712
commit
8245fe4d36
1 changed files with 59 additions and 29 deletions
88
engine/core/vendors/__init__.py
vendored
88
engine/core/vendors/__init__.py
vendored
|
|
@ -105,12 +105,36 @@ class AbstractVendor(ABC):
|
||||||
data for products and stocks, and performing bulk operations like updates or
|
data for products and stocks, and performing bulk operations like updates or
|
||||||
deletions on inactive objects.
|
deletions on inactive objects.
|
||||||
|
|
||||||
Subclasses must implement the following abstract methods:
|
Abstract Methods (must be implemented by subclasses):
|
||||||
- get_products(): Fetch products from vendor's API
|
- get_products(): Fetch products from vendor's API
|
||||||
- update_stock(): Synchronize product stock with vendor
|
- update_stock(): Synchronize product stock with vendor
|
||||||
- update_order_products_statuses(): Update order product statuses from vendor
|
- update_order_products_statuses(): Update order product statuses from vendor
|
||||||
- buy_order_product(): Process purchase of a digital product from vendor
|
- buy_order_product(): Process purchase of a digital product from vendor
|
||||||
|
|
||||||
|
Public Methods (for use by subclasses):
|
||||||
|
- get_vendor_instance(): Get the Vendor model instance
|
||||||
|
- prepare_for_stock_update(): Mark products before sync
|
||||||
|
- delete_inactives(): Clean up stale products after sync
|
||||||
|
- delete_belongings(): Remove all vendor data
|
||||||
|
- resolve_price(): Apply markup to prices
|
||||||
|
- resolve_price_with_currency(): Convert and resolve prices
|
||||||
|
- auto_resolve_category(): Find or create category by name
|
||||||
|
- auto_resolve_brand(): Find or create brand by name
|
||||||
|
- process_attribute(): Create/update product attributes
|
||||||
|
- get_or_create_attribute_safe(): Safely create attributes
|
||||||
|
|
||||||
|
Protected Methods (internal helpers, can be overridden):
|
||||||
|
- _log(): Internal logging with level support
|
||||||
|
- _save_response(): Save API response for debugging
|
||||||
|
- _chunk_data(): Split data into batches
|
||||||
|
- _auto_convert_value(): Convert attribute values to typed format
|
||||||
|
- _auto_resolver_helper(): Internal brand/category resolution
|
||||||
|
- _round_price_marketologically(): Apply psychological pricing
|
||||||
|
- _get_products_queryset(): Get vendor's products queryset
|
||||||
|
- _get_stocks_queryset(): Get vendor's stocks queryset
|
||||||
|
- _get_attribute_values_queryset(): Get vendor's attribute values
|
||||||
|
- _check_updatable(): Validate product can be updated
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
class MyVendorIntegration(AbstractVendor):
|
class MyVendorIntegration(AbstractVendor):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -151,7 +175,8 @@ class AbstractVendor(ABC):
|
||||||
vendor = self.get_vendor_instance(safe=True)
|
vendor = self.get_vendor_instance(safe=True)
|
||||||
return str(vendor.name) if vendor else self.vendor_name
|
return str(vendor.name) if vendor else self.vendor_name
|
||||||
|
|
||||||
def log(self, level: LogLevel, message: str) -> None:
|
def _log(self, level: LogLevel, message: str) -> None:
|
||||||
|
"""Internal logging helper with level-based formatting."""
|
||||||
match level:
|
match level:
|
||||||
case LogLevel.DEBUG:
|
case LogLevel.DEBUG:
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
@ -172,7 +197,7 @@ class AbstractVendor(ABC):
|
||||||
case _:
|
case _:
|
||||||
raise LoggingError("Wrong type of logging level passed: %s", level)
|
raise LoggingError("Wrong type of logging level passed: %s", level)
|
||||||
|
|
||||||
def save_response(self, data: dict[Any, Any] | list[Any]) -> None:
|
def _save_response(self, data: dict[Any, Any] | list[Any]) -> None:
|
||||||
with suppress(Exception):
|
with suppress(Exception):
|
||||||
if settings.DEBUG or config.SAVE_VENDORS_RESPONSES:
|
if settings.DEBUG or config.SAVE_VENDORS_RESPONSES:
|
||||||
vendor_instance = self.get_vendor_instance()
|
vendor_instance = self.get_vendor_instance()
|
||||||
|
|
@ -204,13 +229,13 @@ class AbstractVendor(ABC):
|
||||||
filename = f"response_{timestamp}.json"
|
filename = f"response_{timestamp}.json"
|
||||||
content = ContentFile(json_bytes)
|
content = ContentFile(json_bytes)
|
||||||
|
|
||||||
self.log(LogLevel.DEBUG, f"Saving vendor's response to {filename}")
|
self._log(LogLevel.DEBUG, f"Saving vendor's response to {filename}")
|
||||||
|
|
||||||
vendor_instance.last_processing_response.save(
|
vendor_instance.last_processing_response.save(
|
||||||
filename, content, save=True
|
filename, content, save=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(
|
self._log(
|
||||||
LogLevel.DEBUG,
|
LogLevel.DEBUG,
|
||||||
f"Saved vendor's response to {filename} successfuly!",
|
f"Saved vendor's response to {filename} successfuly!",
|
||||||
)
|
)
|
||||||
|
|
@ -219,9 +244,10 @@ class AbstractVendor(ABC):
|
||||||
raise VendorDebuggingError("Could not save response")
|
raise VendorDebuggingError("Could not save response")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def chunk_data(
|
def _chunk_data(
|
||||||
data: list[Any] | None = None, num_chunks: int = 20
|
data: list[Any] | None = None, num_chunks: int = 20
|
||||||
) -> list[list[Any]] | list[Any]:
|
) -> list[list[Any]] | list[Any]:
|
||||||
|
"""Split data into approximately equal chunks for batch processing."""
|
||||||
if not data:
|
if not data:
|
||||||
return []
|
return []
|
||||||
total = len(data)
|
total = len(data)
|
||||||
|
|
@ -231,12 +257,15 @@ class AbstractVendor(ABC):
|
||||||
return [data[i : i + chunk_size] for i in range(0, total, chunk_size)]
|
return [data[i : i + chunk_size] for i in range(0, total, chunk_size)]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def auto_convert_value(value: Any) -> tuple[Any, str]:
|
def _auto_convert_value(value: Any) -> tuple[Any, str]:
|
||||||
"""
|
"""
|
||||||
Attempts to convert a value to a more specific type.
|
Convert a value to a more specific type for attribute storage.
|
||||||
|
|
||||||
Handles booleans, numbers, objects (dicts), and arrays (lists),
|
Handles booleans, numbers, objects (dicts), and arrays (lists),
|
||||||
even when they are provided as strings.
|
even when they are provided as strings.
|
||||||
Returns a tuple of (converted_value, type_label).
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (converted_value, type_label).
|
||||||
"""
|
"""
|
||||||
# First, handle native types
|
# First, handle native types
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
|
|
@ -290,9 +319,10 @@ class AbstractVendor(ABC):
|
||||||
return value, "string"
|
return value, "string"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def auto_resolver_helper(
|
def _auto_resolver_helper(
|
||||||
model: type[Brand] | type[Category], resolving_name: str
|
model: type[Brand] | type[Category], resolving_name: str
|
||||||
) -> Brand | Category | None:
|
) -> Brand | Category | None:
|
||||||
|
"""Internal helper for resolving Brand/Category by name with deduplication."""
|
||||||
queryset = model.objects.filter(name=resolving_name)
|
queryset = model.objects.filter(name=resolving_name)
|
||||||
if not queryset.exists():
|
if not queryset.exists():
|
||||||
if len(resolving_name) > 255:
|
if len(resolving_name) > 255:
|
||||||
|
|
@ -331,7 +361,7 @@ class AbstractVendor(ABC):
|
||||||
except Category.DoesNotExist:
|
except Category.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.auto_resolver_helper(Category, category_name)
|
return self._auto_resolver_helper(Category, category_name)
|
||||||
|
|
||||||
def auto_resolve_brand(self, brand_name: str = "") -> Brand | None:
|
def auto_resolve_brand(self, brand_name: str = "") -> Brand | None:
|
||||||
if brand_name:
|
if brand_name:
|
||||||
|
|
@ -349,7 +379,7 @@ class AbstractVendor(ABC):
|
||||||
except Brand.DoesNotExist:
|
except Brand.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return self.auto_resolver_helper(Brand, brand_name)
|
return self._auto_resolver_helper(Brand, brand_name)
|
||||||
|
|
||||||
def resolve_price(
|
def resolve_price(
|
||||||
self,
|
self,
|
||||||
|
|
@ -393,9 +423,9 @@ class AbstractVendor(ABC):
|
||||||
return float(round(price / rate, 2)) if rate else float(round(price, 2)) # ty: ignore[unsupported-operator]
|
return float(round(price / rate, 2)) if rate else float(round(price, 2)) # ty: ignore[unsupported-operator]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def round_price_marketologically(price: float) -> float:
|
def _round_price_marketologically(price: float) -> float:
|
||||||
"""
|
"""
|
||||||
Marketological rounding with no cents:
|
Apply psychological pricing rounding.
|
||||||
|
|
||||||
- Prices < 1: leave exactly as-is.
|
- Prices < 1: leave exactly as-is.
|
||||||
- Prices ≥ 1: drop any fractional part, then
|
- Prices ≥ 1: drop any fractional part, then
|
||||||
|
|
@ -466,7 +496,7 @@ class AbstractVendor(ABC):
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_products_queryset(self) -> ProductQuerySet:
|
def _get_products_queryset(self) -> ProductQuerySet:
|
||||||
"""
|
"""
|
||||||
Get a queryset of products associated with this vendor.
|
Get a queryset of products associated with this vendor.
|
||||||
|
|
||||||
|
|
@ -479,7 +509,7 @@ class AbstractVendor(ABC):
|
||||||
return Product.objects.none() # type: ignore[return-value]
|
return Product.objects.none() # type: ignore[return-value]
|
||||||
return Product.objects.for_vendor_not_in_orders(vendor)
|
return Product.objects.for_vendor_not_in_orders(vendor)
|
||||||
|
|
||||||
def get_stocks_queryset(self) -> StockQuerySet:
|
def _get_stocks_queryset(self) -> StockQuerySet:
|
||||||
"""
|
"""
|
||||||
Get a queryset of stocks associated with this vendor.
|
Get a queryset of stocks associated with this vendor.
|
||||||
|
|
||||||
|
|
@ -492,7 +522,7 @@ class AbstractVendor(ABC):
|
||||||
return Stock.objects.none() # type: ignore[return-value]
|
return Stock.objects.none() # type: ignore[return-value]
|
||||||
return Stock.objects.for_vendor_not_in_orders(vendor)
|
return Stock.objects.for_vendor_not_in_orders(vendor)
|
||||||
|
|
||||||
def get_attribute_values_queryset(self) -> AttributeValueQuerySet:
|
def _get_attribute_values_queryset(self) -> AttributeValueQuerySet:
|
||||||
"""
|
"""
|
||||||
Get a queryset of attribute values for this vendor's products.
|
Get a queryset of attribute values for this vendor's products.
|
||||||
|
|
||||||
|
|
@ -519,7 +549,7 @@ class AbstractVendor(ABC):
|
||||||
- "delete": Delete immediately (use with caution)
|
- "delete": Delete immediately (use with caution)
|
||||||
- "description": Mark with special description marker
|
- "description": Mark with special description marker
|
||||||
"""
|
"""
|
||||||
products = self.get_products_queryset()
|
products = self._get_products_queryset()
|
||||||
if not products.exists():
|
if not products.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -551,7 +581,7 @@ class AbstractVendor(ABC):
|
||||||
Returns:
|
Returns:
|
||||||
Total number of products deleted.
|
Total number of products deleted.
|
||||||
"""
|
"""
|
||||||
products_qs = self.get_products_queryset()
|
products_qs = self._get_products_queryset()
|
||||||
|
|
||||||
match inactivation_method:
|
match inactivation_method:
|
||||||
case "deactivate":
|
case "deactivate":
|
||||||
|
|
@ -571,9 +601,9 @@ class AbstractVendor(ABC):
|
||||||
|
|
||||||
Warning: This is a destructive operation. Use with caution.
|
Warning: This is a destructive operation. Use with caution.
|
||||||
"""
|
"""
|
||||||
self.get_attribute_values_queryset().delete()
|
self._get_attribute_values_queryset().delete()
|
||||||
self.get_stocks_queryset().delete()
|
self._get_stocks_queryset().delete()
|
||||||
self.get_products_queryset().delete()
|
self._get_products_queryset().delete()
|
||||||
|
|
||||||
def get_or_create_attribute_safe(
|
def get_or_create_attribute_safe(
|
||||||
self, *, name: str, attr_group: AttributeGroup
|
self, *, name: str, attr_group: AttributeGroup
|
||||||
|
|
@ -602,20 +632,20 @@ class AbstractVendor(ABC):
|
||||||
def process_attribute(
|
def process_attribute(
|
||||||
self, key: str, value: Any, product: Product, attr_group: AttributeGroup
|
self, key: str, value: Any, product: Product, attr_group: AttributeGroup
|
||||||
) -> AttributeValue | None:
|
) -> AttributeValue | None:
|
||||||
self.log(
|
self._log(
|
||||||
LogLevel.DEBUG,
|
LogLevel.DEBUG,
|
||||||
f"Trying to save attribute {key} with value {value} to {attr_group.name} of {product.pk}",
|
f"Trying to save attribute {key} with value {value} to {attr_group.name} of {product.pk}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
self.log(
|
self._log(
|
||||||
LogLevel.WARNING,
|
LogLevel.WARNING,
|
||||||
f"No value for attribute {key!r} at {product.name!r}...",
|
f"No value for attribute {key!r} at {product.name!r}...",
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not attr_group:
|
if not attr_group:
|
||||||
self.log(
|
self._log(
|
||||||
LogLevel.WARNING,
|
LogLevel.WARNING,
|
||||||
f"No group for attribute {key!r} at {product.name!r}...",
|
f"No group for attribute {key!r} at {product.name!r}...",
|
||||||
)
|
)
|
||||||
|
|
@ -624,7 +654,7 @@ class AbstractVendor(ABC):
|
||||||
if key in self.blocked_attributes:
|
if key in self.blocked_attributes:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
value, attr_value_type = self.auto_convert_value(value)
|
value, attr_value_type = self._auto_convert_value(value)
|
||||||
|
|
||||||
if len(key) > 255:
|
if len(key) > 255:
|
||||||
key = key[:255]
|
key = key[:255]
|
||||||
|
|
@ -660,7 +690,7 @@ class AbstractVendor(ABC):
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
self.log(
|
self._log(
|
||||||
LogLevel.WARNING,
|
LogLevel.WARNING,
|
||||||
f"IntegrityError while processing attribute {key!r}...",
|
f"IntegrityError while processing attribute {key!r}...",
|
||||||
)
|
)
|
||||||
|
|
@ -673,14 +703,14 @@ class AbstractVendor(ABC):
|
||||||
defaults={"is_active": True},
|
defaults={"is_active": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log(
|
self._log(
|
||||||
LogLevel.DEBUG,
|
LogLevel.DEBUG,
|
||||||
f"Succesfully saved attribute {key} with value {value} to {attr_group.name} of {product.pk} into {av.uuid}",
|
f"Succesfully saved attribute {key} with value {value} to {attr_group.name} of {product.pk} into {av.uuid}",
|
||||||
)
|
)
|
||||||
|
|
||||||
return av
|
return av
|
||||||
|
|
||||||
def check_updatable(self, product: Product) -> None:
|
def _check_updatable(self, product: Product) -> None:
|
||||||
"""
|
"""
|
||||||
Check if a product can be updated by vendor sync.
|
Check if a product can be updated by vendor sync.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue