tgshops-integrations 2.4__py3-none-any.whl → 3.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.
- tgshops_integrations/middlewares/gateway.py +116 -90
- tgshops_integrations/models/products.py +2 -3
- tgshops_integrations/nocodb_connector/categories_management.py +149 -0
- tgshops_integrations/nocodb_connector/client.py +189 -181
- tgshops_integrations/nocodb_connector/model_mapping.py +93 -84
- tgshops_integrations/nocodb_connector/products_management.py +151 -0
- {tgshops_integrations-2.4.dist-info → tgshops_integrations-3.0.dist-info}/METADATA +1 -1
- tgshops_integrations-3.0.dist-info/RECORD +18 -0
- tgshops_integrations-2.4.dist-info/RECORD +0 -16
- {tgshops_integrations-2.4.dist-info → tgshops_integrations-3.0.dist-info}/WHEEL +0 -0
- {tgshops_integrations-2.4.dist-info → tgshops_integrations-3.0.dist-info}/top_level.txt +0 -0
@@ -1,70 +1,72 @@
|
|
1
|
-
from typing import List,Optional
|
2
|
-
|
1
|
+
from typing import List, Optional, Dict, Union
|
3
2
|
import httpx
|
4
3
|
import requests
|
5
4
|
import io
|
6
|
-
|
7
5
|
from loguru import logger
|
8
|
-
|
9
6
|
from tgshops_integrations.nocodb_connector.model_mapping import ID_FIELD
|
10
7
|
|
11
8
|
|
12
|
-
def custom_key_builder(func, *args, **kwargs):
|
13
|
-
|
9
|
+
def custom_key_builder(func, *args, **kwargs) -> str:
|
10
|
+
"""
|
11
|
+
Custom key builder for caching.
|
12
|
+
Excludes 'self' by starting args processing from args[1:].
|
13
|
+
"""
|
14
14
|
args_key_part = "-".join(str(arg) for arg in args[1:])
|
15
15
|
kwargs_key_part = "-".join(f"{key}-{value}" for key, value in sorted(kwargs.items()))
|
16
16
|
return f"{func.__name__}-{args_key_part}-{kwargs_key_part}"
|
17
17
|
|
18
18
|
|
19
19
|
class NocodbClient:
|
20
|
-
|
21
|
-
def __init__(self,NOCODB_HOST=None,NOCODB_API_KEY=None,SOURCE=None):
|
20
|
+
def __init__(self, NOCODB_HOST: Optional[str] = None, NOCODB_API_KEY: Optional[str] = None, SOURCE: Optional[str] = None):
|
22
21
|
self.NOCODB_HOST = NOCODB_HOST
|
23
22
|
self.NOCODB_API_KEY = NOCODB_API_KEY
|
24
|
-
self.SOURCE=SOURCE
|
23
|
+
self.SOURCE = SOURCE
|
25
24
|
self.httpx_client = httpx.AsyncClient(timeout=60.0)
|
26
|
-
self.httpx_client.headers = {
|
27
|
-
"xc-token": self.NOCODB_API_KEY
|
28
|
-
}
|
25
|
+
self.httpx_client.headers = {"xc-token": self.NOCODB_API_KEY}
|
29
26
|
|
30
|
-
def construct_get_params(
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
def construct_get_params(
|
28
|
+
self,
|
29
|
+
required_fields: Optional[List[str]] = None,
|
30
|
+
projection: Optional[List[str]] = None,
|
31
|
+
extra_where: Optional[str] = None,
|
32
|
+
offset: Optional[int] = None,
|
33
|
+
limit: Optional[int] = None
|
34
|
+
) -> Dict[str, Union[str, int]]:
|
35
|
+
"""
|
36
|
+
Constructs GET parameters for API requests.
|
37
|
+
"""
|
38
|
+
params = {}
|
37
39
|
if projection:
|
38
|
-
|
40
|
+
params["fields"] = ','.join(projection)
|
39
41
|
if required_fields:
|
40
|
-
|
41
|
-
for field in required_fields:
|
42
|
-
extra_params["where"] += f"({field},isnot,null)~and"
|
43
|
-
extra_params["where"] = extra_params["where"].rstrip("~and")
|
42
|
+
params["where"] = "~and".join(f"({field},isnot,null)" for field in required_fields)
|
44
43
|
if extra_where:
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
if limit:
|
52
|
-
extra_params["limit"] = limit
|
53
|
-
return extra_params
|
44
|
+
params["where"] = f"{params.get('where', '')}~and{extra_where}" if params.get("where") else extra_where
|
45
|
+
if offset is not None:
|
46
|
+
params["offset"] = offset
|
47
|
+
if limit is not None:
|
48
|
+
params["limit"] = limit
|
49
|
+
return params
|
54
50
|
|
55
|
-
async def get_table_records(
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
51
|
+
async def get_table_records(
|
52
|
+
self,
|
53
|
+
table_name: str,
|
54
|
+
required_fields: Optional[List[str]] = None,
|
55
|
+
projection: Optional[List[str]] = None,
|
56
|
+
extra_where: Optional[str] = None,
|
57
|
+
limit: Optional[int] = None
|
58
|
+
) -> List[dict]:
|
59
|
+
"""
|
60
|
+
Fetches records from a specified table.
|
61
|
+
"""
|
61
62
|
url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
|
62
|
-
|
63
|
-
response = await self.httpx_client.get(url, params=
|
63
|
+
params = self.construct_get_params(required_fields, projection, extra_where, limit=limit)
|
64
|
+
response = await self.httpx_client.get(url, params=params)
|
64
65
|
if response.status_code == 200:
|
65
|
-
return response.json()
|
66
|
+
return response.json().get("list", [])
|
67
|
+
logger.error(f"Error fetching records: {response.text}")
|
66
68
|
raise Exception(response.text)
|
67
|
-
|
69
|
+
|
68
70
|
async def get_table_records_v2(self,
|
69
71
|
table_name: str,
|
70
72
|
required_fields: list = None,
|
@@ -92,189 +94,195 @@ class NocodbClient:
|
|
92
94
|
raise Exception(response.text)
|
93
95
|
|
94
96
|
async def create_table_record(self, table_name: str, record: dict) -> dict:
|
97
|
+
"""
|
98
|
+
Creates a new record in a table.
|
99
|
+
"""
|
95
100
|
url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
|
96
101
|
response = await self.httpx_client.post(url, json=record)
|
97
102
|
if response.status_code == 200:
|
98
|
-
record["id"] = response.json().get("id")
|
99
|
-
if not record["id"]:
|
100
|
-
record["id"] = response.json().get("Id")
|
103
|
+
record["id"] = response.json().get("id") or response.json().get("Id")
|
101
104
|
return record
|
102
|
-
|
103
|
-
|
104
|
-
async def count_table_records(self, table_name: str) -> int:
|
105
|
-
url = f"{self.NOCODB_HOST}/tables/{table_name}/records/count"
|
106
|
-
response = await self.httpx_client.get(url)
|
107
|
-
if response.status_code == 200:
|
108
|
-
return response.json().get("count", 0)
|
109
|
-
raise Exception(response.text)
|
110
|
-
|
111
|
-
async def update_table_record(self, table_name: str, record_id: str, updated_data: dict) -> bool:
|
112
|
-
url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
|
113
|
-
updated_data[ID_FIELD] = int(record_id)
|
114
|
-
if updated_data["ID"]:
|
115
|
-
updated_data.pop("ID")
|
116
|
-
response = await self.httpx_client.patch(url, json=updated_data)
|
117
|
-
if response.status_code == 200:
|
118
|
-
return True
|
105
|
+
logger.error(f"Error creating record: {response.text}")
|
119
106
|
raise Exception(response.text)
|
120
107
|
|
121
108
|
async def delete_table_record(self, table_name: str, record_id: str) -> dict:
|
109
|
+
"""
|
110
|
+
Deletes a record from a specified table.
|
111
|
+
"""
|
122
112
|
url = f"{self.NOCODB_HOST}/tables/{table_name}/records"
|
123
113
|
response = requests.delete(url, json={"Id": record_id}, headers=self.httpx_client.headers)
|
124
114
|
if response.status_code == 200:
|
125
|
-
logger.info(f"Deleted
|
126
|
-
|
115
|
+
logger.info(f"Deleted record {record_id}")
|
116
|
+
return response.json()
|
117
|
+
logger.error(f"Error deleting record: {response.text}")
|
118
|
+
raise Exception(response.text)
|
127
119
|
|
128
|
-
|
129
|
-
|
120
|
+
async def get_product_categories(self, table_id: str, table_name: str) -> Dict[str, str]:
|
121
|
+
"""
|
122
|
+
Fetches product categories from a specified table.
|
123
|
+
"""
|
130
124
|
url = f"{self.NOCODB_HOST}/tables/{table_id}/records"
|
131
|
-
limit=75
|
132
|
-
|
133
|
-
response = await self.httpx_client.get(url, params=extra_params)
|
134
|
-
|
125
|
+
params = self.construct_get_params(limit=75)
|
126
|
+
response = await self.httpx_client.get(url, params=params)
|
135
127
|
if response.status_code == 200:
|
136
|
-
|
137
|
-
|
128
|
+
return {category[table_name]: category["Id"] for category in response.json().get("list", [])}
|
129
|
+
logger.error(f"Error fetching categories: {response.text}")
|
138
130
|
raise Exception(response.text)
|
139
|
-
return {}
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
async def create_product_category(self, table_id: str, category_name : str, table_name : str, category_id : int = 0) -> dict:
|
144
|
-
url = f"{self.NOCODB_HOST}/tables/{table_id}/records"
|
145
|
-
|
146
|
-
record={table_name: category_name, "Id" : category_id}
|
147
131
|
|
132
|
+
async def create_product_category(
|
133
|
+
self, table_id: str, category_name: str, table_name: str, category_id: int = 0
|
134
|
+
) -> dict:
|
135
|
+
"""
|
136
|
+
Creates a new product category in a specified table.
|
137
|
+
"""
|
138
|
+
url = f"{self.NOCODB_HOST}/tables/{table_id}/records"
|
139
|
+
record = {table_name: category_name, "Id": category_id}
|
148
140
|
response = await self.httpx_client.post(url, json=record)
|
149
141
|
if response.status_code == 200:
|
150
|
-
self.categories = await self.get_product_categories(table_id=table_id, table_name=table_name)
|
151
142
|
return record
|
143
|
+
logger.error(f"Error creating product category: {response.text}")
|
152
144
|
raise Exception(response.text)
|
153
|
-
|
154
|
-
async def get_table_meta(self, table_name: str):
|
155
|
-
return (await self.httpx_client.get(
|
156
|
-
f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}")).json()
|
157
|
-
|
158
145
|
|
159
|
-
async def
|
160
|
-
|
161
|
-
|
146
|
+
async def get_table_meta(self, table_name: str) -> dict:
|
147
|
+
"""
|
148
|
+
Fetches metadata of a table.
|
149
|
+
"""
|
150
|
+
url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}"
|
151
|
+
response = await self.httpx_client.get(url)
|
152
|
+
if response.status_code == 200:
|
153
|
+
return response.json()
|
154
|
+
logger.error(f"Error fetching table metadata: {response.text}")
|
155
|
+
raise Exception(response.text)
|
162
156
|
|
157
|
+
async def get_all_tables(self, source: Optional[str] = None) -> Dict[str, str]:
|
158
|
+
"""
|
159
|
+
Fetches all tables from the specified project source.
|
160
|
+
"""
|
161
|
+
source = source or self.SOURCE
|
163
162
|
url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/projects/{source}/tables?includeM2M=false"
|
164
|
-
response=
|
165
|
-
|
166
|
-
|
167
|
-
|
163
|
+
response = await self.httpx_client.get(url)
|
164
|
+
if response.status_code == 200:
|
165
|
+
tables_info = response.json().get('list', [])
|
166
|
+
self.tables_list = {table["title"]: table["id"] for table in tables_info}
|
167
|
+
return self.tables_list
|
168
|
+
logger.error(f"Failed to fetch tables: {response.text}")
|
169
|
+
raise Exception(response.text)
|
168
170
|
|
169
|
-
async def get_sources(self):
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
171
|
+
async def get_sources(self) -> list:
|
172
|
+
"""
|
173
|
+
Fetches all project sources.
|
174
|
+
"""
|
175
|
+
url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/projects/"
|
176
|
+
response = await self.httpx_client.get(url)
|
177
|
+
if response.status_code == 200:
|
178
|
+
return response.json().get('list', [])
|
179
|
+
logger.error(f"Failed to fetch sources: {response.text}")
|
180
|
+
raise Exception(response.text)
|
181
|
+
|
182
|
+
async def get_table_meta(self, table_name: str) -> dict:
|
183
|
+
"""
|
184
|
+
Fetches metadata of a specified table.
|
185
|
+
"""
|
186
|
+
url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}"
|
187
|
+
response = await self.httpx_client.get(url)
|
188
|
+
if response.status_code == 200:
|
189
|
+
return response.json()
|
190
|
+
logger.error(f"Failed to fetch table metadata: {response.text}")
|
191
|
+
raise Exception(response.text)
|
178
192
|
|
179
|
-
async def create_table_column(self, table_name: str, name: str):
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
193
|
+
async def create_table_column(self, table_name: str, name: str) -> dict:
|
194
|
+
"""
|
195
|
+
Creates a new column in the specified table.
|
196
|
+
"""
|
197
|
+
url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}/columns"
|
198
|
+
payload = {
|
199
|
+
"column_name": name,
|
200
|
+
"dt": "character varying",
|
201
|
+
"dtx": "specificType",
|
202
|
+
"ct": "varchar(45)",
|
203
|
+
"clen": 45,
|
204
|
+
"dtxp": "45",
|
205
|
+
"dtxs": "",
|
206
|
+
"altered": 1,
|
207
|
+
"uidt": "SingleLineText",
|
208
|
+
"uip": "",
|
209
|
+
"uicn": "",
|
210
|
+
"title": name
|
211
|
+
}
|
212
|
+
response = await self.httpx_client.post(url, json=payload)
|
213
|
+
if response.status_code == 200:
|
214
|
+
return response.json()
|
215
|
+
logger.error(f"Failed to create table column: {response.text}")
|
216
|
+
raise Exception(response.text)
|
196
217
|
|
197
218
|
async def link_table_record(
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
219
|
+
self,
|
220
|
+
base_id: str,
|
221
|
+
fk_model_id: str,
|
222
|
+
record_id: str,
|
223
|
+
source_column_id: str,
|
224
|
+
linked_record_id: str
|
225
|
+
) -> dict:
|
204
226
|
"""
|
205
|
-
|
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
|
227
|
+
Links a record to another record in a many-to-many relationship.
|
213
228
|
"""
|
214
229
|
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)
|
230
|
+
response = await self.httpx_client.post(url, headers=self.httpx_client.headers)
|
216
231
|
if response.status_code == 200:
|
217
232
|
return response.json()
|
233
|
+
logger.error(f"Failed to link table record: {response.text}")
|
218
234
|
raise Exception(response.text)
|
219
235
|
|
220
236
|
async def unlink_table_record(
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
237
|
+
self,
|
238
|
+
base_id: str,
|
239
|
+
fk_model_id: str,
|
240
|
+
record_id: str,
|
241
|
+
source_column_id: str,
|
242
|
+
linked_record_id: str
|
243
|
+
) -> dict:
|
227
244
|
"""
|
228
|
-
|
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
|
245
|
+
Unlinks a record from another record in a many-to-many relationship.
|
235
246
|
"""
|
236
|
-
|
237
|
-
response = await self.httpx_client.delete(
|
247
|
+
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}"
|
248
|
+
response = await self.httpx_client.delete(url)
|
238
249
|
if response.status_code == 200:
|
239
250
|
return response.json()
|
251
|
+
logger.error(f"Failed to unlink table record: {response.text}")
|
240
252
|
raise Exception(response.text)
|
241
253
|
|
242
254
|
async def save_image_to_nocodb(
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
255
|
+
self,
|
256
|
+
image_url: str,
|
257
|
+
image_name: str,
|
258
|
+
source_column_id: str,
|
259
|
+
product_table_name: str,
|
260
|
+
images_column_id: str
|
261
|
+
) -> Optional[str]:
|
249
262
|
"""
|
250
|
-
|
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
|
263
|
+
Saves an image to NocoDB's storage.
|
255
264
|
"""
|
256
265
|
try:
|
257
266
|
response = requests.get(image_url)
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
else:
|
265
|
-
raise Exception(f"Failed to fetch the image. Status code: {response.status_code}")
|
266
|
-
|
267
|
+
response.raise_for_status()
|
268
|
+
except requests.RequestException as e:
|
269
|
+
logger.error(f"Error loading image from URL {image_url}: {e}")
|
270
|
+
return None
|
271
|
+
|
272
|
+
file = io.BytesIO(response.content)
|
267
273
|
file_size = file.getbuffer().nbytes
|
268
274
|
|
269
|
-
if file_size:
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
275
|
+
if not file_size:
|
276
|
+
logger.error(f"Image file from {image_url} is empty.")
|
277
|
+
return None
|
278
|
+
|
279
|
+
files = {'file': (image_name, file, 'image/jpeg')}
|
280
|
+
url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/storage/upload?path=noco/{source_column_id}/{product_table_name}/{images_column_id}"
|
281
|
+
try:
|
282
|
+
response = await self.httpx_client.post(url, files=files, timeout=httpx.Timeout(200.0))
|
274
283
|
if response.status_code == 200:
|
275
|
-
return response.json()[0]
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
return ""
|
284
|
+
return response.json()[0].get('url', None)
|
285
|
+
logger.error(f"Error saving image {image_name}: {response.text}")
|
286
|
+
except Exception as e:
|
287
|
+
logger.error(f"Unexpected error saving image {image_name}: {e}")
|
288
|
+
return None
|