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.
- tgshops_integrations/__init__.py +0 -0
- tgshops_integrations/middlewares/__init__.py +0 -0
- tgshops_integrations/middlewares/gateway.py +111 -0
- tgshops_integrations/models/__init__.py +0 -0
- tgshops_integrations/models/categories.py +43 -0
- tgshops_integrations/models/products.py +50 -0
- tgshops_integrations/nocodb_connector/__init__.py +0 -0
- tgshops_integrations/nocodb_connector/categories.py +102 -0
- tgshops_integrations/nocodb_connector/client.py +186 -0
- tgshops_integrations/nocodb_connector/model_mapping.py +166 -0
- tgshops_integrations/nocodb_connector/products.py +153 -0
- tgshops_integrations/nocodb_connector/tables.py +11 -0
- {tgshops_integrations-0.1.dist-info → tgshops_integrations-0.2.dist-info}/METADATA +1 -1
- tgshops_integrations-0.2.dist-info/RECORD +16 -0
- tgshops_integrations-0.2.dist-info/top_level.txt +1 -0
- tgshops_integrations-0.1.dist-info/RECORD +0 -4
- tgshops_integrations-0.1.dist-info/top_level.txt +0 -1
- {tgshops_integrations-0.1.dist-info → tgshops_integrations-0.2.dist-info}/WHEEL +0 -0
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.
|
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,,
|
@@ -1 +0,0 @@
|
|
1
|
-
|
File without changes
|