tgshops-integrations 3.0__py3-none-any.whl → 3.2__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.
@@ -23,8 +23,8 @@ from tgshops_integrations.nocodb_connector.model_mapping import (
23
23
  PRODUCT_IMAGES_LOOKUP_FIELD,
24
24
  PRODUCT_EXTERNAL_ID,
25
25
  )
26
- from services.nocodb_connector.categories import CategoryManager
27
- from services.nocodb_connector.products import ProductManager
26
+ from tgshops_integrations.nocodb_connector.categories import CategoryManager
27
+ from tgshops_integrations.nocodb_connector.products import ProductManager
28
28
  from loguru import logger
29
29
 
30
30
 
@@ -1,5 +1,5 @@
1
1
  from pydantic import BaseModel
2
- from typing import Any, List, Optional
2
+ from typing import Any, List,Dict, Optional
3
3
  from datetime import datetime
4
4
 
5
5
  from pydantic import BaseModel, Field, schema, validator
@@ -28,12 +28,12 @@ class ExternalProductModel(BaseModel):
28
28
  class ProductModel(BaseModel):
29
29
  id: Optional[str]
30
30
  external_id: Optional[str]
31
- category: Optional[List[int]]
32
- category_name: Optional[List[str]]
31
+ product_properties: Optional[List[str]]
32
+ categories_structure: Optional[Dict[str, Any]]
33
33
  name: str
34
34
  description: Optional[str]
35
35
  price: Optional[float]
36
- final_price: Optional[int]
36
+ final_price: Optional[float]
37
37
  currency: Optional[str]
38
38
  stock_qty: int
39
39
  orders_qty: int = Field(0, hidden_field=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tgshops-integrations
3
- Version: 3.0
3
+ Version: 3.2
4
4
  Summary: Library is intended to provide the integration of the external service or CRM system with the TelegramShops/It allows to configure the relationship between NocoDB list of the products used further to display in the shop/As a resultss the products can be synchronized and updated uppon the request.
5
5
  Home-page: https://git.the-devs.com/virtual-shops/shop-system/shop-backend-integrations/integration-library/integration-library
6
6
  Author: Dimi Latoff
@@ -1,18 +1,16 @@
1
1
  tgshops_integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  tgshops_integrations/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- tgshops_integrations/middlewares/gateway.py,sha256=Gzr8gZWQ3B3tiqpE_iSqtAkINDbsm7QT_v_bjT1b8V4,6223
3
+ tgshops_integrations/middlewares/gateway.py,sha256=-YQi3-62k6gkxJLx27FESe6wlRrD4_Ltqk8wZJ8ExYg,6247
4
4
  tgshops_integrations/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  tgshops_integrations/models/categories.py,sha256=EG6C8g5dOfXB2MH-vtqH13aqB7_VyOobY2FHpDb-fsY,977
6
- tgshops_integrations/models/products.py,sha256=zkRqXLU-tAReDF5R9j3q58le686dw3PapxcFOGUpk7Q,1376
6
+ tgshops_integrations/models/products.py,sha256=CpQ5LpeigklgVnkKPCm5jR9hfJsoFbWKSsDiTI32RSo,1405
7
7
  tgshops_integrations/nocodb_connector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- tgshops_integrations/nocodb_connector/categories.py,sha256=eoAWRA0aSE84_ONrwflM1ZFXLSCFjYl_CFqUf7lA_EU,9063
9
8
  tgshops_integrations/nocodb_connector/categories_management.py,sha256=dvPXcrgruJyuWFT7Ho-8YRcjJxhR3Hcw0hJiVKQgcyA,6603
10
9
  tgshops_integrations/nocodb_connector/client.py,sha256=U2rNozjluWVul-VZroQW-8b2j6nEtKrhOkFMz1L8KmI,12022
11
10
  tgshops_integrations/nocodb_connector/model_mapping.py,sha256=ixv-uT1Pmt1szZCQ5pGsMbwRvmnHZmn0NqG77nxZkDM,8551
12
- tgshops_integrations/nocodb_connector/products.py,sha256=kAp7lUaRO7CkU_SumbIdLOJf38SmDBEyBBZCYyyyOFM,9313
13
11
  tgshops_integrations/nocodb_connector/products_management.py,sha256=t_ZDyIAPTe_6UrxLuJ32Uhlk8ssErP6Eo0n-TIZfEko,6046
14
12
  tgshops_integrations/nocodb_connector/tables.py,sha256=ha_QXZXd93mht0fR5E1nM0wUpz1ePon-pIdO2HI67l8,356
15
- tgshops_integrations-3.0.dist-info/METADATA,sha256=E4e2SV-nFT2HLUEzRZ90PWI1BTAzoaTtnXm-ZYalN5k,2774
16
- tgshops_integrations-3.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
17
- tgshops_integrations-3.0.dist-info/top_level.txt,sha256=HFNtxqDpzmlF4ZLnMiwhbU7pOa_YozxU2zBl0bnUmcY,21
18
- tgshops_integrations-3.0.dist-info/RECORD,,
13
+ tgshops_integrations-3.2.dist-info/METADATA,sha256=daiTMAx10_7qtGpB4ILTHYZCEnU9eFtuYENzwki-VTc,2774
14
+ tgshops_integrations-3.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
15
+ tgshops_integrations-3.2.dist-info/top_level.txt,sha256=HFNtxqDpzmlF4ZLnMiwhbU7pOa_YozxU2zBl0bnUmcY,21
16
+ tgshops_integrations-3.2.dist-info/RECORD,,
@@ -1,168 +0,0 @@
1
- from typing import List
2
- from loguru import logger
3
-
4
- from aiocache import cached
5
- from tgshops_integrations.models.categories import CategoryModel,CategoryResponseModel,CategoryListResponseModel
6
- from tgshops_integrations.models.products import ProductModel
7
- from tgshops_integrations.nocodb_connector.client import custom_key_builder, NocodbClient
8
- from tgshops_integrations.nocodb_connector.model_mapping import CATEGORY_IMAGE_FIELD, CATEGORY_NAME_FIELD, CATEGORY_PARENT_FIELD, \
9
- CATEGORY_PARENT_ID_FIELD, PRODUCT_NAME_FIELD,CATEGORY_ID_OF_CATEGORY_FIELD, dump_category_data, get_pagination_info, parse_category_data
10
-
11
-
12
- class CategoryManager(NocodbClient):
13
- def __init__(self,table_id=None,logging=False,config_type=None,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None,filter_buttons=[]):
14
- super().__init__(NOCODB_HOST=NOCODB_HOST,NOCODB_API_KEY=NOCODB_API_KEY,SOURCE=SOURCE)
15
- self.NOCODB_HOST = NOCODB_HOST
16
- self.NOCODB_API_KEY = NOCODB_API_KEY
17
- self.SOURCE=SOURCE
18
- self.CONFIG_TYPE=config_type
19
- self.categories_table=table_id
20
- self.external_categories={}
21
- self.logging=logging
22
- self.filter_categories=[]
23
- self.filter_buttons=filter_buttons
24
- self.required_fields = [CATEGORY_NAME_FIELD]
25
- self.projection = ["Id", CATEGORY_NAME_FIELD, CATEGORY_PARENT_ID_FIELD, CATEGORY_ID_OF_CATEGORY_FIELD]
26
- # self.projection = ["Id"]
27
-
28
- @cached(ttl=30, key_builder=custom_key_builder)
29
- async def get_categories(self, table_id: str) -> List[CategoryModel]:
30
- records = await self.get_table_records(table_id, self.required_fields, self.projection)
31
- return [parse_category_data(record) for record in records]
32
-
33
- @cached(ttl=30, key_builder=custom_key_builder)
34
- async def get_categories_v2(self,
35
- table_id: str,
36
- offset: int = None,
37
- limit: int = None) -> CategoryModel:
38
- response = (await self.get_table_records_v2(table_name=self.categories_table,
39
- required_fields=self.required_fields,
40
- projection=self.projection,
41
- offset=offset,
42
- limit=limit))
43
- page_info = get_pagination_info(page_info=response['pageInfo'])
44
- categories = [parse_category_data(record) for record in response['list']]
45
- return CategoryListResponseModel(categories=categories, page_info=page_info)
46
-
47
- @cached(ttl=30, key_builder=custom_key_builder)
48
- async def get_category(self, table_id: str, category_id: str) -> CategoryModel:
49
- record = await self.get_table_record(self.categories_table, category_id, self.required_fields, self.projection)
50
- return parse_category_data(record)
51
-
52
- async def create_category(self, table_id: str, category: CategoryModel) -> CategoryModel:
53
- category_json = dump_category_data(category)
54
- record = await self.create_table_record(self.categories_table, category_json)
55
- return parse_category_data(record)
56
-
57
- @cached(ttl=30, key_builder=custom_key_builder)
58
- async def get_categories_in_category(self, table_id: str, category_id: str) -> List[CategoryModel]:
59
- # ! In case category_id == 0,
60
- # we need to get all categories without parent by field CATEGORY_PARENT_FIELD not CATEGORY_PARENT_ID_FIELD
61
- records = await self.get_table_records(
62
- table_name=self.categories_table,
63
- required_fields=self.required_fields,
64
- projection=self.projection,
65
- extra_where=(f"({CATEGORY_PARENT_ID_FIELD},eq,{category_id})"
66
- if category_id else f"({CATEGORY_PARENT_FIELD},eq,0)"))
67
- return [parse_category_data(record) for record in records]
68
-
69
- @cached(ttl=30, key_builder=custom_key_builder)
70
- async def get_categories_in_category_v2(self,
71
- table_id: str,
72
- category_id: str,
73
- offset: int,
74
- limit: int) -> CategoryModel:
75
-
76
- response = await self.get_table_records_v2(
77
- table_name=self.categories_table,
78
- required_fields=self.required_fields,
79
- projection=self.projection,
80
- extra_where=(f"({CATEGORY_PARENT_ID_FIELD},eq,{category_id})"
81
- if category_id else f"({CATEGORY_PARENT_FIELD},eq,0)"),
82
- offset=offset,
83
- limit=limit)
84
- categories = [parse_category_data(record) for record in response['list']]
85
- page_info = get_pagination_info(page_info=response['pageInfo'])
86
- return CategoryModel(categories=categories, page_info=page_info)
87
-
88
- async def update_categories(self,external_products: List[ProductModel]) -> List[ProductModel]:
89
- # Get the names of the tables from the DB for further handling
90
- self.categories=await self.get_product_categories(table_id=self.categories_table, table_name=PRODUCT_NAME_FIELD)
91
-
92
- categories_list=[*self.categories.keys()]
93
- categories_to_create=[]
94
-
95
- for product in external_products:
96
- # Check for new categories
97
- # DEPRECATED IF some category number exists on external side
98
- # if product.category:
99
- # for external_category_id,category_name in zip(product.category,product.category_name):
100
- # if category_name not in [*self.categories.keys()]:
101
- # new_category= await self.create_product_category(table_id=self.categories_table,category_name=category_name,category_id=external_category_id,table_name=PRODUCT_NAME_FIELD)
102
- # if self.logging:
103
- # logger.info(f"New Category {new_category}")
104
- # self.external_categories[new_category["Id"]]=external_category_id
105
- # self.categories=await self.get_product_categories(table_id=self.categories_table, table_name=PRODUCT_NAME_FIELD)
106
- #Else if there is just the name, create the category with new id
107
- # else:
108
- #Needs the buttons to be initialized, can connect items to buttons , which allows filtered acces through the menu
109
- for num,category_name in enumerate(product.category_name):
110
- if category_name not in categories_list:
111
- if self.filter_buttons:
112
- parent_id=self.categories[self.filter_buttons[num]]
113
- else:
114
- parent_id=self.categories[product.category_II_name[0]]
115
- categories_to_create.append([category_name,parent_id])
116
- categories_list.append(category_name)
117
-
118
- if categories_to_create:
119
- categories_to_create.sort(key=lambda x: x[0])
120
- if [*self.categories.values()]:
121
- new_id=max(self.categories.values())+1
122
- else:
123
- new_id=1
124
- for category,parent_id in categories_to_create:
125
- new_category= await self.create_product_category(table_id=self.categories_table,category_name=category,category_id=new_id,table_name=PRODUCT_NAME_FIELD)
126
- if self.logging:
127
- logger.info(f"New Category {new_category}")
128
- if self.filter_buttons:
129
- #Rewind categories
130
- await self.link_categories(parent_id=parent_id,child_id=new_id)
131
- new_id+=1
132
-
133
- async def link_categories(self,parent_id:int,child_id:int):
134
- metadata = await self.get_table_meta(self.categories_table)
135
-
136
- linked_column = None
137
- for col in metadata['columns']:
138
- if (col["title"]=="Set parent category" and col["uidt"] == "Links"):
139
- linked_column = col
140
- break
141
- await self.link_table_record(
142
- linked_column["base_id"],
143
- linked_column["fk_model_id"],
144
- child_id,
145
- linked_column["id"],
146
- parent_id)
147
-
148
- async def unlink_categories(self,parent_id:int,child_id:int):
149
- metadata = await self.get_table_meta(self.categories_table)
150
-
151
- linked_column = None
152
- for col in metadata['columns']:
153
- if col["uidt"] == "Links":
154
- linked_column = col
155
- break
156
- await self.unlink_table_record(
157
- linked_column["base_id"],
158
- linked_column["fk_model_id"],
159
- parent_id,
160
- linked_column["id"],
161
- child_id)
162
-
163
- async def map_categories(self,external_products: List[ProductModel]) -> List[ProductModel]:
164
- for num,product in enumerate(external_products):
165
- if not product.category:
166
- if product.category_name:
167
- external_products[num].category=[str(self.categories[category_name]) for category_name in product.category_name]
168
- return external_products
@@ -1,181 +0,0 @@
1
- from typing import List,Optional
2
-
3
- from aiocache import cached
4
- from tgshops_integrations.models.products import ProductModel
5
- from tgshops_integrations.models.products import ProductModel, ProductModel
6
- from tgshops_integrations.nocodb_connector.client import custom_key_builder, NocodbClient
7
- from tgshops_integrations.nocodb_connector.model_mapping import dump_product_data,dump_product_data_with_check, get_pagination_info, ID_FIELD, \
8
- parse_product_data, PRODUCT_CATEGORY_ID_LOOKUP_FIELD, PRODUCT_NAME_FIELD, PRODUCT_PRICE_FIELD, \
9
- PRODUCT_STOCK_FIELD,PRODUCT_EXTERNAL_ID,PRODUCT_IMAGES_LOOKUP_FIELD
10
-
11
- from tgshops_integrations.nocodb_connector.tables import *
12
- from loguru import logger
13
- import hashlib
14
-
15
-
16
- class ProductManager(NocodbClient):
17
-
18
- def __init__(self, table_id=None, logging=False, NOCODB_HOST=None, NOCODB_API_KEY=None, SOURCE=None):
19
- super().__init__(NOCODB_HOST=NOCODB_HOST, NOCODB_API_KEY=NOCODB_API_KEY, SOURCE=SOURCE)
20
- self.NOCODB_HOST = NOCODB_HOST
21
- self.NOCODB_API_KEY = NOCODB_API_KEY
22
- self.SOURCE = SOURCE
23
- self.logging = logging
24
- self.required_fields = [PRODUCT_NAME_FIELD, PRODUCT_PRICE_FIELD]
25
- self.projection = []
26
- self.external_categories = {}
27
- self.products_table = table_id
28
- self.actual_products = []
29
- self.columns = []
30
-
31
- def hash_product(self,product,special_attributes=False):
32
- if special_attributes:
33
- hash_string = ''.join(attr.description for attr in product.extra_attributes if attr.name.endswith('*'))
34
- # hash_string = f"{product.external_id}{product.price}{product.category_name.sort()}{product.name}{product.description}"
35
- else:
36
- # Concatenate relevant attributes into a single string
37
- hash_string = f"{product.external_id}{product.price}{product.category_name.sort()}{product.name}{product.description}"
38
- # hash_string = f"{product.external_id}{product.price}{product.category_name}{product.name}{product.description}{product.preview_url}"
39
- # Hash the concatenated string
40
- hash_object = hashlib.sha256(hash_string.encode())
41
- hex_dig = hash_object.hexdigest()
42
- return hex_dig
43
-
44
- @cached(ttl=30, key_builder=custom_key_builder)
45
- async def get_products(self, table_id: str) -> List[ProductModel]:
46
- records = await self.get_table_records(self.products_table, self.required_fields, self.projection)
47
- return [parse_product_data(record) for record in records]
48
-
49
- # @cached(ttl=30, key_builder=custom_key_builder)
50
- async def get_products_v2(self, offset: int, limit: int, table_id: Optional[str] = None) -> List[ProductModel]:
51
- # Get the names of the tables from the DB for further handling
52
- await self.get_all_tables()
53
- response = await self.get_table_records_v2(table_name=self.products_table,
54
- required_fields=self.required_fields,
55
- projection=self.projection,
56
- offset=offset,
57
- limit=limit)
58
- products = [await parse_product_data(record) for record in response['list']]
59
-
60
- return products
61
-
62
- @cached(ttl=180, key_builder=custom_key_builder)
63
- async def search_products(self, table_id: str, search_string: str, limit: int) -> List[ProductModel]:
64
- records = await self.get_table_records(
65
- table_name=self.products_table,
66
- required_fields=self.required_fields,
67
- projection=self.projection,
68
- extra_where=f"({PRODUCT_NAME_FIELD},like,%{search_string}%)", # Update with actual product name field
69
- limit=limit
70
- )
71
- return [parse_product_data(record) for record in records]
72
-
73
- @cached(ttl=180, key_builder=custom_key_builder)
74
- async def search_products_v2(self, table_id: str, search_string: str, limit: int) -> List[ProductModel]:
75
- records = (await self.get_table_records_v2(
76
- table_name=self.products_table,
77
- required_fields=self.required_fields,
78
- projection=self.projection,
79
- extra_where=f"({PRODUCT_NAME_FIELD},like,%{search_string}%)", # Update with actual product name field
80
- limit=limit
81
- ))['list']
82
- return [parse_product_data(record) for record in records]
83
-
84
- @cached(ttl=60, key_builder=custom_key_builder)
85
- async def get_product(self, table_id: str, product_id: str) -> ProductModel:
86
- record = await self.get_table_record(self.products_table, product_id)
87
- return parse_product_data(record)
88
-
89
- @cached(ttl=60, key_builder=custom_key_builder)
90
- async def get_product_v2(self, table_id: str, product_id: str) -> ProductModel:
91
- record = await self.get_table_record(self.products_table, product_id)
92
- product = parse_product_data(record)
93
-
94
- related_products = await self.get_table_records_v2(
95
- table_name=self.products_table,
96
- required_fields=self.required_fields,
97
- projection=self.projection,
98
- extra_where=(f'({PRODUCT_STOCK_FIELD},gt,0)~and'
99
- f"({PRODUCT_CATEGORY_ID_LOOKUP_FIELD},eq,{product.category[0]})~and"
100
- f'({PRODUCT_NAME_FIELD},neq,{product.name})'),
101
- limit=5
102
- )
103
- related_products = [parse_product_data(product) for product in related_products['list']]
104
-
105
- product.related_products = related_products
106
- return product
107
-
108
- @cached(ttl=60, key_builder=custom_key_builder)
109
- async def get_product_in_category(self, table_id: str, category_id: str = None) -> List[ProductModel]:
110
- if category_id is None:
111
- return await self.get_products(table_id=self.products_table)
112
-
113
- records = await self.get_table_records(
114
- table_name=self.products_table,
115
- required_fields=self.required_fields,
116
- projection=self.projection,
117
- extra_where=(f'({PRODUCT_STOCK_FIELD},gt,0)~and'
118
- f"({PRODUCT_CATEGORY_ID_LOOKUP_FIELD},eq,{category_id})")
119
- )
120
- return [parse_product_data(record) for record in records]
121
-
122
- @cached(ttl=60, key_builder=custom_key_builder)
123
- async def get_product_in_category_v2(self,
124
- table_id: str,
125
- offset: int,
126
- limit: int,
127
- category_id: str = None) -> ProductModel:
128
- if category_id is None:
129
- return await self.get_products_v2(table_id=self.products_table, offset=offset, limit=limit)
130
-
131
- response = (await self.get_table_records_v2(
132
- table_name=self.products_table,
133
- required_fields=self.required_fields,
134
- projection=self.projection,
135
- extra_where=(f'({PRODUCT_STOCK_FIELD},gt,0)~and'
136
- f"({PRODUCT_CATEGORY_ID_LOOKUP_FIELD},eq,{category_id})"),
137
- offset=offset,
138
- limit=limit
139
- ))
140
- page_info = get_pagination_info(page_info=response['pageInfo'])
141
- products = [parse_product_data(record) for record in response['list']]
142
- return ProductModel(products=products, page_info=page_info)
143
-
144
- @cached(ttl=60, key_builder=custom_key_builder)
145
- async def get_products_by_ids(self, table_id: str, product_ids: list) -> List[ProductModel]:
146
- product_ids_str = ','.join(str(product_id) for product_id in product_ids)
147
-
148
- records = await self.get_table_records(
149
- table_name=self.products_table,
150
- required_fields=self.required_fields,
151
- projection=self.projection,
152
- extra_where=f"({ID_FIELD},in,{product_ids_str})")
153
- return [parse_product_data(record) for record in records]
154
-
155
- @cached(ttl=60, key_builder=custom_key_builder)
156
- async def update_attributes(self,products:List[ProductModel]):
157
- system_attributes = [PRODUCT_EXTERNAL_ID,PRODUCT_IMAGES_LOOKUP_FIELD]
158
- attributes=await self.get_table_meta(table_name=self.products_table)
159
- self.columns =[item['title'].lower() for item in attributes.get('columns', [])]
160
-
161
- #TODO Requires Validation
162
- for attribute_name in system_attributes:
163
- if attribute_name.lower() not in self.columns:
164
- response =await self.create_table_column(table_name=self.products_table,name=attribute_name)
165
- logger.info(f"Created attribute: {attribute_name}")
166
-
167
- for item in products:
168
- attributes=await self.get_table_meta(table_name=self.products_table)
169
- self.columns =[item['title'].lower() for item in attributes.get('columns', [])]
170
-
171
- for attribute in item.extra_attributes:
172
- if attribute.name.rstrip().lower() not in self.columns:
173
- response =await self.create_table_column(table_name=self.products_table,name=attribute.name.lower())
174
- logger.info(f"Created attribute: {attribute.name.lower()}")
175
-
176
-
177
- def find_product_id_by_name(self,name: str):
178
- for product in self.actual_products.products:
179
- if product.name == name:
180
- return product.id
181
- return None # Return None if no product is found with the given name