tgshops-integrations 0.5__py3-none-any.whl → 1.1__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,8 +1,11 @@
1
1
  from typing import List,Optional
2
+ import importlib.util
3
+ from pathlib import Path
2
4
 
3
5
  from aiocache import cached
4
6
  from tgshops_integrations.models.products import ProductModel
5
7
  from tgshops_integrations.nocodb_connector.client import NocodbClient
8
+
6
9
  from tgshops_integrations.nocodb_connector.model_mapping import dump_product_data,dump_product_data_with_check, get_pagination_info, ID_FIELD, \
7
10
  parse_product_data, PRODUCT_CATEGORY_ID_LOOKUP_FIELD, PRODUCT_NAME_FIELD, PRODUCT_PRICE_FIELD, \
8
11
  PRODUCT_STOCK_FIELD
@@ -10,59 +13,95 @@ from tgshops_integrations.nocodb_connector.model_mapping import dump_product_dat
10
13
  from tgshops_integrations.nocodb_connector.categories import CategoryManager
11
14
  from tgshops_integrations.nocodb_connector.products import ProductManager
12
15
  from tgshops_integrations.nocodb_connector.tables import *
13
- from loguru import logger
14
- import hashlib
16
+ # from .config import NOCODB_CATEGORIES,NOCODB_PRODUCTS,NOCODB_STATUSES,NOCODB_BOT_MESSAGES,NOCODB_ORDERS
15
17
 
18
+ from loguru import logger
16
19
 
17
20
  class Gateway(NocodbClient):
18
21
 
19
- def __init__(self,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None):
22
+ def __init__(self,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None,filter_buttons=[],config_path=None,special_attributes=False):
20
23
  super().__init__(NOCODB_HOST=NOCODB_HOST,NOCODB_API_KEY=NOCODB_API_KEY,SOURCE=SOURCE)
21
- self.NOCODB_HOST = NOCODB_HOST
22
- self.NOCODB_API_KEY = NOCODB_API_KEY
23
- self.logging=logging
24
- self.required_fields = [PRODUCT_NAME_FIELD, PRODUCT_PRICE_FIELD]
24
+ if config_path:
25
+ self.load_config_from_path(config_path)
26
+
27
+ self.logging = logging
28
+ self.required_fields = [self.config.PRODUCT_NAME_FIELD, self.config.PRODUCT_PRICE_FIELD]
25
29
  self.projection = []
30
+ self.special_attributes = special_attributes
31
+ self.filter_buttons = filter_buttons
26
32
 
33
+ def load_config_from_path(self,config_path):
34
+ if config_path.exists():
35
+ spec = importlib.util.spec_from_file_location("config", config_path)
36
+ self.config = importlib.util.module_from_spec(spec)
37
+ spec.loader.exec_module(self.config)
38
+ else:
39
+ raise FileNotFoundError(f"Configuration file not found at {config_path}")
27
40
 
28
41
  async def load_data(self,SOURCE=None):
29
42
  self.SOURCE=SOURCE
30
43
  await self.get_all_tables()
31
- self.category_manager=CategoryManager(table_id=self.tables_list[NOCODB_CATEGORIES],NOCODB_HOST=self.NOCODB_HOST,NOCODB_API_KEY=self.NOCODB_API_KEY)
32
- self.product_manager=ProductManager(table_id=self.tables_list[NOCODB_PRODUCTS],NOCODB_HOST=self.NOCODB_HOST,NOCODB_API_KEY=self.NOCODB_API_KEY)
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)
33
46
 
34
47
  async def create_product(self,product: ProductModel) -> ProductModel:
35
- products_table = self.tables_list[NOCODB_PRODUCTS]
48
+ products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
36
49
  data = dump_product_data_with_check(data=product ,data_check=self.category_manager.categories)
37
50
  # product_json = dump_product_data_with_check(data=product,data_check=self.categories)
38
- data.pop("ID")
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["ExternalImages"]=[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
+
39
61
  record = await self.create_table_record(table_name=products_table, record=data)
40
- logger.info(f"Created product {record['id']}")
41
- return parse_product_data(record)
62
+ # logger.info(f"Created product {record['id']}")
63
+ logger.info(f"Created product {external_id}")
42
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
+
43
77
  async def update_products(self, external_products: List[ProductModel]):
44
- products_table = self.tables_list[NOCODB_PRODUCTS]
45
- self.product_manager.actual_products=await self.product_manager.get_products_v2(offset=0,limit=200)
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)
84
+
46
85
  self.ids_mapping={product.external_id : product.id for product in self.product_manager.actual_products}
47
86
  products_meta= {product.external_id : product for product in self.product_manager.actual_products}
48
87
 
49
88
  for product in external_products:
50
89
  if product.external_id in self.ids_mapping.keys():
51
90
  product.id=self.ids_mapping[product.external_id]
52
- if self.product_manager.hash_product(product)!=self.product_manager.hash_product(products_meta[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):
53
92
  await self.update_product(product=product)
54
93
  else:
55
94
  await self.create_product(product=product)
56
95
 
57
96
  async def update_product(self, product: ProductModel):
58
- products_table = self.tables_list[NOCODB_PRODUCTS]
97
+ products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
59
98
  data = dump_product_data_with_check(data=product ,data_check=self.category_manager.categories)
60
99
 
61
100
  await self.update_table_record(
62
101
  table_name=products_table,
63
102
  record_id=product.id,
64
103
  updated_data=data)
65
- logger.info(f"Updated product {product.id}")
104
+ logger.info(f"Updated product {product.external_id}")
66
105
 
67
106
 
68
107
  def find_product_id_by_name(self,name: str):
@@ -73,13 +112,14 @@ class Gateway(NocodbClient):
73
112
 
74
113
  async def create_table_column(self, name: str, table_id: Optional[str] = None):
75
114
 
115
+ #TODO needs to be deleted
76
116
  BEARER_TOKEN = "jpdxJtyfDXdjbvxKAcIij1HA8HGalgalLLXZ46DV"
77
117
 
78
118
  headers = {
79
119
  "Authorization": f"Bearer {BEARER_TOKEN}"
80
120
  }
81
121
  if not table_id:
82
- table_id = self.tables_list[NOCODB_PRODUCTS]
122
+ table_id = self.tables_list[self.config.NOCODB_PRODUCTS]
83
123
 
84
124
  response = await self.httpx_client.post(
85
125
  f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_id}/columns",
@@ -99,13 +139,12 @@ class Gateway(NocodbClient):
99
139
  },
100
140
  headers=headers
101
141
  )
102
-
103
142
  logger.info(response.text())
104
143
 
105
144
  return response.json()
106
145
 
107
146
  async def delete_all_products(self):
108
- items = await self.product_manager.get_products_v2(offset=0,limit=100)
109
- products_table = self.tables_list[NOCODB_PRODUCTS]
147
+ items = await self.product_manager.get_products_v2(offset=0,limit=200)
148
+ products_table = self.tables_list[self.config.NOCODB_PRODUCTS]
110
149
  for num,item in enumerate(items):
111
150
  await self.delete_table_record(products_table, item.id)
@@ -6,20 +6,24 @@ from tgshops_integrations.models.categories import CategoryModel,CategoryRespons
6
6
  from tgshops_integrations.models.products import ProductModel
7
7
  from tgshops_integrations.nocodb_connector.client import custom_key_builder, NocodbClient
8
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, dump_category_data, get_pagination_info, parse_category_data
9
+ CATEGORY_PARENT_ID_FIELD, PRODUCT_NAME_FIELD,CATEGORY_ID_OF_CATEGORY_FIELD, dump_category_data, get_pagination_info, parse_category_data
10
10
 
11
11
 
12
12
  class CategoryManager(NocodbClient):
13
- def __init__(self,table_id=None,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None):
13
+ def __init__(self,table_id=None,logging=False,config_type=None,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None,filter_buttons=[]):
14
14
  super().__init__(NOCODB_HOST=NOCODB_HOST,NOCODB_API_KEY=NOCODB_API_KEY,SOURCE=SOURCE)
15
15
  self.NOCODB_HOST = NOCODB_HOST
16
16
  self.NOCODB_API_KEY = NOCODB_API_KEY
17
17
  self.SOURCE=SOURCE
18
+ self.CONFIG_TYPE=config_type
18
19
  self.categories_table=table_id
19
20
  self.external_categories={}
20
21
  self.logging=logging
22
+ self.filter_categories=[]
23
+ self.filter_buttons=filter_buttons
21
24
  self.required_fields = [CATEGORY_NAME_FIELD]
22
- self.projection = ["Id", CATEGORY_NAME_FIELD, CATEGORY_PARENT_ID_FIELD, CATEGORY_IMAGE_FIELD]
25
+ self.projection = ["Id", CATEGORY_NAME_FIELD, CATEGORY_PARENT_ID_FIELD, CATEGORY_ID_OF_CATEGORY_FIELD]
26
+ # self.projection = ["Id"]
23
27
 
24
28
  @cached(ttl=30, key_builder=custom_key_builder)
25
29
  async def get_categories(self, table_id: str) -> List[CategoryModel]:
@@ -81,22 +85,84 @@ class CategoryManager(NocodbClient):
81
85
  page_info = get_pagination_info(page_info=response['pageInfo'])
82
86
  return CategoryModel(categories=categories, page_info=page_info)
83
87
 
84
- async def update_categories(self,external_products: List[ProductModel]):
88
+ async def update_categories(self,external_products: List[ProductModel]) -> List[ProductModel]:
85
89
  # Get the names of the tables from the DB for further handling
86
- await self.get_product_categories(table_id=self.categories_table, table_name=PRODUCT_NAME_FIELD)
87
- await self.map_categories(external_products=external_products)
88
-
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
+
89
95
  for product in external_products:
90
- for external_category_id,category_name in zip(product.category,product.category_name):
91
- if category_name not in self.categories.keys():
92
- 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)
93
- if self.logging:
94
- logger.info(f"New Category {new_category}")
95
- self.external_categories[new_category["Id"]]=external_category_id
96
+ # Check for new categories
97
+ # 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)
96
162
 
97
163
  async def map_categories(self,external_products: List[ProductModel]) -> List[ProductModel]:
98
- for product in external_products:
164
+ for num,product in enumerate(external_products):
99
165
  if not product.category:
100
- for category in product.category_name:
101
- product.category.append(self.categories[category])
166
+ if product.category_name:
167
+ external_products[num].category=[str(self.categories[category_name]) for category_name in product.category_name]
102
168
  return external_products
@@ -2,6 +2,8 @@ from typing import List,Optional
2
2
 
3
3
  import httpx
4
4
  import requests
5
+ import io
6
+
5
7
  from loguru import logger
6
8
 
7
9
  from tgshops_integrations.nocodb_connector.model_mapping import ID_FIELD
@@ -20,7 +22,7 @@ class NocodbClient:
20
22
  self.NOCODB_HOST = NOCODB_HOST
21
23
  self.NOCODB_API_KEY = NOCODB_API_KEY
22
24
  self.SOURCE=SOURCE
23
- self.httpx_client = httpx.AsyncClient()
25
+ self.httpx_client = httpx.AsyncClient(timeout=60.0)
24
26
  self.httpx_client.headers = {
25
27
  "xc-token": self.NOCODB_API_KEY
26
28
  }
@@ -76,12 +78,6 @@ class NocodbClient:
76
78
  if response.status_code == 200:
77
79
  return response.json()
78
80
  raise Exception(response.text)
79
-
80
- # class ProductModel(BaseProductModel):
81
- # extra_option_choice_required: bool = False
82
- # extra_option_categories: List[ExtraOptionCategoriesResponseModel] = []
83
- # related_products: List[BaseProductModel] = None
84
- # metadata : ProductModel
85
81
 
86
82
  async def get_table_record(self,
87
83
  table_name: str,
@@ -112,9 +108,11 @@ class NocodbClient:
112
108
  return response.json().get("count", 0)
113
109
  raise Exception(response.text)
114
110
 
115
- async def update_table_record(self, table_name: str, record_id: str, updated_data: dict) -> bool:
111
+ async def update_table_record(self, table_name: str, record_id: str, updated_data: dict) -> bool:
116
112
  url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
117
- updated_data[ID_FIELD] = record_id
113
+ updated_data[ID_FIELD] = int(record_id)
114
+ if updated_data["ID"]:
115
+ updated_data.pop("ID")
118
116
  response = await self.httpx_client.patch(url, json=updated_data)
119
117
  if response.status_code == 200:
120
118
  return True
@@ -135,8 +133,12 @@ class NocodbClient:
135
133
  response = await self.httpx_client.get(url, params=extra_params)
136
134
 
137
135
  if response.status_code == 200:
138
- self.categories={category[table_name] : category["Id"] for category in response.json()["list"]}
139
- # raise Exception(response.text)
136
+ categories={category[table_name] : category["Id"] for category in response.json()["list"]}
137
+ return categories
138
+ raise Exception(response.text)
139
+ return {}
140
+
141
+
140
142
 
141
143
  async def create_product_category(self, table_id: str, category_name : str, table_name : str, category_id : int = 0) -> dict:
142
144
  url = f"{self.NOCODB_HOST}/tables/{table_id}/records"
@@ -145,10 +147,7 @@ class NocodbClient:
145
147
 
146
148
  response = await self.httpx_client.post(url, json=record)
147
149
  if response.status_code == 200:
148
- # record["id"] = response.json().get("Id")
149
- # if not record["id"]:
150
- # record["id"] = response.json().get("Id")
151
- await self.get_product_categories(table_id=table_id, table_name=table_name)
150
+ self.categories = await self.get_product_categories(table_id=table_id, table_name=table_name)
152
151
  return record
153
152
  raise Exception(response.text)
154
153
 
@@ -171,16 +170,107 @@ class NocodbClient:
171
170
  return (await self.httpx_client.get(
172
171
  f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/projects/")).json().get(
173
172
  'list', [])
173
+
174
+ async def get_table_meta(self, table_name: str):
175
+ return (await self.httpx_client.get(
176
+ f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}")).json()
177
+
178
+
179
+ async def create_table_column(self, table_name: str, name: str):
180
+ return (await self.httpx_client.post(
181
+ f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}/columns",
182
+ json={
183
+ "column_name": name,
184
+ "dt": "character varying",
185
+ "dtx": "specificType",
186
+ "ct": "varchar(45)",
187
+ "clen": 45,
188
+ "dtxp": "45",
189
+ "dtxs": "",
190
+ "altered": 1,
191
+ "uidt": "SingleLineText",
192
+ "uip": "",
193
+ "uicn": "",
194
+ "title": name
195
+ })).json()
174
196
 
175
- def link_tables(self, source: str, parent_id: str, child_id: str, parent_table: str, child_table: str):
197
+ async def link_table_record(
198
+ self,
199
+ base_id: str,
200
+ fk_model_id: str,
201
+ record_id: str,
202
+ source_column_id: str,
203
+ linked_record_id: str) -> dict:
176
204
  """
177
- Связывает таблицы
178
- :param source:
179
- :param parent_id:
180
- :param child_id:
181
- :param parent_table:
182
- :param child_table:
183
- :return:
205
+ base_id
206
+ fk_model_id - ID of linked column
207
+ record_id - ID of source record to be linked
208
+ source_column_id -ID of source column
209
+ linked_record_id - ID of linked record
210
+
211
+ POST /api/v1/db/data/noco/pwb8m0yee7nvw6m/mtk2pg9eiix11qs/242/mm/ct5sskewp6sg54q/91
212
+ /fk_model- smr8uvm11kurzprp
184
213
  """
185
- url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/data/noco/{source}/{parent_table}/{parent_id}/mm/{child_table}/{child_id}"
186
- return requests.post(url, headers=self.httpx_client.headers).json()
214
+ url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/data/noco/{base_id}/{fk_model_id}/{record_id}/mm/{source_column_id}/{linked_record_id}"
215
+ response = await self.httpx_client.post(url,headers=self.httpx_client.headers)
216
+ if response.status_code == 200:
217
+ return response.json()
218
+ raise Exception(response.text)
219
+
220
+ async def unlink_table_record(
221
+ self,
222
+ base_id: str,
223
+ fk_model_id: str,
224
+ record_id: str,
225
+ source_column_id: str,
226
+ linked_record_id: str) -> dict:
227
+ """
228
+ base_id
229
+ fk_model_id - ID of linked column
230
+ record_id - ID of source record to be linked
231
+ source_column_id -ID of source column
232
+ linked_record_id - ID of linked record
233
+
234
+ POST /api/v1/db/data/noco/pwb8m0yee7nvw6m/mtk2pg9eiix11qs/242/mm/ct5sskewp6sg54q/91
235
+ """
236
+ path = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/data/noco/{base_id}/{fk_model_id}/{record_id}/mm/{source_column_id}/{linked_record_id}"
237
+ response = await self.httpx_client.delete(path)
238
+ if response.status_code == 200:
239
+ return response.json()
240
+ raise Exception(response.text)
241
+
242
+ async def save_image_to_nocodb(
243
+ self,
244
+ image_url: str,
245
+ image_name: str,
246
+ source_column_id: str,
247
+ product_table_name: str,
248
+ images_column_id: str) -> dict:
249
+ """
250
+ source
251
+ fk_model_id - ID of linked column
252
+ record_id - ID of source record to be linked
253
+ source_column_id -ID of source column
254
+ linked_record_id - ID of linked record
255
+ """
256
+
257
+ response = requests.get(image_url)
258
+ if response.status_code == 200:
259
+ file = io.BytesIO(response.content)
260
+ else:
261
+ raise Exception(f"Failed to fetch the image. Status code: {response.status_code}")
262
+
263
+ file_size = file.getbuffer().nbytes
264
+
265
+ if file_size:
266
+
267
+ files = {'file': (image_name, file, 'image/jpeg')}
268
+
269
+ url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/storage/upload?path=noco/{source_column_id}/{product_table_name}/{images_column_id}"
270
+ timeout = httpx.Timeout(100.0)
271
+ response = await self.httpx_client.post(url,files=files,headers=self.httpx_client.headers,timeout=timeout)
272
+ if response.status_code == 200:
273
+ return response.json()[0]['url']
274
+ raise Exception(response.text)
275
+ else:
276
+ return ""
@@ -8,6 +8,62 @@ from tgshops_integrations.models.products import ExtraAttribute, ProductModel
8
8
  from tgshops_integrations.models.categories import CategoryResponseModel,PaginationResponseModel
9
9
  from tgshops_integrations.models.products import ProductModel
10
10
 
11
+ import importlib.util
12
+ from pathlib import Path
13
+
14
+
15
+ # Helper function to load config.py dynamically
16
+ def load_config(config_path):
17
+ config_path = Path(config_path)
18
+ if config_path.exists():
19
+ spec = importlib.util.spec_from_file_location("config", config_path)
20
+ config = importlib.util.module_from_spec(spec)
21
+ spec.loader.exec_module(config)
22
+ return config
23
+ else:
24
+ raise FileNotFoundError(f"Configuration file not found at {config_path}")
25
+
26
+ # Modify this to accept config_path dynamically
27
+ def initialize_model_mapping(config_path):
28
+ global CATEGORY_IMAGE_FIELD, ID_FIELD, CATEGORY_NAME_FIELD, CATEGORY_PARENT_ID_FIELD, CATEGORY_PARENT_FIELD,CATEGORY_ID_OF_CATEGORY_FIELD
29
+ global PRODUCT_NAME_FIELD, PRODUCT_DESCRIPTION_FIELD, PRODUCT_PRICE_FIELD, PRODUCT_CURRENCY_FIELD, PRODUCT_STOCK_FIELD
30
+ global PRODUCT_CATEGORY_NAME_FIELD, PRODUCT_CATEGORY_ID_FIELD, PRODUCT_IMAGE_FIELD, PRODUCT_DISCOUNT_PRICE_FIELD
31
+ global PRODUCT_CATEGORY_ID_LOOKUP_FIELD, PRODUCT_REQUIRED_OPTIONS_FIELD, PRODUCT_CATEGORIES_EXTRA_OPTIONS_FIELD
32
+ global PRODUCT_CATEGORIES_EXTRA_OPTION_NAMES_FIELD, PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD, PRODUCT_ID_FIELD
33
+ global PRODUCT_EXTERNAL_ID, PRODUCT_CHECKOUT_MODE, NEW_ID_FIELD, NOCODB_CHECKOUT_MODES
34
+
35
+ config = load_config(config_path)
36
+
37
+ # Step 3: Load all required constants from config.py
38
+ CATEGORY_IMAGE_FIELD = config.CATEGORY_IMAGE_FIELD
39
+ ID_FIELD = config.ID_FIELD
40
+ CATEGORY_NAME_FIELD = config.CATEGORY_NAME_FIELD
41
+ CATEGORY_PARENT_ID_FIELD = config.CATEGORY_PARENT_ID_FIELD
42
+ CATEGORY_PARENT_FIELD = config.CATEGORY_PARENT_FIELD
43
+ CATEGORY_ID_OF_CATEGORY_FIELD = config.CATEGORY_ID_OF_CATEGORY_FIELD
44
+
45
+ PRODUCT_NAME_FIELD = config.PRODUCT_NAME_FIELD
46
+ PRODUCT_DESCRIPTION_FIELD = config.PRODUCT_DESCRIPTION_FIELD
47
+ PRODUCT_PRICE_FIELD = config.PRODUCT_PRICE_FIELD
48
+ PRODUCT_CURRENCY_FIELD = config.PRODUCT_CURRENCY_FIELD
49
+ PRODUCT_STOCK_FIELD = config.PRODUCT_STOCK_FIELD
50
+ PRODUCT_CATEGORY_NAME_FIELD = config.PRODUCT_CATEGORY_NAME_FIELD
51
+
52
+ PRODUCT_CATEGORY_ID_FIELD = config.PRODUCT_CATEGORY_ID_FIELD
53
+ PRODUCT_IMAGE_FIELD = config.PRODUCT_IMAGE_FIELD
54
+ PRODUCT_DISCOUNT_PRICE_FIELD = config.PRODUCT_DISCOUNT_PRICE_FIELD
55
+ PRODUCT_CATEGORY_ID_LOOKUP_FIELD = config.PRODUCT_CATEGORY_ID_LOOKUP_FIELD
56
+ PRODUCT_REQUIRED_OPTIONS_FIELD = config.PRODUCT_REQUIRED_OPTIONS_FIELD
57
+ PRODUCT_CATEGORIES_EXTRA_OPTIONS_FIELD = config.PRODUCT_CATEGORIES_EXTRA_OPTIONS_FIELD
58
+ PRODUCT_CATEGORIES_EXTRA_OPTION_NAMES_FIELD = config.PRODUCT_CATEGORIES_EXTRA_OPTION_NAMES_FIELD
59
+ PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD = config.PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD
60
+ PRODUCT_ID_FIELD = config.PRODUCT_ID_FIELD
61
+ PRODUCT_EXTERNAL_ID = config.PRODUCT_EXTERNAL_ID
62
+ PRODUCT_CHECKOUT_MODE = config.PRODUCT_CHECKOUT_MODE
63
+ NEW_ID_FIELD = config.NEW_ID_FIELD
64
+
65
+ NOCODB_CHECKOUT_MODES = config.NOCODB_CHECKOUT_MODES
66
+
11
67
 
12
68
  def get_pagination_info(page_info: dict) -> PaginationResponseModel:
13
69
  page_info = PaginationResponseModel(total_rows=page_info['totalRows'],
@@ -17,15 +73,6 @@ def get_pagination_info(page_info: dict) -> PaginationResponseModel:
17
73
  is_last_page=page_info['isLastPage'])
18
74
  return page_info
19
75
 
20
-
21
- ID_FIELD = "Id"
22
- NEW_ID_FIELD = "id"
23
- CATEGORY_IMAGE_FIELD = "Изображение"
24
- CATEGORY_NAME_FIELD = "Название"
25
- CATEGORY_PARENT_FIELD = "Назначить родительскую категорию"
26
- CATEGORY_PARENT_ID_FIELD = "ID родительской категории"
27
-
28
-
29
76
  def parse_category_data(data: dict) -> CategoryResponseModel:
30
77
  preview_url = ""
31
78
  if data.get(CATEGORY_IMAGE_FIELD):
@@ -47,37 +94,8 @@ def dump_category_data(data: CategoryModel) -> dict:
47
94
  }
48
95
 
49
96
 
50
- # PRODUCT_IMAGE_FIELD = "Изображения"
51
- # PRODUCT_NAME_FIELD = "Название"
52
- # PRODUCT_STOCK_FIELD = "Доступное количество"
53
- # PRODUCT_PRICE_FIELD = "Стоимость"
54
- # PRODUCT_CURRENCY_FIELD = "Валюта"
55
- # PRODUCT_DESCRIPTION_FIELD = "Описание"
56
- # PRODUCT_CATEGORY_NAME_FIELD = "Название категорий"
57
- # PRODUCT_DISCOUNT_PRICE_FIELD = "Стоимость со скидкой"
58
-
59
- PRODUCT_IMAGE_FIELD="Images"
60
- PRODUCT_NAME_FIELD="Name"
61
- PRODUCT_DESCRIPTION_FIELD = "Description"
62
- PRODUCT_ID_FIELD="ID"
63
- PRODUCT_EXTERNAL_ID="ExternalId"
64
- PRODUCT_PRICE_FIELD="Price"
65
- PRODUCT_CURRENCY_FIELD = "Currency"
66
- PRODUCT_STOCK_FIELD = "Number of pieces"
67
- PRODUCT_CATEGORY_ID_FIELD = "Category"
68
- PRODUCT_DISCOUNT_PRICE_FIELD = "Discounted price"
69
- PRODUCT_CATEGORY_NAME_FIELD = "Name of categories"
70
- # PRODUCT_CATEGORY_ID_LOOKUP_FIELD = "ID Категории"
71
- PRODUCT_CATEGORY_ID_LOOKUP_FIELD = "ID of category"
72
- PRODUCT_REQUIRED_OPTIONS_FIELD = "Выбор обязательных опций"
73
- PRODUCT_CATEGORIES_EXTRA_OPTIONS_FIELD = "Выбор категории доп опций"
74
- PRODUCT_CATEGORIES_EXTRA_OPTION_NAMES_FIELD = "Названия категорий доп опций"
75
- PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD = "Обязательный выбор?"
76
-
77
-
78
97
  def dump_product_data(data: ProductModel) -> dict:
79
- if data.external_id=="21":
80
- print("Hoi")
98
+
81
99
  preview_url = ([{'url': image_url,
82
100
  'title': f'{secrets.token_hex(6)}.jpeg',
83
101
  'mimetype': 'image/jpeg'}
@@ -91,9 +109,7 @@ def dump_product_data(data: ProductModel) -> dict:
91
109
  PRODUCT_PRICE_FIELD: data.price,
92
110
  PRODUCT_CURRENCY_FIELD: data.currency,
93
111
  PRODUCT_STOCK_FIELD: data.stock_qty,
94
- #TODO Add for several categories
95
112
  PRODUCT_CATEGORY_NAME_FIELD:[data.category_name] if data.category_name else None,
96
- # PRODUCT_CATEGORY_ID_FIELD: [{"id": int(data.category[0])}] if data.category else None,
97
113
  PRODUCT_CATEGORY_ID_FIELD: [{'Id': data.category}] if data.category else None,
98
114
  PRODUCT_IMAGE_FIELD: preview_url,
99
115
  PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price
@@ -107,6 +123,9 @@ def dump_product_data_with_check(data: ProductModel, data_check: dict) -> dict:
107
123
  for image_url in data.preview_url]
108
124
  if data.preview_url
109
125
  else [])
126
+
127
+ extra_data = {item.name : item.description for item in data.extra_attributes}
128
+
110
129
  product_data = {
111
130
  PRODUCT_ID_FIELD: data.id,
112
131
  PRODUCT_EXTERNAL_ID: data.external_id,
@@ -115,13 +134,15 @@ def dump_product_data_with_check(data: ProductModel, data_check: dict) -> dict:
115
134
  PRODUCT_PRICE_FIELD: data.price,
116
135
  PRODUCT_CURRENCY_FIELD: data.currency,
117
136
  PRODUCT_STOCK_FIELD: data.stock_qty,
118
- #TODO Add for several categories
119
137
  PRODUCT_CATEGORY_NAME_FIELD:[data.category_name] if data.category_name else None,
120
- #TODO Add for several categories
121
- PRODUCT_CATEGORY_ID_FIELD: [{'Id': data_check[data.category_name[0]]}] if data.category else None,
138
+ PRODUCT_CATEGORY_ID_FIELD: [{'Id': data_check[item]} for item in data.category_name] if data.category else None,
122
139
  PRODUCT_IMAGE_FIELD: preview_url,
123
- PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price
140
+ PRODUCT_CHECKOUT_MODE: NOCODB_CHECKOUT_MODES,
141
+ PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price,
124
142
  }
143
+
144
+ if len(extra_data)>0:
145
+ product_data.update(extra_data)
125
146
  return product_data
126
147
 
127
148
 
@@ -26,10 +26,16 @@ class ProductManager(NocodbClient):
26
26
  self.external_categories={}
27
27
  self.products_table=table_id
28
28
  self.actual_products=[]
29
+ self.columns=[]
29
30
 
30
- def hash_product(self,product):
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:
31
36
  # Concatenate relevant attributes into a single string
32
- hash_string = f"{product.external_id}{product.price}{product.category}{product.name}{product.description}{product.preview_url}"
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}"
33
39
  # Hash the concatenated string
34
40
  hash_object = hashlib.sha256(hash_string.encode())
35
41
  hex_dig = hash_object.hexdigest()
@@ -144,7 +150,18 @@ class ProductManager(NocodbClient):
144
150
  required_fields=self.required_fields,
145
151
  projection=self.projection,
146
152
  extra_where=f"({ID_FIELD},in,{product_ids_str})")
147
- return [parse_product_data(record) for record in records]
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
+ for item in products:
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
+ for attribute in item.extra_attributes:
161
+ if attribute.name.rstrip().lower() not in self.columns:
162
+ response =await self.create_table_column(table_name=self.products_table,name=attribute.name.lower())
163
+ logger.info(f"Created attribute: {attribute.name.lower()}")
164
+
148
165
 
149
166
  def find_product_id_by_name(self,name: str):
150
167
  for product in self.actual_products.products:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tgshops-integrations
3
- Version: 0.5
3
+ Version: 1.1
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
@@ -0,0 +1,16 @@
1
+ tgshops_integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ tgshops_integrations/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ tgshops_integrations/middlewares/gateway.py,sha256=ib7s39K9_VefobHQwtv7ajR3mTDjDziAZd00GBSZ1wA,7391
4
+ tgshops_integrations/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ tgshops_integrations/models/categories.py,sha256=EG6C8g5dOfXB2MH-vtqH13aqB7_VyOobY2FHpDb-fsY,977
6
+ tgshops_integrations/models/products.py,sha256=i0vP_eJMVCB-W25BCoodIB0AhsMTqYiDO48N-B6Ueo0,1379
7
+ tgshops_integrations/nocodb_connector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ tgshops_integrations/nocodb_connector/categories.py,sha256=YfQQ8UAkOonNZKO-oTH36vODu0eE-ElG-Me2bH5LdEM,9072
9
+ tgshops_integrations/nocodb_connector/client.py,sha256=MD08P25jYWg0ZoLGk9cZ7cH8WJ6vtMn9oXW1N0LuyZ0,11504
10
+ tgshops_integrations/nocodb_connector/model_mapping.py,sha256=CeGAwEZLK_NlK_eBTtsH6oRsjo6Z-QxWdC9KnwEhtPw,8770
11
+ tgshops_integrations/nocodb_connector/products.py,sha256=23uXnmznJN6fZw3tZ3a7dJg06LkI2QaNfVhSKochPn4,8677
12
+ tgshops_integrations/nocodb_connector/tables.py,sha256=ha_QXZXd93mht0fR5E1nM0wUpz1ePon-pIdO2HI67l8,356
13
+ tgshops_integrations-1.1.dist-info/METADATA,sha256=uU8JvqZdrwZETXch9M3YmqvDD_ByhPieCHMTdB1LEpU,2774
14
+ tgshops_integrations-1.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
15
+ tgshops_integrations-1.1.dist-info/top_level.txt,sha256=HFNtxqDpzmlF4ZLnMiwhbU7pOa_YozxU2zBl0bnUmcY,21
16
+ tgshops_integrations-1.1.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- tgshops_integrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tgshops_integrations/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- tgshops_integrations/middlewares/gateway.py,sha256=tZMJZJBRVmmIrlKmspm5KMbmNDCv4RgXFjydcayx56k,5042
4
- tgshops_integrations/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- tgshops_integrations/models/categories.py,sha256=EG6C8g5dOfXB2MH-vtqH13aqB7_VyOobY2FHpDb-fsY,977
6
- tgshops_integrations/models/products.py,sha256=i0vP_eJMVCB-W25BCoodIB0AhsMTqYiDO48N-B6Ueo0,1379
7
- tgshops_integrations/nocodb_connector/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- tgshops_integrations/nocodb_connector/categories.py,sha256=GHB7vEns9viK3NbgR4fY42UzMCUVnr4R1HKP1NORaHg,5900
9
- tgshops_integrations/nocodb_connector/client.py,sha256=qq2UQDbRCOqcH0ng49l1xOihY2-Zc3tMm2yqQUMk_pk,8307
10
- tgshops_integrations/nocodb_connector/model_mapping.py,sha256=9199eOz4ISqduDv5BkxWgPXt-QT6fd5dtCGVMZoOAN4,7471
11
- tgshops_integrations/nocodb_connector/products.py,sha256=TZGBfEfH11NxOeeqZQoEiMi1WEuSMpmTJ7JDu8KtgNw,7548
12
- tgshops_integrations/nocodb_connector/tables.py,sha256=ha_QXZXd93mht0fR5E1nM0wUpz1ePon-pIdO2HI67l8,356
13
- tgshops_integrations-0.5.dist-info/METADATA,sha256=69nGOSfX2e1gdThBxwhPsqe-nvWPbgIZ6Lkv2h-K9EY,2774
14
- tgshops_integrations-0.5.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
15
- tgshops_integrations-0.5.dist-info/top_level.txt,sha256=HFNtxqDpzmlF4ZLnMiwhbU7pOa_YozxU2zBl0bnUmcY,21
16
- tgshops_integrations-0.5.dist-info/RECORD,,