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