From 8245fe4d364bcc4df50305dd46368080b7a95c86 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Wed, 28 Jan 2026 13:46:18 +0300 Subject: [PATCH] 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. --- engine/core/vendors/__init__.py | 88 ++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/engine/core/vendors/__init__.py b/engine/core/vendors/__init__.py index 876bbd11..1b10b38c 100644 --- a/engine/core/vendors/__init__.py +++ b/engine/core/vendors/__init__.py @@ -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.