tgshops-integrations 2.4__py3-none-any.whl → 3.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|