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.
@@ -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