tgshops-integrations 2.4__py3-none-any.whl → 3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,36 +1,68 @@
1
- from typing import List,Optional
2
- import importlib.util
1
+ from typing import List, Optional
3
2
  from pathlib import Path
3
+ import importlib.util
4
4
 
5
5
  from aiocache import cached
6
- from tgshops_integrations.models.products import ProductModel
6
+ from models.products import ProductModel
7
+
8
+ # TODO: For test purposes, remove in production
9
+ import sys
10
+ sys.path.append('/home/latoff/Desktop/MarketBots')
11
+
7
12
  from tgshops_integrations.nocodb_connector.client import NocodbClient
13
+ from tgshops_integrations.nocodb_connector.model_mapping import (
14
+ dump_product_data,
15
+ dump_product_data_with_check,
16
+ get_pagination_info,
17
+ ID_FIELD,
18
+ parse_product_data,
19
+ PRODUCT_CATEGORY_ID_LOOKUP_FIELD,
20
+ PRODUCT_NAME_FIELD,
21
+ PRODUCT_PRICE_FIELD,
22
+ PRODUCT_STOCK_FIELD,
23
+ PRODUCT_IMAGES_LOOKUP_FIELD,
24
+ PRODUCT_EXTERNAL_ID,
25
+ )
26
+ from services.nocodb_connector.categories import CategoryManager
27
+ from services.nocodb_connector.products import ProductManager
28
+ from loguru import logger
8
29
 
9
- from tgshops_integrations.nocodb_connector.model_mapping import dump_product_data,dump_product_data_with_check, get_pagination_info, ID_FIELD, \
10
- parse_product_data, PRODUCT_CATEGORY_ID_LOOKUP_FIELD, PRODUCT_NAME_FIELD, PRODUCT_PRICE_FIELD, \
11
- PRODUCT_STOCK_FIELD, PRODUCT_IMAGES_LOOKUP_FIELD
12
30
 
13
- from tgshops_integrations.nocodb_connector.categories import CategoryManager
14
- from tgshops_integrations.nocodb_connector.products import ProductManager
15
- from tgshops_integrations.nocodb_connector.tables import *
16
- # from .config import NOCODB_CATEGORIES,NOCODB_PRODUCTS,NOCODB_STATUSES,NOCODB_BOT_MESSAGES,NOCODB_ORDERS
31
+ def custom_key_builder(func, *args, **kwargs):
32
+ """
33
+ Key builder function for caching.
34
+ Excludes 'self' by processing args from args[1:].
35
+ """
36
+ args_key_part = "-".join(str(arg) for arg in args[1:])
37
+ kwargs_key_part = "-".join(f"{key}-{value}" for key, value in sorted(kwargs.items()))
38
+ return f"{func.__name__}-{args_key_part}-{kwargs_key_part}"
17
39
 
18
- from loguru import logger
19
40
 
20
41
  class Gateway(NocodbClient):
21
42
 
22
- def __init__(self,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None,filter_buttons=[],config_path=None,special_attributes=False):
23
- super().__init__(NOCODB_HOST=NOCODB_HOST,NOCODB_API_KEY=NOCODB_API_KEY,SOURCE=SOURCE)
24
- if config_path:
25
- self.load_config_from_path(config_path)
43
+ def __init__(
44
+ self,
45
+ logging: bool = False,
46
+ NOCODB_HOST: Optional[str] = None,
47
+ NOCODB_API_KEY: Optional[str] = None,
48
+ SOURCE: Optional[str] = None,
49
+ filter_buttons: Optional[List[str]] = [],
50
+ config_path: Optional[Path] = None,
51
+ special_attributes: bool = False,
52
+ ):
53
+ super().__init__(NOCODB_HOST=NOCODB_HOST, NOCODB_API_KEY=NOCODB_API_KEY, SOURCE=SOURCE)
26
54
 
27
55
  self.logging = logging
28
- self.required_fields = [self.config.PRODUCT_NAME_FIELD, self.config.PRODUCT_PRICE_FIELD]
56
+ self.required_fields = []
29
57
  self.projection = []
30
58
  self.special_attributes = special_attributes
31
59
  self.filter_buttons = filter_buttons
32
60
 
33
- def load_config_from_path(self,config_path):
61
+ if config_path:
62
+ self.load_config_from_path(config_path)
63
+
64
+ def load_config_from_path(self, config_path: Path):
65
+ """Loads configuration from the specified path."""
34
66
  if config_path.exists():
35
67
  spec = importlib.util.spec_from_file_location("config", config_path)
36
68
  self.config = importlib.util.module_from_spec(spec)
@@ -38,80 +70,74 @@ class Gateway(NocodbClient):
38
70
  else:
39
71
  raise FileNotFoundError(f"Configuration file not found at {config_path}")
40
72
 
41
- async def load_data(self,SOURCE=None):
42
- self.SOURCE=SOURCE
43
- await self.get_all_tables()
44
- self.category_manager=CategoryManager(table_id=self.tables_list[self.config.NOCODB_CATEGORIES],NOCODB_HOST=self.NOCODB_HOST,NOCODB_API_KEY=self.NOCODB_API_KEY,logging=True,filter_buttons=self.filter_buttons)
45
- self.product_manager=ProductManager(table_id=self.tables_list[self.config.NOCODB_PRODUCTS],NOCODB_HOST=self.NOCODB_HOST,NOCODB_API_KEY=self.NOCODB_API_KEY,logging=True)
46
-
47
- async def create_product(self,product: ProductModel) -> ProductModel:
48
- products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
49
- data = dump_product_data_with_check(data=product ,data_check=self.category_manager.categories)
50
- # product_json = dump_product_data_with_check(data=product,data_check=self.categories)
51
- external_id = data.pop("ID")
52
- metadata = await self.get_table_meta(self.tables_list["Products"])
53
- images_column=[column["id"] for column in metadata["columns"] if column["column_name"] == "Images"][0]
54
- data[PRODUCT_IMAGES_LOOKUP_FIELD]=[image['title'] for image in data['Images']]
55
-
56
- for num,item in enumerate(data['Images']):
57
- url_before=item['url']
58
- image_name=item['title']
59
- data['Images'][num]['url']=await self.save_image_to_nocodb(source_column_id=self.SOURCE,image_url=url_before,image_name=image_name,product_table_name=products_table,images_column_id=images_column)
60
-
61
- record = await self.create_table_record(table_name=products_table, record=data)
62
- # logger.info(f"Created product {record['id']}")
63
- logger.info(f"Created product {external_id}")
64
-
65
- async def get_all_products(self):
66
- actual_products=[]
67
- products_portion=[]
68
- portion=200
69
- # TODO Check once busy
70
- for i in range(10):
71
- products_portion=await self.product_manager.get_products_v2(offset=i*portion,limit=portion)
72
- actual_products.extend(products_portion)
73
- if len(products_portion) < 200:
74
- break
75
- return actual_products
76
-
77
- async def update_products(self, external_products: List[ProductModel]):
78
- products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
79
- await self.product_manager.update_attributes(products=external_products)
80
- # Updates categories if there were a new ones created
81
- # external_products=await self.category_manager.map_categories(external_products=external_products)
82
- self.product_manager.actual_products=await self.get_all_products()
83
- # self.product_manager.actual_products = await self.product_manager.get_products_v2(offset=0,limit=200)
73
+ async def load_data(self, SOURCE: Optional[str] = None):
74
+ """Loads necessary data including tables, categories, and products."""
75
+ self.SOURCE = SOURCE
76
+ self.tables_list = await self.get_all_tables()
77
+ self.products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
78
+ self.category_manager = CategoryManager(
79
+ table_id=self.tables_list[self.config.NOCODB_CATEGORIES],
80
+ NOCODB_HOST=self.NOCODB_HOST,
81
+ NOCODB_API_KEY=self.NOCODB_API_KEY,
82
+ logging=True,
83
+ filter_buttons=self.filter_buttons,
84
+ )
85
+ self.product_manager = ProductManager(
86
+ table_id=self.tables_list[self.config.NOCODB_PRODUCTS],
87
+ NOCODB_HOST=self.NOCODB_HOST,
88
+ NOCODB_API_KEY=self.NOCODB_API_KEY,
89
+ logging=True,
90
+ )
91
+
92
+ @cached(ttl=60, key_builder=custom_key_builder)
93
+ async def update_attributes(self, products: List[ProductModel]):
94
+ """Updates attributes for the product table."""
95
+ system_attributes = [PRODUCT_EXTERNAL_ID, PRODUCT_IMAGES_LOOKUP_FIELD]
96
+ attributes = await self.get_table_meta(table_name=self.products_table)
97
+ self.columns = [item['title'].lower() for item in attributes.get('columns', [])]
98
+
99
+ # Ensure system attributes exist
100
+ for attribute_name in system_attributes:
101
+ if attribute_name.lower() not in self.columns:
102
+ await self.create_table_column(table_name=self.products_table, name=attribute_name)
103
+ logger.info(f"Created attribute: {attribute_name}")
104
+
105
+ # Validate and add extra attributes
106
+ for item in products:
107
+ attributes = await self.get_table_meta(table_name=self.products_table)
108
+ self.columns = [col['title'].lower() for col in attributes.get('columns', [])]
109
+
110
+ for attribute in item.extra_attributes:
111
+ if attribute.name.rstrip().lower() not in self.columns:
112
+ await self.create_table_column(table_name=self.products_table, name=attribute.name.lower())
113
+ logger.info(f"Created attribute: {attribute.name.lower()}")
84
114
 
85
- self.ids_mapping={product.external_id : product.id for product in self.product_manager.actual_products}
86
- products_meta= {product.external_id : product for product in self.product_manager.actual_products}
115
+ async def update_products(self, external_products: List[ProductModel]):
116
+ """Updates product data by comparing with existing records."""
117
+ await self.update_attributes(products=external_products)
118
+ self.actual_products = await self.product_manager.get_all_products()
119
+ self.ids_mapping = {product.external_id: product.id for product in self.actual_products}
120
+ self.products_meta = {product.external_id: product for product in self.actual_products}
87
121
 
88
122
  for product in external_products:
89
- if product.external_id in self.ids_mapping.keys():
90
- product.id=self.ids_mapping[product.external_id]
91
- if self.product_manager.hash_product(product,special_attributes=self.special_attributes)!=self.product_manager.hash_product(products_meta[product.external_id],special_attributes=self.special_attributes):
92
- await self.update_product(product=product)
123
+ if product.external_id in self.ids_mapping:
124
+ product.id = self.ids_mapping[product.external_id]
125
+ current_hash = self.product_manager.hash_product(product, special_attributes=self.special_attributes)
126
+ existing_hash = self.product_manager.hash_product(
127
+ self.products_meta[product.external_id],
128
+ special_attributes=self.special_attributes,
129
+ )
130
+ if current_hash != existing_hash:
131
+ await self.product_manager.update_product(product=product)
93
132
  else:
94
- await self.create_product(product=product)
95
-
96
- async def update_product(self, product: ProductModel):
97
- products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
98
- data = dump_product_data_with_check(data=product ,data_check=self.category_manager.categories)
99
-
100
- await self.update_table_record(
101
- table_name=products_table,
102
- record_id=product.id,
103
- updated_data=data)
104
- logger.info(f"Updated product {product.external_id}")
105
-
106
-
107
- def find_product_id_by_name(self,name: str):
108
- for product in self.product_manager.actual_products.products:
109
- if product.name == name:
110
- return product.id
111
- return None # Return None if no product is found with the given name
112
-
113
- async def delete_all_products(self):
114
- items = await self.product_manager.get_products_v2(offset=0,limit=200)
115
- products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
116
- for num,item in enumerate(items):
117
- await self.delete_table_record(products_table, item.id)
133
+ checked_data = dump_product_data_with_check(
134
+ data=product,
135
+ data_check=self.category_manager.categories,
136
+ )
137
+ await self.product_manager.create_product(product=product, checked_data=checked_data)
138
+
139
+ async def delete_all_products(self):
140
+ """Deletes all products in the table."""
141
+ items = await self.product_manager.get_products_v2(offset=0, limit=200)
142
+ for item in items:
143
+ await self.product_manager.delete_table_record(self.products_table, item.id)
@@ -4,7 +4,6 @@ from datetime import datetime
4
4
 
5
5
  from pydantic import BaseModel, Field, schema, validator
6
6
 
7
-
8
7
  class ExtraAttribute(BaseModel):
9
8
  name: str
10
9
  description: Optional[str]
@@ -29,12 +28,12 @@ class ExternalProductModel(BaseModel):
29
28
  class ProductModel(BaseModel):
30
29
  id: Optional[str]
31
30
  external_id: Optional[str]
32
- category: Optional[List[str]]
31
+ category: Optional[List[int]]
33
32
  category_name: Optional[List[str]]
34
33
  name: str
35
34
  description: Optional[str]
36
35
  price: Optional[float]
37
- final_price: Optional[float]
36
+ final_price: Optional[int]
38
37
  currency: Optional[str]
39
38
  stock_qty: int
40
39
  orders_qty: int = Field(0, hidden_field=True)
@@ -0,0 +1,149 @@
1
+ from typing import List
2
+ from loguru import logger
3
+ from aiocache import cached
4
+ from models.categories import CategoryModel, CategoryResponseModel, CategoryListResponseModel
5
+ from models.products import ProductModel
6
+ from tgshops_integrations.nocodb_connector.client import custom_key_builder, NocodbClient
7
+ from tgshops_integrations.nocodb_connector.model_mapping import (
8
+ CATEGORY_IMAGE_FIELD,
9
+ CATEGORY_NAME_FIELD,
10
+ CATEGORY_PARENT_FIELD,
11
+ CATEGORY_PARENT_ID_FIELD,
12
+ PRODUCT_NAME_FIELD,
13
+ CATEGORY_ID_OF_CATEGORY_FIELD,
14
+ dump_category_data,
15
+ get_pagination_info,
16
+ parse_category_data,
17
+ )
18
+
19
+ class CategoryManager(NocodbClient):
20
+ def __init__(
21
+ self,
22
+ table_id=None,
23
+ logging=False,
24
+ config_type=None,
25
+ NOCODB_HOST=None,
26
+ NOCODB_API_KEY=None,
27
+ SOURCE=None,
28
+ filter_buttons=None,
29
+ ):
30
+ super().__init__(NOCODB_HOST=NOCODB_HOST, NOCODB_API_KEY=NOCODB_API_KEY, SOURCE=SOURCE)
31
+ self.NOCODB_HOST = NOCODB_HOST
32
+ self.NOCODB_API_KEY = NOCODB_API_KEY
33
+ self.SOURCE = SOURCE
34
+ self.CONFIG_TYPE = config_type
35
+ self.categories_table = table_id
36
+ self.external_categories = {}
37
+ self.logging = logging
38
+ self.filter_categories = []
39
+ self.filter_buttons = filter_buttons or []
40
+ self.required_fields = [CATEGORY_NAME_FIELD]
41
+ self.projection = ["Id", CATEGORY_NAME_FIELD, CATEGORY_PARENT_ID_FIELD, CATEGORY_ID_OF_CATEGORY_FIELD]
42
+
43
+ @cached(ttl=30, key_builder=custom_key_builder)
44
+ async def get_categories(self, table_id: str) -> List[CategoryModel]:
45
+ records = await self.get_table_records(table_id, self.required_fields, self.projection)
46
+ return [parse_category_data(record) for record in records]
47
+
48
+ @cached(ttl=30, key_builder=custom_key_builder)
49
+ async def get_categories_v2(self, table_id: str, offset: int = None, limit: int = None) -> CategoryListResponseModel:
50
+ response = await self.get_table_records_v2(
51
+ table_name=self.categories_table,
52
+ required_fields=self.required_fields,
53
+ projection=self.projection,
54
+ offset=offset,
55
+ limit=limit,
56
+ )
57
+ page_info = get_pagination_info(page_info=response['pageInfo'])
58
+ categories = [parse_category_data(record) for record in response['list']]
59
+ return CategoryListResponseModel(categories=categories, page_info=page_info)
60
+
61
+ @cached(ttl=30, key_builder=custom_key_builder)
62
+ async def get_category(self, table_id: str, category_id: str) -> CategoryModel:
63
+ record = await self.get_table_record(self.categories_table, category_id, self.required_fields, self.projection)
64
+ return parse_category_data(record)
65
+
66
+ async def create_category(self, table_id: str, category: CategoryModel) -> CategoryModel:
67
+ category_json = dump_category_data(category)
68
+ record = await self.create_table_record(self.categories_table, category_json)
69
+ return parse_category_data(record)
70
+
71
+ @cached(ttl=30, key_builder=custom_key_builder)
72
+ async def get_categories_in_category(self, table_id: str, category_id: str) -> List[CategoryModel]:
73
+ extra_where = (
74
+ f"({CATEGORY_PARENT_ID_FIELD},eq,{category_id})"
75
+ if category_id
76
+ else f"({CATEGORY_PARENT_FIELD},eq,0)"
77
+ )
78
+ records = await self.get_table_records(
79
+ table_name=self.categories_table,
80
+ required_fields=self.required_fields,
81
+ projection=self.projection,
82
+ extra_where=extra_where,
83
+ )
84
+ return [parse_category_data(record) for record in records]
85
+
86
+ async def update_categories(self, external_products: List[ProductModel]) -> None:
87
+ self.categories = await self.get_product_categories(table_id=self.categories_table, table_name=PRODUCT_NAME_FIELD)
88
+ categories_list = list(self.categories.keys())
89
+ properties_to_create = []
90
+
91
+ for product in external_products:
92
+ for num, product_property in enumerate(product.product_properties):
93
+ if product_property not in categories_list:
94
+ parent_id = (
95
+ self.categories[self.filter_buttons[num]]
96
+ if self.filter_buttons
97
+ else self.categories[product.category_I_name[0]]
98
+ )
99
+ properties_to_create.append([product_property, parent_id])
100
+ categories_list.append(product_property)
101
+
102
+ if properties_to_create:
103
+ properties_to_create.sort(key=lambda x: x[0])
104
+ new_id = max(self.categories.values(), default=0) + 1
105
+
106
+ for product_property, parent_id in properties_to_create:
107
+ new_property = await self.create_product_category(
108
+ table_id=self.categories_table,
109
+ property_name=product_property,
110
+ category_id=new_id,
111
+ table_name=PRODUCT_NAME_FIELD,
112
+ )
113
+ if self.logging:
114
+ logger.info(f"New Category: {new_property}")
115
+ new_id += 1
116
+
117
+ async def link_categories(self, parent_id: int, child_id: int):
118
+ metadata = await self.get_table_meta(self.categories_table)
119
+ linked_column = next((col for col in metadata['columns'] if col["title"] == "Set parent category" and col["uidt"] == "Links"), None)
120
+
121
+ if linked_column:
122
+ await self.link_table_record(
123
+ linked_column["base_id"],
124
+ linked_column["fk_model_id"],
125
+ child_id,
126
+ linked_column["id"],
127
+ parent_id,
128
+ )
129
+
130
+ async def unlink_categories(self, parent_id: int, child_id: int):
131
+ metadata = await self.get_table_meta(self.categories_table)
132
+ linked_column = next((col for col in metadata['columns'] if col["uidt"] == "Links"), None)
133
+
134
+ if linked_column:
135
+ await self.unlink_table_record(
136
+ linked_column["base_id"],
137
+ linked_column["fk_model_id"],
138
+ parent_id,
139
+ linked_column["id"],
140
+ child_id,
141
+ )
142
+
143
+ async def map_categories(self, external_products: List[ProductModel]) -> List[ProductModel]:
144
+ for num, product in enumerate(external_products):
145
+ if not product.category and product.product_properties:
146
+ external_products[num].category = [
147
+ str(self.categories[property_name]) for property_name in product.product_properties
148
+ ]
149
+ return external_products