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.
@@ -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
- # Exclude 'self' by starting args processing from args[1:]
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(self,
31
- required_fields: list = None,
32
- projection: list = None,
33
- extra_where: str = None,
34
- offset: int = None,
35
- limit: int = None) -> dict:
36
- extra_params = {}
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
- extra_params["fields"] = ','.join(projection)
40
+ params["fields"] = ','.join(projection)
39
41
  if required_fields:
40
- extra_params["where"] = ""
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
- if not extra_params.get("where"):
46
- extra_params["where"] = extra_where
47
- else:
48
- extra_params["where"] += f"~and{extra_where}"
49
- if offset:
50
- extra_params['offset'] = offset
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(self,
56
- table_name: str,
57
- required_fields: list = None,
58
- projection: list = None,
59
- extra_where: str = None,
60
- limit: int = None) -> List[dict]:
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
- extra_params = self.construct_get_params(required_fields, projection, extra_where, limit=limit)
63
- response = await self.httpx_client.get(url, params=extra_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()["list"]
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
- raise Exception(response.text)
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 item {record_id}")
126
- return response.json()
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
- # Not transport
129
- async def get_product_categories(self, table_id: str,table_name : str) -> int:
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
- extra_params = self.construct_get_params(limit=limit)
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
- categories={category[table_name] : category["Id"] for category in response.json()["list"]}
137
- return categories
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 get_all_tables(self, source: Optional[str] = None):
160
- if not source:
161
- source=self.SOURCE
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=(await self.httpx_client.get(url)).json()
165
- tables_info=response.get('list', [])
166
- self.tables_list={table["title"] : table["id"] for table in tables_info}
167
- return self.tables_list
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
- return (await self.httpx_client.get(
171
- f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/projects/")).json().get(
172
- 'list', [])
173
-
174
- async def get_table_meta(self, table_name: str):
175
- return (await self.httpx_client.get(
176
- f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}")).json()
177
-
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
- return (await self.httpx_client.post(
181
- f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/meta/tables/{table_name}/columns",
182
- json={
183
- "column_name": name,
184
- "dt": "character varying",
185
- "dtx": "specificType",
186
- "ct": "varchar(45)",
187
- "clen": 45,
188
- "dtxp": "45",
189
- "dtxs": "",
190
- "altered": 1,
191
- "uidt": "SingleLineText",
192
- "uip": "",
193
- "uicn": "",
194
- "title": name
195
- })).json()
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
- self,
199
- base_id: str,
200
- fk_model_id: str,
201
- record_id: str,
202
- source_column_id: str,
203
- linked_record_id: str) -> dict:
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
- base_id
206
- fk_model_id - ID of linked column
207
- record_id - ID of source record to be linked
208
- source_column_id -ID of source column
209
- linked_record_id - ID of linked record
210
-
211
- POST /api/v1/db/data/noco/pwb8m0yee7nvw6m/mtk2pg9eiix11qs/242/mm/ct5sskewp6sg54q/91
212
- /fk_model- smr8uvm11kurzprp
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
- self,
222
- base_id: str,
223
- fk_model_id: str,
224
- record_id: str,
225
- source_column_id: str,
226
- linked_record_id: str) -> dict:
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
- base_id
229
- fk_model_id - ID of linked column
230
- record_id - ID of source record to be linked
231
- source_column_id -ID of source column
232
- linked_record_id - ID of linked record
233
-
234
- POST /api/v1/db/data/noco/pwb8m0yee7nvw6m/mtk2pg9eiix11qs/242/mm/ct5sskewp6sg54q/91
245
+ Unlinks a record from another record in a many-to-many relationship.
235
246
  """
236
- path = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/data/noco/{base_id}/{fk_model_id}/{record_id}/mm/{source_column_id}/{linked_record_id}"
237
- response = await self.httpx_client.delete(path)
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
- self,
244
- image_url: str,
245
- image_name: str,
246
- source_column_id: str,
247
- product_table_name: str,
248
- images_column_id: str) -> dict:
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
- source
251
- fk_model_id - ID of linked column
252
- record_id - ID of source record to be linked
253
- source_column_id -ID of source column
254
- linked_record_id - ID of linked record
263
+ Saves an image to NocoDB's storage.
255
264
  """
256
265
  try:
257
266
  response = requests.get(image_url)
258
- except:
259
- logger.info(f"Error with loading image via url - {image_url}")
260
- return ""
261
-
262
- if response.status_code == 200:
263
- file = io.BytesIO(response.content)
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
- files = {'file': (image_name, file, 'image/jpeg')}
271
- url = f"{self.NOCODB_HOST.replace('/api/v2', '/api/v1')}/db/storage/upload?path=noco/{source_column_id}/{product_table_name}/{images_column_id}"
272
- timeout = httpx.Timeout(200.0)
273
- response = await self.httpx_client.post(url,files=files,headers=self.httpx_client.headers,timeout=timeout)
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]['url']
276
- else:
277
- logger.info(f"Error with posting image {image_name}, skipping it.")
278
- return ""
279
- else:
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