tgshops-integrations 0.1__py3-none-any.whl → 0.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.
File without changes
File without changes
@@ -0,0 +1,111 @@
1
+ from typing import List,Optional
2
+
3
+ from aiocache import cached
4
+ from models.products import ProductModel
5
+ from nocodb_connector.client import NocodbClient
6
+ from nocodb_connector.model_mapping import dump_product_data,dump_product_data_with_check, get_pagination_info, ID_FIELD, \
7
+ parse_product_data, PRODUCT_CATEGORY_ID_LOOKUP_FIELD, PRODUCT_NAME_FIELD, PRODUCT_PRICE_FIELD, \
8
+ PRODUCT_STOCK_FIELD
9
+
10
+ from tgshops_integrations.nocodb_connector.categories import CategoryManager
11
+ from tgshops_integrations.nocodb_connector.products import ProductManager
12
+ from tgshops_integrations.nocodb_connector.tables import *
13
+ from loguru import logger
14
+ import hashlib
15
+
16
+
17
+ class Gateway(NocodbClient):
18
+
19
+ def __init__(self,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None):
20
+ 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]
25
+ self.projection = []
26
+
27
+
28
+ async def load_data(self,SOURCE=None):
29
+ self.SOURCE=SOURCE
30
+ 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)
33
+
34
+ async def create_product(self,product: ProductModel) -> ProductModel:
35
+ products_table = self.tables_list[NOCODB_PRODUCTS]
36
+ data = dump_product_data_with_check(data=product ,data_check=self.category_manager.categories)
37
+ # product_json = dump_product_data_with_check(data=product,data_check=self.categories)
38
+ data.pop("ID")
39
+ 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)
42
+
43
+ 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)
46
+ self.ids_mapping={product.external_id : product.id for product in self.product_manager.actual_products}
47
+ products_meta= {product.external_id : product for product in self.product_manager.actual_products}
48
+
49
+ for product in external_products:
50
+ if product.external_id in self.ids_mapping.keys():
51
+ 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]):
53
+ await self.update_product(product=product)
54
+ else:
55
+ await self.create_product(product=product)
56
+
57
+ async def update_product(self, product: ProductModel):
58
+ products_table = self.tables_list[NOCODB_PRODUCTS]
59
+ data = dump_product_data_with_check(data=product ,data_check=self.category_manager.categories)
60
+
61
+ await self.update_table_record(
62
+ table_name=products_table,
63
+ record_id=product.id,
64
+ updated_data=data)
65
+ logger.info(f"Updated product {product.id}")
66
+
67
+
68
+ def find_product_id_by_name(self,name: str):
69
+ for product in self.product_manager.actual_products.products:
70
+ if product.name == name:
71
+ return product.id
72
+ return None # Return None if no product is found with the given name
73
+
74
+ async def create_table_column(self, name: str, table_id: Optional[str] = None):
75
+
76
+ BEARER_TOKEN = "jpdxJtyfDXdjbvxKAcIij1HA8HGalgalLLXZ46DV"
77
+
78
+ headers = {
79
+ "Authorization": f"Bearer {BEARER_TOKEN}"
80
+ }
81
+ if not table_id:
82
+ table_id = self.tables_list[NOCODB_PRODUCTS]
83
+
84
+ response = await self.httpx_client.post(
85
+ f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_id}/columns",
86
+ json={
87
+ "column_name": name,
88
+ "dt": "character varying",
89
+ "dtx": "specificType",
90
+ "ct": "varchar(45)",
91
+ "clen": 45,
92
+ "dtxp": "45",
93
+ "dtxs": "",
94
+ "altered": 1,
95
+ "uidt": "SingleLineText",
96
+ "uip": "",
97
+ "uicn": "",
98
+ "title": name
99
+ },
100
+ headers=headers
101
+ )
102
+
103
+ logger.info(response.text())
104
+
105
+ return response.json()
106
+
107
+ 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]
110
+ for num,item in enumerate(items):
111
+ await self.delete_table_record(products_table, item.id)
File without changes
@@ -0,0 +1,43 @@
1
+ from pydantic import BaseModel
2
+ from pydantic import BaseModel, Field, schema, validator
3
+ from typing import List
4
+
5
+ class PaginationResponseModel(BaseModel):
6
+ total_rows: int
7
+ page: int
8
+ page_size: int
9
+ is_first_page: bool
10
+ is_last_page: bool
11
+
12
+
13
+ class ExternalCategoryModel(BaseModel):
14
+ external_id: str
15
+ name: str
16
+ parent_category: str
17
+ preview_url: str
18
+
19
+
20
+ class CategoryModel(ExternalCategoryModel):
21
+ name: str
22
+ parent_category: str = ''
23
+ preview_url: str = ''
24
+
25
+ @validator('name')
26
+ def not_empty_string(cls, value):
27
+ if not value:
28
+ raise ValueError('Name cannot be an empty string.')
29
+ return value
30
+
31
+
32
+ class CategoryResponseModel(BaseModel):
33
+ id: str
34
+ name: str
35
+ parent_category: str
36
+ preview_url: str
37
+
38
+ # TODO: add bitrix category id here
39
+ external_id: str = None
40
+
41
+ class CategoryListResponseModel(BaseModel):
42
+ categories: List[CategoryResponseModel]
43
+ page_info: PaginationResponseModel
@@ -0,0 +1,50 @@
1
+ from pydantic import BaseModel
2
+ from typing import Any, List, Optional
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel, Field, schema, validator
6
+
7
+
8
+ class ExtraAttribute(BaseModel):
9
+ name: str
10
+ description: Optional[str]
11
+
12
+ class ExternalProductModel(BaseModel):
13
+ external_id: str
14
+
15
+ category: Optional[List[str]]
16
+ category_name: Optional[List[str]]
17
+ name: str
18
+ description: Optional[str]
19
+ price: Optional[float]
20
+ final_price: Optional[float]
21
+ currency: Optional[str]
22
+ stock_qty: int
23
+ orders_qty: int = Field(0, hidden_field=True)
24
+ created: datetime = Field(default=datetime.now, hidden_field=True)
25
+ updated: datetime = Field(default=datetime.now, hidden_field=True)
26
+ preview_url: List[str] = []
27
+ extra_attributes: List[ExtraAttribute] = []
28
+
29
+ class ProductModel(BaseModel):
30
+ id: Optional[str]
31
+ external_id: Optional[str]
32
+
33
+ category: Optional[List[str]]
34
+ category_name: Optional[List[str]]
35
+ name: str
36
+ description: Optional[str]
37
+ price: Optional[float]
38
+ final_price: Optional[float]
39
+ currency: Optional[str]
40
+ stock_qty: int
41
+ orders_qty: int = Field(0, hidden_field=True)
42
+ created: datetime = Field(default=datetime.now, hidden_field=True)
43
+ updated: datetime = Field(default=datetime.now, hidden_field=True)
44
+ preview_url: List[str] = []
45
+ extra_attributes: List[ExtraAttribute] = []
46
+
47
+
48
+
49
+ class ProductModel(ExternalProductModel):
50
+ id: str
File without changes
@@ -0,0 +1,102 @@
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, dump_category_data, get_pagination_info, parse_category_data
10
+
11
+
12
+ class CategoryManager(NocodbClient):
13
+ def __init__(self,table_id=None,logging=False,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None):
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.categories_table=table_id
19
+ self.external_categories={}
20
+ self.logging=logging
21
+ self.required_fields = [CATEGORY_NAME_FIELD]
22
+ self.projection = ["Id", CATEGORY_NAME_FIELD, CATEGORY_PARENT_ID_FIELD, CATEGORY_IMAGE_FIELD]
23
+
24
+ @cached(ttl=30, key_builder=custom_key_builder)
25
+ async def get_categories(self, table_id: str) -> List[CategoryModel]:
26
+ records = await self.get_table_records(table_id, self.required_fields, self.projection)
27
+ return [parse_category_data(record) for record in records]
28
+
29
+ @cached(ttl=30, key_builder=custom_key_builder)
30
+ async def get_categories_v2(self,
31
+ table_id: str,
32
+ offset: int = None,
33
+ limit: int = None) -> CategoryModel:
34
+ response = (await self.get_table_records_v2(table_name=self.categories_table,
35
+ required_fields=self.required_fields,
36
+ projection=self.projection,
37
+ offset=offset,
38
+ limit=limit))
39
+ page_info = get_pagination_info(page_info=response['pageInfo'])
40
+ categories = [parse_category_data(record) for record in response['list']]
41
+ return CategoryListResponseModel(categories=categories, page_info=page_info)
42
+
43
+ @cached(ttl=30, key_builder=custom_key_builder)
44
+ async def get_category(self, table_id: str, category_id: str) -> CategoryModel:
45
+ record = await self.get_table_record(self.categories_table, category_id, self.required_fields, self.projection)
46
+ return parse_category_data(record)
47
+
48
+ async def create_category(self, table_id: str, category: CategoryModel) -> CategoryModel:
49
+ category_json = dump_category_data(category)
50
+ record = await self.create_table_record(self.categories_table, category_json)
51
+ return parse_category_data(record)
52
+
53
+ @cached(ttl=30, key_builder=custom_key_builder)
54
+ async def get_categories_in_category(self, table_id: str, category_id: str) -> List[CategoryModel]:
55
+ # ! In case category_id == 0,
56
+ # we need to get all categories without parent by field CATEGORY_PARENT_FIELD not CATEGORY_PARENT_ID_FIELD
57
+ records = await self.get_table_records(
58
+ table_name=self.categories_table,
59
+ required_fields=self.required_fields,
60
+ projection=self.projection,
61
+ extra_where=(f"({CATEGORY_PARENT_ID_FIELD},eq,{category_id})"
62
+ if category_id else f"({CATEGORY_PARENT_FIELD},eq,0)"))
63
+ return [parse_category_data(record) for record in records]
64
+
65
+ @cached(ttl=30, key_builder=custom_key_builder)
66
+ async def get_categories_in_category_v2(self,
67
+ table_id: str,
68
+ category_id: str,
69
+ offset: int,
70
+ limit: int) -> CategoryModel:
71
+
72
+ response = await self.get_table_records_v2(
73
+ table_name=self.categories_table,
74
+ required_fields=self.required_fields,
75
+ projection=self.projection,
76
+ extra_where=(f"({CATEGORY_PARENT_ID_FIELD},eq,{category_id})"
77
+ if category_id else f"({CATEGORY_PARENT_FIELD},eq,0)"),
78
+ offset=offset,
79
+ limit=limit)
80
+ categories = [parse_category_data(record) for record in response['list']]
81
+ page_info = get_pagination_info(page_info=response['pageInfo'])
82
+ return CategoryModel(categories=categories, page_info=page_info)
83
+
84
+ async def update_categories(self,external_products: List[ProductModel]):
85
+ # 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
+
89
+ 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
+
97
+ async def map_categories(self,external_products: List[ProductModel]) -> List[ProductModel]:
98
+ for product in external_products:
99
+ if not product.category:
100
+ for category in product.category_name:
101
+ product.category.append(self.categories[category])
102
+ return external_products
@@ -0,0 +1,186 @@
1
+ from typing import List,Optional
2
+
3
+ import httpx
4
+ import requests
5
+ from loguru import logger
6
+
7
+ from tgshops_integrations.nocodb_connector.model_mapping import ID_FIELD
8
+
9
+
10
+ def custom_key_builder(func, *args, **kwargs):
11
+ # Exclude 'self' by starting args processing from args[1:]
12
+ args_key_part = "-".join(str(arg) for arg in args[1:])
13
+ kwargs_key_part = "-".join(f"{key}-{value}" for key, value in sorted(kwargs.items()))
14
+ return f"{func.__name__}-{args_key_part}-{kwargs_key_part}"
15
+
16
+
17
+ class NocodbClient:
18
+
19
+ def __init__(self,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None):
20
+ self.NOCODB_HOST = NOCODB_HOST
21
+ self.NOCODB_API_KEY = NOCODB_API_KEY
22
+ self.SOURCE=SOURCE
23
+ self.httpx_client = httpx.AsyncClient()
24
+ self.httpx_client.headers = {
25
+ "xc-token": self.NOCODB_API_KEY
26
+ }
27
+
28
+ def construct_get_params(self,
29
+ required_fields: list = None,
30
+ projection: list = None,
31
+ extra_where: str = None,
32
+ offset: int = None,
33
+ limit: int = None) -> dict:
34
+ extra_params = {}
35
+ if projection:
36
+ extra_params["fields"] = ','.join(projection)
37
+ if required_fields:
38
+ extra_params["where"] = ""
39
+ for field in required_fields:
40
+ extra_params["where"] += f"({field},isnot,null)~and"
41
+ extra_params["where"] = extra_params["where"].rstrip("~and")
42
+ if extra_where:
43
+ if not extra_params.get("where"):
44
+ extra_params["where"] = extra_where
45
+ else:
46
+ extra_params["where"] += f"~and{extra_where}"
47
+ if offset:
48
+ extra_params['offset'] = offset
49
+ if limit:
50
+ extra_params["limit"] = limit
51
+ return extra_params
52
+
53
+ async def get_table_records(self,
54
+ table_name: str,
55
+ required_fields: list = None,
56
+ projection: list = None,
57
+ extra_where: str = None,
58
+ limit: int = None) -> List[dict]:
59
+ url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
60
+ extra_params = self.construct_get_params(required_fields, projection, extra_where, limit=limit)
61
+ response = await self.httpx_client.get(url, params=extra_params)
62
+ if response.status_code == 200:
63
+ return response.json()["list"]
64
+ raise Exception(response.text)
65
+
66
+ async def get_table_records_v2(self,
67
+ table_name: str,
68
+ required_fields: list = None,
69
+ projection: list = None,
70
+ extra_where: str = None,
71
+ offset: int = None,
72
+ limit: int = 25) -> dict:
73
+ url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
74
+ extra_params = self.construct_get_params(required_fields, projection, extra_where, offset=offset, limit=limit)
75
+ response = await self.httpx_client.get(url, params=extra_params)
76
+ if response.status_code == 200:
77
+ return response.json()
78
+ 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
+
86
+ async def get_table_record(self,
87
+ table_name: str,
88
+ record_id: str,
89
+ required_fields: list = None,
90
+ projection: list = None) -> dict:
91
+ url = f"{self.NOCODB_HOST}/tables/{table_name}/records/{record_id}"
92
+ extra_params = self.construct_get_params(required_fields, projection)
93
+ response = await self.httpx_client.get(url, params=extra_params)
94
+ if response.status_code == 200:
95
+ return response.json()
96
+ raise Exception(response.text)
97
+
98
+ async def create_table_record(self, table_name: str, record: dict) -> dict:
99
+ url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
100
+ response = await self.httpx_client.post(url, json=record)
101
+ if response.status_code == 200:
102
+ record["id"] = response.json().get("id")
103
+ if not record["id"]:
104
+ record["id"] = response.json().get("Id")
105
+ return record
106
+ raise Exception(response.text)
107
+
108
+ async def count_table_records(self, table_name: str) -> int:
109
+ url = f"{self.NOCODB_HOST}/tables/{table_name}/records/count"
110
+ response = await self.httpx_client.get(url)
111
+ if response.status_code == 200:
112
+ return response.json().get("count", 0)
113
+ raise Exception(response.text)
114
+
115
+ async def update_table_record(self, table_name: str, record_id: str, updated_data: dict) -> bool:
116
+ url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
117
+ updated_data[ID_FIELD] = record_id
118
+ response = await self.httpx_client.patch(url, json=updated_data)
119
+ if response.status_code == 200:
120
+ return True
121
+ raise Exception(response.text)
122
+
123
+ async def delete_table_record(self, table_name: str, record_id: str) -> dict:
124
+ url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
125
+ response = requests.delete(url, json={"Id": record_id}, headers=self.httpx_client.headers)
126
+ if response.status_code == 200:
127
+ logger.info(f"Deleted item {record_id}")
128
+ return response.json()
129
+
130
+ # Not transport
131
+ async def get_product_categories(self, table_id: str,table_name : str) -> int:
132
+ url = f"{self.NOCODB_HOST}/tables/{table_id}/records"
133
+ limit=75
134
+ extra_params = self.construct_get_params(limit=limit)
135
+ response = await self.httpx_client.get(url, params=extra_params)
136
+
137
+ if response.status_code == 200:
138
+ self.categories={category[table_name] : category["Id"] for category in response.json()["list"]}
139
+ # raise Exception(response.text)
140
+
141
+ async def create_product_category(self, table_id: str, category_name : str, table_name : str, category_id : int = 0) -> dict:
142
+ url = f"{self.NOCODB_HOST}/tables/{table_id}/records"
143
+
144
+ record={table_name: category_name, "Id" : category_id}
145
+
146
+ response = await self.httpx_client.post(url, json=record)
147
+ 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)
152
+ return record
153
+ raise Exception(response.text)
154
+
155
+ async def get_table_meta(self, table_name: str):
156
+ return (await self.httpx_client.get(
157
+ f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}")).json()
158
+
159
+
160
+ async def get_all_tables(self, source: Optional[str] = None):
161
+ if not source:
162
+ source=self.SOURCE
163
+
164
+ url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/projects/{source}/tables?includeM2M=false"
165
+ response=(await self.httpx_client.get(url)).json()
166
+ tables_info=response.get('list', [])
167
+ self.tables_list={table["title"] : table["id"] for table in tables_info}
168
+ return self.tables_list
169
+
170
+ async def get_sources(self):
171
+ return (await self.httpx_client.get(
172
+ f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/projects/")).json().get(
173
+ 'list', [])
174
+
175
+ def link_tables(self, source: str, parent_id: str, child_id: str, parent_table: str, child_table: str):
176
+ """
177
+ Связывает таблицы
178
+ :param source:
179
+ :param parent_id:
180
+ :param child_id:
181
+ :param parent_table:
182
+ :param child_table:
183
+ :return:
184
+ """
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()
@@ -0,0 +1,166 @@
1
+ import json
2
+ import secrets
3
+
4
+ import markdown
5
+
6
+ from tgshops_integrations.models.categories import CategoryModel
7
+ from tgshops_integrations.models.products import ExtraAttribute, ProductModel
8
+ from tgshops_integrations.models.categories import CategoryResponseModel,PaginationResponseModel
9
+ from tgshops_integrations.models.products import ProductModel
10
+
11
+
12
+ def get_pagination_info(page_info: dict) -> PaginationResponseModel:
13
+ page_info = PaginationResponseModel(total_rows=page_info['totalRows'],
14
+ page=page_info['page'],
15
+ page_size=page_info['pageSize'],
16
+ is_first_page=page_info['isFirstPage'],
17
+ is_last_page=page_info['isLastPage'])
18
+ return page_info
19
+
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
+ def parse_category_data(data: dict) -> CategoryResponseModel:
30
+ preview_url = ""
31
+ if data.get(CATEGORY_IMAGE_FIELD):
32
+ preview_url = data[CATEGORY_IMAGE_FIELD][0].get("url", "")
33
+ return CategoryResponseModel(
34
+ id=str(data[ID_FIELD]),
35
+ name=data.get(CATEGORY_NAME_FIELD, ""),
36
+ parent_category=str(data.get(CATEGORY_PARENT_ID_FIELD, 0)),
37
+ preview_url=preview_url,
38
+ )
39
+
40
+
41
+ def dump_category_data(data: CategoryModel) -> dict:
42
+ return {
43
+ CATEGORY_NAME_FIELD: data.name,
44
+ CATEGORY_PARENT_FIELD: data.parent_category,
45
+ CATEGORY_IMAGE_FIELD: [
46
+ {"url": data.preview_url, 'title': f'{secrets.token_hex(6)}.jpeg', 'mimetype': 'image/jpeg'}]
47
+ }
48
+
49
+
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
+ def dump_product_data(data: ProductModel) -> dict:
79
+ if data.external_id=="21":
80
+ print("Hoi")
81
+ preview_url = ([{'url': image_url,
82
+ 'title': f'{secrets.token_hex(6)}.jpeg',
83
+ 'mimetype': 'image/jpeg'}
84
+ for image_url in data.preview_url]
85
+ if data.preview_url
86
+ else [])
87
+
88
+ return {
89
+ PRODUCT_NAME_FIELD: data.name,
90
+ PRODUCT_DESCRIPTION_FIELD: data.description,
91
+ PRODUCT_PRICE_FIELD: data.price,
92
+ PRODUCT_CURRENCY_FIELD: data.currency,
93
+ PRODUCT_STOCK_FIELD: data.stock_qty,
94
+ #TODO Add for several categories
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
+ PRODUCT_CATEGORY_ID_FIELD: [{'Id': data.category}] if data.category else None,
98
+ PRODUCT_IMAGE_FIELD: preview_url,
99
+ PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price
100
+ }
101
+
102
+ def dump_product_data_with_check(data: ProductModel, data_check: dict) -> dict:
103
+
104
+ preview_url = ([{'url': image_url,
105
+ 'title': f'{secrets.token_hex(6)}.jpeg',
106
+ 'mimetype': 'image/jpeg'}
107
+ for image_url in data.preview_url]
108
+ if data.preview_url
109
+ else [])
110
+ product_data = {
111
+ PRODUCT_ID_FIELD: data.id,
112
+ PRODUCT_EXTERNAL_ID: data.external_id,
113
+ PRODUCT_NAME_FIELD: data.name,
114
+ PRODUCT_DESCRIPTION_FIELD: data.description,
115
+ PRODUCT_PRICE_FIELD: data.price,
116
+ PRODUCT_CURRENCY_FIELD: data.currency,
117
+ PRODUCT_STOCK_FIELD: data.stock_qty,
118
+ #TODO Add for several categories
119
+ 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,
122
+ PRODUCT_IMAGE_FIELD: preview_url,
123
+ PRODUCT_DISCOUNT_PRICE_FIELD: data.final_price
124
+ }
125
+ return product_data
126
+
127
+
128
+ async def parse_product_data(data: dict) -> ProductModel:
129
+ preview_url = [image['url'] for image in data[PRODUCT_IMAGE_FIELD]] if data.get(PRODUCT_IMAGE_FIELD, '') else []
130
+ primary_keys = [ID_FIELD,PRODUCT_NAME_FIELD, PRODUCT_DESCRIPTION_FIELD, PRODUCT_PRICE_FIELD,
131
+ PRODUCT_CURRENCY_FIELD, PRODUCT_STOCK_FIELD, PRODUCT_CATEGORY_ID_FIELD, PRODUCT_IMAGE_FIELD,
132
+ PRODUCT_CATEGORY_NAME_FIELD, PRODUCT_DISCOUNT_PRICE_FIELD, PRODUCT_CATEGORY_ID_LOOKUP_FIELD,
133
+ PRODUCT_REQUIRED_OPTIONS_FIELD, PRODUCT_CATEGORIES_EXTRA_OPTIONS_FIELD,
134
+ PRODUCT_CATEGORIES_EXTRA_OPTION_NAMES_FIELD, PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD,
135
+ "UpdatedAt", "CreatedAt"]
136
+
137
+ # Dynamically adding extra attributes
138
+ extra_attributes = []
139
+ for key, value in data.items():
140
+ if key not in primary_keys and value is not None and type(value) in [str, int, float]:
141
+ extra_attributes.append(ExtraAttribute(name=key, description=str(value)))
142
+
143
+ product = ProductModel(
144
+ id=str(data[ID_FIELD]) if data.get(ID_FIELD) else data.get(NEW_ID_FIELD),
145
+ external_id=data.get(PRODUCT_EXTERNAL_ID, ""),
146
+ name=data.get(PRODUCT_NAME_FIELD, ""),
147
+ description=data.get(PRODUCT_DESCRIPTION_FIELD, "") if data.get(PRODUCT_DESCRIPTION_FIELD) else "",
148
+ price=data.get(PRODUCT_PRICE_FIELD, 0.0),
149
+ currency=data.get(PRODUCT_CURRENCY_FIELD, ["RUB","CZK","GBP"]) if data.get(PRODUCT_CURRENCY_FIELD) else "RUB",
150
+ stock_qty=data.get(PRODUCT_STOCK_FIELD, 0),
151
+ preview_url=preview_url,
152
+ category_name=data.get(PRODUCT_CATEGORY_NAME_FIELD, []) if data.get(PRODUCT_CATEGORY_NAME_FIELD) else [],
153
+ category=data.get(PRODUCT_CATEGORY_ID_LOOKUP_FIELD, []) if data.get(PRODUCT_CATEGORY_ID_LOOKUP_FIELD) else [],
154
+ # category=[],
155
+ extra_attributes=extra_attributes,
156
+ extra_option_choice_required=any(data.get(PRODUCT_EXTRA_CHOICE_REQUIRED_FIELD, [])),
157
+ metadata = data
158
+ )
159
+ if data.get(PRODUCT_DISCOUNT_PRICE_FIELD, data.get(PRODUCT_PRICE_FIELD, 0.0)):
160
+ product.final_price = data.get(PRODUCT_DISCOUNT_PRICE_FIELD, data.get(PRODUCT_PRICE_FIELD, 0.0))
161
+
162
+ return product
163
+
164
+
165
+
166
+
@@ -0,0 +1,153 @@
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
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
+
30
+ def hash_product(self,product):
31
+ # 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}"
33
+ # Hash the concatenated string
34
+ hash_object = hashlib.sha256(hash_string.encode())
35
+ hex_dig = hash_object.hexdigest()
36
+ return hex_dig
37
+
38
+ @cached(ttl=30, key_builder=custom_key_builder)
39
+ async def get_products(self, table_id: str) -> List[ProductModel]:
40
+ records = await self.get_table_records(self.products_table, self.required_fields, self.projection)
41
+ return [parse_product_data(record) for record in records]
42
+
43
+ # @cached(ttl=30, key_builder=custom_key_builder)
44
+ async def get_products_v2(self, offset: int, limit: int, table_id: Optional[str] = None) -> List[ProductModel]:
45
+ # Get the names of the tables from the DB for further handling
46
+ await self.get_all_tables()
47
+ response = await self.get_table_records_v2(table_name=self.products_table,
48
+ required_fields=self.required_fields,
49
+ projection=self.projection,
50
+ offset=offset,
51
+ limit=limit)
52
+ products = [await parse_product_data(record) for record in response['list']]
53
+
54
+ return products
55
+
56
+ @cached(ttl=180, key_builder=custom_key_builder)
57
+ async def search_products(self, table_id: str, search_string: str, limit: int) -> List[ProductModel]:
58
+ records = await self.get_table_records(
59
+ table_name=self.products_table,
60
+ required_fields=self.required_fields,
61
+ projection=self.projection,
62
+ extra_where=f"({PRODUCT_NAME_FIELD},like,%{search_string}%)", # Update with actual product name field
63
+ limit=limit
64
+ )
65
+ return [parse_product_data(record) for record in records]
66
+
67
+ @cached(ttl=180, key_builder=custom_key_builder)
68
+ async def search_products_v2(self, table_id: str, search_string: str, limit: int) -> List[ProductModel]:
69
+ records = (await self.get_table_records_v2(
70
+ table_name=self.products_table,
71
+ required_fields=self.required_fields,
72
+ projection=self.projection,
73
+ extra_where=f"({PRODUCT_NAME_FIELD},like,%{search_string}%)", # Update with actual product name field
74
+ limit=limit
75
+ ))['list']
76
+ return [parse_product_data(record) for record in records]
77
+
78
+ @cached(ttl=60, key_builder=custom_key_builder)
79
+ async def get_product(self, table_id: str, product_id: str) -> ProductModel:
80
+ record = await self.get_table_record(self.products_table, product_id)
81
+ return parse_product_data(record)
82
+
83
+ @cached(ttl=60, key_builder=custom_key_builder)
84
+ async def get_product_v2(self, table_id: str, product_id: str) -> ProductModel:
85
+ record = await self.get_table_record(self.products_table, product_id)
86
+ product = parse_product_data(record)
87
+
88
+ related_products = await self.get_table_records_v2(
89
+ table_name=self.products_table,
90
+ required_fields=self.required_fields,
91
+ projection=self.projection,
92
+ extra_where=(f'({PRODUCT_STOCK_FIELD},gt,0)~and'
93
+ f"({PRODUCT_CATEGORY_ID_LOOKUP_FIELD},eq,{product.category[0]})~and"
94
+ f'({PRODUCT_NAME_FIELD},neq,{product.name})'),
95
+ limit=5
96
+ )
97
+ related_products = [parse_product_data(product) for product in related_products['list']]
98
+
99
+ product.related_products = related_products
100
+ return product
101
+
102
+ @cached(ttl=60, key_builder=custom_key_builder)
103
+ async def get_product_in_category(self, table_id: str, category_id: str = None) -> List[ProductModel]:
104
+ if category_id is None:
105
+ return await self.get_products(table_id=self.products_table)
106
+
107
+ records = await self.get_table_records(
108
+ table_name=self.products_table,
109
+ required_fields=self.required_fields,
110
+ projection=self.projection,
111
+ extra_where=(f'({PRODUCT_STOCK_FIELD},gt,0)~and'
112
+ f"({PRODUCT_CATEGORY_ID_LOOKUP_FIELD},eq,{category_id})")
113
+ )
114
+ return [parse_product_data(record) for record in records]
115
+
116
+ @cached(ttl=60, key_builder=custom_key_builder)
117
+ async def get_product_in_category_v2(self,
118
+ table_id: str,
119
+ offset: int,
120
+ limit: int,
121
+ category_id: str = None) -> ProductModel:
122
+ if category_id is None:
123
+ return await self.get_products_v2(table_id=self.products_table, offset=offset, limit=limit)
124
+
125
+ response = (await self.get_table_records_v2(
126
+ table_name=self.products_table,
127
+ required_fields=self.required_fields,
128
+ projection=self.projection,
129
+ extra_where=(f'({PRODUCT_STOCK_FIELD},gt,0)~and'
130
+ f"({PRODUCT_CATEGORY_ID_LOOKUP_FIELD},eq,{category_id})"),
131
+ offset=offset,
132
+ limit=limit
133
+ ))
134
+ page_info = get_pagination_info(page_info=response['pageInfo'])
135
+ products = [parse_product_data(record) for record in response['list']]
136
+ return ProductModel(products=products, page_info=page_info)
137
+
138
+ @cached(ttl=60, key_builder=custom_key_builder)
139
+ async def get_products_by_ids(self, table_id: str, product_ids: list) -> List[ProductModel]:
140
+ product_ids_str = ','.join(str(product_id) for product_id in product_ids)
141
+
142
+ records = await self.get_table_records(
143
+ table_name=self.products_table,
144
+ required_fields=self.required_fields,
145
+ projection=self.projection,
146
+ extra_where=f"({ID_FIELD},in,{product_ids_str})")
147
+ return [parse_product_data(record) for record in records]
148
+
149
+ def find_product_id_by_name(self,name: str):
150
+ for product in self.actual_products.products:
151
+ if product.name == name:
152
+ return product.id
153
+ return None # Return None if no product is found with the given name
@@ -0,0 +1,11 @@
1
+ NOCODB_CATEGORIES="Categories"
2
+ NOCODB_PRODUCTS="Products"
3
+ NOCODB_STATUSES="Order statuses"
4
+ NOCODB_BOT_MESSAGES="Bot messages"
5
+ NOCODB_ORDERS="Orders"
6
+
7
+ # NOCODB_CATEGORIES="Категории"
8
+ # NOCODB_PRODUCTS="Товары"
9
+ # NOCODB_STATUSES="Статусы заказов"
10
+ # NOCODB_BOT_MESSAGES="Сообщения бота"
11
+ # NOCODB_ORDERS="Заказы"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tgshops-integrations
3
- Version: 0.1
3
+ Version: 0.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
@@ -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=ze7e0JB0DpwVHTu8HSZSW3r884wNTy17P-e7Sk9HyNA,4979
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.2.dist-info/METADATA,sha256=1qDeJ1TrLut0tfjloxGEVFVjyU4CwQc-e_0FuFd4jZ8,2391
14
+ tgshops_integrations-0.2.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
15
+ tgshops_integrations-0.2.dist-info/top_level.txt,sha256=HFNtxqDpzmlF4ZLnMiwhbU7pOa_YozxU2zBl0bnUmcY,21
16
+ tgshops_integrations-0.2.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ tgshops_integrations
@@ -1,4 +0,0 @@
1
- tgshops_integrations-0.1.dist-info/METADATA,sha256=tmTHSs-Fv4h1eCpzSWz9ciefn0kh5kKYGOgDBaMA2TU,2391
2
- tgshops_integrations-0.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
3
- tgshops_integrations-0.1.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
- tgshops_integrations-0.1.dist-info/RECORD,,