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:
Egor Pavlovich Gorbunov 2026-01-28 13:46:18 +03:00
parent 0db5b9b712
commit 8245fe4d36

View file

@ -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.