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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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