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
|
||||
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
|
||||
- update_stock(): Synchronize product stock with vendor
|
||||
- update_order_products_statuses(): Update order product statuses 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:
|
||||
class MyVendorIntegration(AbstractVendor):
|
||||
def __init__(self):
|
||||
|
|
@ -151,7 +175,8 @@ class AbstractVendor(ABC):
|
|||
vendor = self.get_vendor_instance(safe=True)
|
||||
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:
|
||||
case LogLevel.DEBUG:
|
||||
if settings.DEBUG:
|
||||
|
|
@ -172,7 +197,7 @@ class AbstractVendor(ABC):
|
|||
case _:
|
||||
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):
|
||||
if settings.DEBUG or config.SAVE_VENDORS_RESPONSES:
|
||||
vendor_instance = self.get_vendor_instance()
|
||||
|
|
@ -204,13 +229,13 @@ class AbstractVendor(ABC):
|
|||
filename = f"response_{timestamp}.json"
|
||||
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(
|
||||
filename, content, save=True
|
||||
)
|
||||
|
||||
self.log(
|
||||
self._log(
|
||||
LogLevel.DEBUG,
|
||||
f"Saved vendor's response to {filename} successfuly!",
|
||||
)
|
||||
|
|
@ -219,9 +244,10 @@ class AbstractVendor(ABC):
|
|||
raise VendorDebuggingError("Could not save response")
|
||||
|
||||
@staticmethod
|
||||
def chunk_data(
|
||||
def _chunk_data(
|
||||
data: list[Any] | None = None, num_chunks: int = 20
|
||||
) -> list[list[Any]] | list[Any]:
|
||||
"""Split data into approximately equal chunks for batch processing."""
|
||||
if not data:
|
||||
return []
|
||||
total = len(data)
|
||||
|
|
@ -231,12 +257,15 @@ class AbstractVendor(ABC):
|
|||
return [data[i : i + chunk_size] for i in range(0, total, chunk_size)]
|
||||
|
||||
@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),
|
||||
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
|
||||
if isinstance(value, bool):
|
||||
|
|
@ -290,9 +319,10 @@ class AbstractVendor(ABC):
|
|||
return value, "string"
|
||||
|
||||
@staticmethod
|
||||
def auto_resolver_helper(
|
||||
def _auto_resolver_helper(
|
||||
model: type[Brand] | type[Category], resolving_name: str
|
||||
) -> Brand | Category | None:
|
||||
"""Internal helper for resolving Brand/Category by name with deduplication."""
|
||||
queryset = model.objects.filter(name=resolving_name)
|
||||
if not queryset.exists():
|
||||
if len(resolving_name) > 255:
|
||||
|
|
@ -331,7 +361,7 @@ class AbstractVendor(ABC):
|
|||
except Category.DoesNotExist:
|
||||
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:
|
||||
if brand_name:
|
||||
|
|
@ -349,7 +379,7 @@ class AbstractVendor(ABC):
|
|||
except Brand.DoesNotExist:
|
||||
pass
|
||||
|
||||
return self.auto_resolver_helper(Brand, brand_name)
|
||||
return self._auto_resolver_helper(Brand, brand_name)
|
||||
|
||||
def resolve_price(
|
||||
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]
|
||||
|
||||
@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: 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.
|
||||
|
||||
|
|
@ -479,7 +509,7 @@ class AbstractVendor(ABC):
|
|||
return Product.objects.none() # type: ignore[return-value]
|
||||
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.
|
||||
|
||||
|
|
@ -492,7 +522,7 @@ class AbstractVendor(ABC):
|
|||
return Stock.objects.none() # type: ignore[return-value]
|
||||
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.
|
||||
|
||||
|
|
@ -519,7 +549,7 @@ class AbstractVendor(ABC):
|
|||
- "delete": Delete immediately (use with caution)
|
||||
- "description": Mark with special description marker
|
||||
"""
|
||||
products = self.get_products_queryset()
|
||||
products = self._get_products_queryset()
|
||||
if not products.exists():
|
||||
return
|
||||
|
||||
|
|
@ -551,7 +581,7 @@ class AbstractVendor(ABC):
|
|||
Returns:
|
||||
Total number of products deleted.
|
||||
"""
|
||||
products_qs = self.get_products_queryset()
|
||||
products_qs = self._get_products_queryset()
|
||||
|
||||
match inactivation_method:
|
||||
case "deactivate":
|
||||
|
|
@ -571,9 +601,9 @@ class AbstractVendor(ABC):
|
|||
|
||||
Warning: This is a destructive operation. Use with caution.
|
||||
"""
|
||||
self.get_attribute_values_queryset().delete()
|
||||
self.get_stocks_queryset().delete()
|
||||
self.get_products_queryset().delete()
|
||||
self._get_attribute_values_queryset().delete()
|
||||
self._get_stocks_queryset().delete()
|
||||
self._get_products_queryset().delete()
|
||||
|
||||
def get_or_create_attribute_safe(
|
||||
self, *, name: str, attr_group: AttributeGroup
|
||||
|
|
@ -602,20 +632,20 @@ class AbstractVendor(ABC):
|
|||
def process_attribute(
|
||||
self, key: str, value: Any, product: Product, attr_group: AttributeGroup
|
||||
) -> AttributeValue | None:
|
||||
self.log(
|
||||
self._log(
|
||||
LogLevel.DEBUG,
|
||||
f"Trying to save attribute {key} with value {value} to {attr_group.name} of {product.pk}",
|
||||
)
|
||||
|
||||
if not value:
|
||||
self.log(
|
||||
self._log(
|
||||
LogLevel.WARNING,
|
||||
f"No value for attribute {key!r} at {product.name!r}...",
|
||||
)
|
||||
return None
|
||||
|
||||
if not attr_group:
|
||||
self.log(
|
||||
self._log(
|
||||
LogLevel.WARNING,
|
||||
f"No group for attribute {key!r} at {product.name!r}...",
|
||||
)
|
||||
|
|
@ -624,7 +654,7 @@ class AbstractVendor(ABC):
|
|||
if key in self.blocked_attributes:
|
||||
return None
|
||||
|
||||
value, attr_value_type = self.auto_convert_value(value)
|
||||
value, attr_value_type = self._auto_convert_value(value)
|
||||
|
||||
if len(key) > 255:
|
||||
key = key[:255]
|
||||
|
|
@ -660,7 +690,7 @@ class AbstractVendor(ABC):
|
|||
continue
|
||||
raise
|
||||
except IntegrityError:
|
||||
self.log(
|
||||
self._log(
|
||||
LogLevel.WARNING,
|
||||
f"IntegrityError while processing attribute {key!r}...",
|
||||
)
|
||||
|
|
@ -673,14 +703,14 @@ class AbstractVendor(ABC):
|
|||
defaults={"is_active": True},
|
||||
)
|
||||
|
||||
self.log(
|
||||
self._log(
|
||||
LogLevel.DEBUG,
|
||||
f"Succesfully saved attribute {key} with value {value} to {attr_group.name} of {product.pk} into {av.uuid}",
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue