tgshops-integrations 0.3__py3-none-any.whl → 1.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,4 +1,6 @@
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
@@ -10,46 +12,88 @@ from tgshops_integrations.nocodb_connector.model_mapping import dump_product_dat
10
12
  from tgshops_integrations.nocodb_connector.categories import CategoryManager
11
13
  from tgshops_integrations.nocodb_connector.products import ProductManager
12
14
  from tgshops_integrations.nocodb_connector.tables import *
15
+ # from .config import NOCODB_CATEGORIES,NOCODB_PRODUCTS,NOCODB_STATUSES,NOCODB_BOT_MESSAGES,NOCODB_ORDERS
16
+
13
17
  from loguru import logger
14
- import hashlib
15
18
 
19
+ # Step 1: Define the path to config.py (one level above)
20
+ config_path = Path(__file__).resolve().parent.parent / '../config.py'
21
+
22
+ # Step 2: Load config.py dynamically using importlib
23
+ spec = importlib.util.spec_from_file_location("config", config_path)
24
+ config = importlib.util.module_from_spec(spec)
25
+ spec.loader.exec_module(config)
26
+
27
+ # Step 3: Access variables from config.py
28
+ NOCODB_CATEGORIES = config.NOCODB_CATEGORIES
29
+ NOCODB_PRODUCTS = config.NOCODB_PRODUCTS
30
+ NOCODB_STATUSES = config.NOCODB_STATUSES
31
+ NOCODB_BOT_MESSAGES = config.NOCODB_BOT_MESSAGES
32
+ NOCODB_ORDERS = config.NOCODB_ORDERS
16
33
 
17
34
  class Gateway(NocodbClient):
18
35
 
19
- def __init__(self,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None):
36
+ def __init__(self,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None,filter_buttons=[],special_attributes=False):
20
37
  super().__init__(NOCODB_HOST=NOCODB_HOST,NOCODB_API_KEY=NOCODB_API_KEY,SOURCE=SOURCE)
21
38
  self.NOCODB_HOST = NOCODB_HOST
22
39
  self.NOCODB_API_KEY = NOCODB_API_KEY
23
40
  self.logging=logging
24
41
  self.required_fields = [PRODUCT_NAME_FIELD, PRODUCT_PRICE_FIELD]
25
42
  self.projection = []
26
-
43
+ self.special_attributes=special_attributes
44
+ self.filter_buttons=filter_buttons
27
45
 
28
46
  async def load_data(self,SOURCE=None):
29
47
  self.SOURCE=SOURCE
30
48
  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)
49
+ self.category_manager=CategoryManager(table_id=self.tables_list[NOCODB_CATEGORIES],NOCODB_HOST=self.NOCODB_HOST,NOCODB_API_KEY=self.NOCODB_API_KEY,logging=True,filter_buttons=self.filter_buttons)
50
+ self.product_manager=ProductManager(table_id=self.tables_list[NOCODB_PRODUCTS],NOCODB_HOST=self.NOCODB_HOST,NOCODB_API_KEY=self.NOCODB_API_KEY,logging=True)
33
51
 
34
52
  async def create_product(self,product: ProductModel) -> ProductModel:
35
53
  products_table = self.tables_list[NOCODB_PRODUCTS]
36
54
  data = dump_product_data_with_check(data=product ,data_check=self.category_manager.categories)
37
55
  # product_json = dump_product_data_with_check(data=product,data_check=self.categories)
38
- data.pop("ID")
56
+ external_id = data.pop("ID")
57
+ metadata = await self.get_table_meta(self.tables_list["Products"])
58
+ images_column=[column["id"] for column in metadata["columns"] if column["column_name"] == "Images"][0]
59
+ data["ExternalImages"]=[image['title'] for image in data['Images']]
60
+
61
+ for num,item in enumerate(data['Images']):
62
+ url_before=item['url']
63
+ image_name=item['title']
64
+ 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)
65
+
39
66
  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)
67
+ # logger.info(f"Created product {record['id']}")
68
+ logger.info(f"Created product {external_id}")
42
69
 
70
+ async def get_all_products(self):
71
+ actual_products=[]
72
+ products_portion=[]
73
+ portion=200
74
+ # TODO Check once busy
75
+ for i in range(10):
76
+ products_portion=await self.product_manager.get_products_v2(offset=i*portion,limit=portion)
77
+ actual_products.extend(products_portion)
78
+ if len(products_portion) < 200:
79
+ break
80
+ return actual_products
81
+
43
82
  async def update_products(self, external_products: List[ProductModel]):
44
83
  products_table = self.tables_list[NOCODB_PRODUCTS]
45
- self.product_manager.actual_products=await self.product_manager.get_products_v2(offset=0,limit=200)
84
+ await self.product_manager.update_attributes(products=external_products)
85
+ # Updates categories if there were a new ones created
86
+ external_products=await self.category_manager.map_categories(external_products=external_products)
87
+ self.product_manager.actual_products=await self.get_all_products()
88
+ # self.product_manager.actual_products = await self.product_manager.get_products_v2(offset=0,limit=200)
89
+
46
90
  self.ids_mapping={product.external_id : product.id for product in self.product_manager.actual_products}
47
91
  products_meta= {product.external_id : product for product in self.product_manager.actual_products}
48
92
 
49
93
  for product in external_products:
50
94
  if product.external_id in self.ids_mapping.keys():
51
95
  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]):
96
+ 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
97
  await self.update_product(product=product)
54
98
  else:
55
99
  await self.create_product(product=product)
@@ -62,7 +106,7 @@ class Gateway(NocodbClient):
62
106
  table_name=products_table,
63
107
  record_id=product.id,
64
108
  updated_data=data)
65
- logger.info(f"Updated product {product.id}")
109
+ logger.info(f"Updated product {product.external_id}")
66
110
 
67
111
 
68
112
  def find_product_id_by_name(self,name: str):
@@ -99,13 +143,12 @@ class Gateway(NocodbClient):
99
143
  },
100
144
  headers=headers
101
145
  )
102
-
103
146
  logger.info(response.text())
104
147
 
105
148
  return response.json()
106
149
 
107
150
  async def delete_all_products(self):
108
- items = await self.product_manager.get_products_v2(offset=0,limit=100)
151
+ items = await self.product_manager.get_products_v2(offset=0,limit=200)
109
152
  products_table = self.tables_list[NOCODB_PRODUCTS]
110
153
  for num,item in enumerate(items):
111
154
  await self.delete_table_record(products_table, item.id)
@@ -29,7 +29,6 @@ class ExternalProductModel(BaseModel):
29
29
  class ProductModel(BaseModel):
30
30
  id: Optional[str]
31
31
  external_id: Optional[str]
32
-
33
32
  category: Optional[List[str]]
34
33
  category_name: Optional[List[str]]
35
34
  name: str
@@ -43,8 +42,3 @@ class ProductModel(BaseModel):
43
42
  updated: datetime = Field(default=datetime.now, hidden_field=True)
44
43
  preview_url: List[str] = []
45
44
  extra_attributes: List[ExtraAttribute] = []
46
-
47
-
48
-
49
- class ProductModel(ExternalProductModel):
50
- id: str
@@ -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,45 @@ 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
+ # Step 1: Define the path to config.py (one level above)
15
+ config_path = Path(__file__).resolve().parent.parent / '../config.py'
16
+
17
+ # Step 2: Load config.py dynamically using importlib
18
+ spec = importlib.util.spec_from_file_location("config", config_path)
19
+ config = importlib.util.module_from_spec(spec)
20
+ spec.loader.exec_module(config)
21
+
22
+ # Step 3: Load all required constants from config.py
23
+ CATEGORY_IMAGE_FIELD = config.CATEGORY_IMAGE_FIELD
24
+ ID_FIELD = config.ID_FIELD
25
+ CATEGORY_NAME_FIELD = config.CATEGORY_NAME_FIELD
26
+ CATEGORY_PARENT_ID_FIELD = config.CATEGORY_PARENT_ID_FIELD
27
+ CATEGORY_PARENT_FIELD = config.CATEGORY_PARENT_FIELD
28
+
29
+ PRODUCT_NAME_FIELD = config.PRODUCT_NAME_FIELD
30
+ PRODUCT_DESCRIPTION_FIELD = config.PRODUCT_DESCRIPTION_FIELD
31
+ PRODUCT_PRICE_FIELD = config.PRODUCT_PRICE_FIELD
32
+ PRODUCT_CURRENCY_FIELD = config.PRODUCT_CURRENCY_FIELD
33
+ PRODUCT_STOCK_FIELD = config.PRODUCT_STOCK_FIELD
34
+ PRODUCT_CATEGORY_NAME_FIELD = config.PRODUCT_CATEGORY_NAME_FIELD
35
+ PRODUCT_CATEGORY_ID_FIELD = config.PRODUCT_CATEGORY_ID_FIELD
36
+ PRODUCT_IMAGE_FIELD = config.PRODUCT_IMAGE_FIELD
37
+ PRODUCT_DISCOUNT_PRICE_FIELD = config.PRODUCT_DISCOUNT_PRICE_FIELD
38
+ PRODUCT_CATEGORY_ID_LOOKUP_FIELD = config.PRODUCT_CATEGORY_ID_LOOKUP_FIELD
39
+ PRODUCT_REQUIRED_OPTIONS_FIELD = config.PRODUCT_REQUIRED_OPTIONS_FIELD
40
+ PRODUCT_CATEGORIES_EXTRA_OPTIONS_FIELD = config.PRODUCT_CATEGORIES_EXTRA_OPTIONS_FIELD
41
+ PRODUCT_CATEGORIES_EXTRA_OPTION_NAMES_FIELD = config.PRODUCT_CATEGORIES_EXTRA_OPTION_NAMES_FIELD
42
+ PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD = config.PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD
43
+ PRODUCT_ID_FIELD = config.PRODUCT_ID_FIELD
44
+ PRODUCT_EXTERNAL_ID = config.PRODUCT_EXTERNAL_ID
45
+ PRODUCT_CHECKOUT_MODE = config.PRODUCT_CHECKOUT_MODE
46
+ NEW_ID_FIELD = config.NEW_ID_FIELD
47
+
48
+ NOCODB_CHECKOUT_MODES = config.NOCODB_CHECKOUT_MODES
49
+
11
50
 
12
51
  def get_pagination_info(page_info: dict) -> PaginationResponseModel:
13
52
  page_info = PaginationResponseModel(total_rows=page_info['totalRows'],
@@ -17,15 +56,6 @@ def get_pagination_info(page_info: dict) -> PaginationResponseModel:
17
56
  is_last_page=page_info['isLastPage'])
18
57
  return page_info
19
58
 
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
59
  def parse_category_data(data: dict) -> CategoryResponseModel:
30
60
  preview_url = ""
31
61
  if data.get(CATEGORY_IMAGE_FIELD):
@@ -47,37 +77,8 @@ def dump_category_data(data: CategoryModel) -> dict:
47
77
  }
48
78
 
49
79
 
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
80
  def dump_product_data(data: ProductModel) -> dict:
79
- if data.external_id=="21":
80
- print("Hoi")
81
+
81
82
  preview_url = ([{'url': image_url,
82
83
  'title': f'{secrets.token_hex(6)}.jpeg',
83
84
  'mimetype': 'image/jpeg'}
@@ -91,9 +92,7 @@ def dump_product_data(data: ProductModel) -> dict:
91
92
  PRODUCT_PRICE_FIELD: data.price,
92
93
  PRODUCT_CURRENCY_FIELD: data.currency,
93
94
  PRODUCT_STOCK_FIELD: data.stock_qty,
94
- #TODO Add for several categories
95
95
  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
96
  PRODUCT_CATEGORY_ID_FIELD: [{'Id': data.category}] if data.category else None,
98
97
  PRODUCT_IMAGE_FIELD: preview_url,
99
98
  PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price
@@ -107,6 +106,9 @@ def dump_product_data_with_check(data: ProductModel, data_check: dict) -> dict:
107
106
  for image_url in data.preview_url]
108
107
  if data.preview_url
109
108
  else [])
109
+
110
+ extra_data = {item.name : item.description for item in data.extra_attributes}
111
+
110
112
  product_data = {
111
113
  PRODUCT_ID_FIELD: data.id,
112
114
  PRODUCT_EXTERNAL_ID: data.external_id,
@@ -115,13 +117,15 @@ def dump_product_data_with_check(data: ProductModel, data_check: dict) -> dict:
115
117
  PRODUCT_PRICE_FIELD: data.price,
116
118
  PRODUCT_CURRENCY_FIELD: data.currency,
117
119
  PRODUCT_STOCK_FIELD: data.stock_qty,
118
- #TODO Add for several categories
119
120
  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,
121
+ PRODUCT_CATEGORY_ID_FIELD: [{'Id': data_check[item]} for item in data.category_name] if data.category else None,
122
122
  PRODUCT_IMAGE_FIELD: preview_url,
123
- PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price
123
+ PRODUCT_CHECKOUT_MODE: NOCODB_CHECKOUT_MODES,
124
+ PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price,
124
125
  }
126
+
127
+ if len(extra_data)>0:
128
+ product_data.update(extra_data)
125
129
  return product_data
126
130
 
127
131
 
@@ -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:
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.1
2
+ Name: tgshops-integrations
3
+ Version: 1.0
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
+ Home-page: https://git.the-devs.com/virtual-shops/shop-system/shop-backend-integrations/integration-library/integration-library
6
+ Author: Dimi Latoff
7
+ Author-email: drpozd@gmail.com
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+
16
+
17
+
18
+
19
+
20
+ ```python
21
+
22
+ from typing import List
23
+ import asyncio
24
+
25
+ from config import NocoDBConfig
26
+ from tgshops_integrations.middlewares.gateway import Gateway
27
+ from services.bitrix.client import BitrixClient
28
+
29
+ # Your credentials are here and source of the target table
30
+ NOCODB_HOST = NocoDBConfig.HOST
31
+ NOCODB_API_KEY = NocoDBConfig.API_KEY
32
+ SOURCE=NocoDBConfig.source_table
33
+
34
+ async def main():
35
+
36
+ # Here is your client to upload data from your service
37
+ bitrixService=BitrixClient()
38
+
39
+ # Products have to be in a according to the ProductModel
40
+ # class ProductModel(BaseModel):
41
+ # id: Optional[str]
42
+ # external_id: Optional[str]
43
+ # category: Optional[List[str]]
44
+ # category_name: Optional[List[str]]
45
+ # name: str
46
+ # description: Optional[str]
47
+ # price: Optional[float]
48
+ # final_price: Optional[float]
49
+ # currency: Optional[str]
50
+ # stock_qty: int
51
+ # orders_qty: int = Field(0, hidden_field=True)
52
+ # created: datetime = Field(default=datetime.now, hidden_field=True)
53
+ # updated: datetime = Field(default=datetime.now, hidden_field=True)
54
+ # preview_url: List[str] = []
55
+ # extra_attributes: List[ExtraAttribute] = []
56
+
57
+ bitrix_product_list=await bitrixService.get_crm_product_list()
58
+
59
+ NocoGateway = Gateway(NOCODB_HOST=NOCODB_HOST,NOCODB_API_KEY=NOCODB_API_KEY)
60
+
61
+ # Example how to clean your table
62
+ # await NocoGateway.load_data(SOURCE=SOURCE)
63
+ # await NocoGateway.delete_all_products()
64
+
65
+
66
+ # In order to obtain data from the table need to call load data, to obtain it for further comparation
67
+ await NocoGateway.load_data(SOURCE=SOURCE)
68
+ # Initializes any missing categories
69
+ await NocoGateway.category_manager.update_categories(external_products=bitrix_product_list)
70
+
71
+ # Creates or updates the products
72
+ await NocoGateway.update_products(external_products=bitrix_product_list)
73
+
74
+
75
+ asyncio.run(main())
76
+
77
+
78
+
79
+
80
+
81
+ ```
@@ -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=GxXs-lVtrdWq3WTh_j0Hi1ffGsIOCCKJfOu3eyiRI2g,7441
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=xLmfHGZBM4aEf2ZxVuitx5ckXQFSF9VpYms6qy2ao8Y,7653
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.0.dist-info/METADATA,sha256=7D9nxdGywF15S1rf_XBFr3DKX3skyvq_P9O-EQ1oeyo,2774
14
+ tgshops_integrations-1.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
15
+ tgshops_integrations-1.0.dist-info/top_level.txt,sha256=HFNtxqDpzmlF4ZLnMiwhbU7pOa_YozxU2zBl0bnUmcY,21
16
+ tgshops_integrations-1.0.dist-info/RECORD,,
@@ -1,75 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tgshops-integrations
3
- Version: 0.3
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
- Home-page: https://git.the-devs.com/virtual-shops/shop-system/shop-backend-integrations/integration-library/integration-library
6
- Author: Dimi Latoff
7
- Author-email: drpozd@gmail.com
8
- License: MIT
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Operating System :: OS Independent
12
- Requires-Python: >=3.10
13
- Description-Content-Type: text/markdown
14
-
15
-
16
-
17
-
18
-
19
-
20
- ```python
21
-
22
- import tgshops_integrations
23
- import ExternalProductModel, ExternalCategoryModel from tgshops_integrations.models
24
-
25
- # Load external data
26
- external_categories = [ExternalCategoryModel(
27
- name="Drinks",
28
- image_url="https://example.com/image.jpg",
29
- external_id="0"
30
- ), CategoryModel(
31
- name="Coffee",
32
- image_url="https://example.com/image.jpg",
33
- external_id="1"
34
- )]
35
- external_products = [ExternalProductModel(
36
- name="Coffee",
37
- description="",
38
- price=10.0,
39
- currency="USD",
40
- image_url="https://example.com/image.jpg",
41
- category=List["0", "1"],
42
- external_id="0"
43
- )]
44
-
45
-
46
- # Initialise
47
- product_service = tgshops_integrations.ProductService(token="your_token_here")
48
-
49
- await product_service.update_categories(
50
- external_categories=external_categories
51
- )
52
-
53
- await product_service.update_products(
54
- external_products=external_products
55
- )
56
-
57
- # Here is the the custom integration of your service, which has to return products according to the ExternalProductModel
58
- bitrixService=BitrixClient()
59
- bitrix_product_list=await bitrixService.get_crm_product_list()
60
-
61
- # One gateway can work with several table / DBs
62
- NocoGateway = Gateway(NOCODB_HOST=NOCODB_HOST,NOCODB_API_KEY=NOCODB_API_KEY)
63
-
64
- # await NocoGateway.load_data()
65
- # await NocoGateway.delete_all_products()
66
-
67
- # Load data provides the access to the certain table and allows to obtain the data about the products or catergories
68
- await NocoGateway.load_data(SOURCE=SOURCE)
69
- await NocoGateway.category_manager.update_categories(external_products=bitrix_product_list)
70
- await NocoGateway.update_products(external_products=bitrix_product_list)
71
-
72
-
73
-
74
-
75
- ```
@@ -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=rRVwEo1NP5pvarzvbwD9NbkTqtqxRDfIHCpNfx54Aus,1440
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.3.dist-info/METADATA,sha256=A5rJekXxjrEVJ6aXaR42G-fH4pgOwUntpyn4YYqH5NY,2391
14
- tgshops_integrations-0.3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
15
- tgshops_integrations-0.3.dist-info/top_level.txt,sha256=HFNtxqDpzmlF4ZLnMiwhbU7pOa_YozxU2zBl0bnUmcY,21
16
- tgshops_integrations-0.3.dist-info/RECORD,,