konecty_sdk_python 2.1.1__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.
- KonectySdkPython/__init__.py +41 -0
- KonectySdkPython/cli/__init__.py +38 -0
- KonectySdkPython/cli/apply.py +555 -0
- KonectySdkPython/cli/backup.py +161 -0
- KonectySdkPython/cli/pull.py +309 -0
- KonectySdkPython/lib/__init__.py +43 -0
- KonectySdkPython/lib/client.py +950 -0
- KonectySdkPython/lib/exceptions.py +26 -0
- KonectySdkPython/lib/feature_types/__init__.py +46 -0
- KonectySdkPython/lib/feature_types/cross_module_query.py +78 -0
- KonectySdkPython/lib/feature_types/kpi.py +21 -0
- KonectySdkPython/lib/feature_types/query_json.py +242 -0
- KonectySdkPython/lib/file_manager.py +248 -0
- KonectySdkPython/lib/filters.py +155 -0
- KonectySdkPython/lib/http.py +95 -0
- KonectySdkPython/lib/model.py +88 -0
- KonectySdkPython/lib/serialization.py +13 -0
- KonectySdkPython/lib/services/__init__.py +5 -0
- KonectySdkPython/lib/services/aggregation.py +142 -0
- KonectySdkPython/lib/services/auth.py +105 -0
- KonectySdkPython/lib/services/base.py +63 -0
- KonectySdkPython/lib/services/change_user.py +64 -0
- KonectySdkPython/lib/services/comments.py +89 -0
- KonectySdkPython/lib/services/export.py +54 -0
- KonectySdkPython/lib/services/files.py +59 -0
- KonectySdkPython/lib/services/notifications.py +41 -0
- KonectySdkPython/lib/services/query.py +192 -0
- KonectySdkPython/lib/services/stream.py +133 -0
- KonectySdkPython/lib/services/subscriptions.py +26 -0
- KonectySdkPython/lib/settings.py +152 -0
- KonectySdkPython/lib/types.py +418 -0
- konecty_sdk_python-2.1.1.dist-info/METADATA +155 -0
- konecty_sdk_python-2.1.1.dist-info/RECORD +35 -0
- konecty_sdk_python-2.1.1.dist-info/WHEEL +4 -0
- konecty_sdk_python-2.1.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
"""Módulo para gerenciar configurações do Konecty."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union, cast
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
|
|
10
|
+
from .exceptions import KonectyAPIError, KonectyError, KonectyValidationError
|
|
11
|
+
from .feature_types.kpi import KpiConfig
|
|
12
|
+
from .file_manager import FileManager
|
|
13
|
+
from .filters import KonectyFilter, KonectyFindParams
|
|
14
|
+
from .http import StreamResponse
|
|
15
|
+
from .http import request as _http_request
|
|
16
|
+
from .serialization import json_serial
|
|
17
|
+
from .services.aggregation import AggregationService
|
|
18
|
+
from .services.auth import AuthService
|
|
19
|
+
from .services.change_user import ChangeUserService
|
|
20
|
+
from .services.comments import CommentsService
|
|
21
|
+
from .services.export import ExportService
|
|
22
|
+
from .services.files import FilesService
|
|
23
|
+
from .services.notifications import NotificationsService
|
|
24
|
+
from .services.query import QueryResult, QueryService
|
|
25
|
+
from .services.stream import FindStreamResult, StreamService
|
|
26
|
+
from .services.subscriptions import SubscriptionsService
|
|
27
|
+
from .types import KonectyDateTime, KonectyDict, KonectyUpdateId
|
|
28
|
+
|
|
29
|
+
# Configura o logger do urllib3 para mostrar apenas erros
|
|
30
|
+
logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
KONECTY_UPDATE_IGNORE_FIELDS = [
|
|
35
|
+
"_id",
|
|
36
|
+
"code",
|
|
37
|
+
"_updatedAt",
|
|
38
|
+
"_createdAt",
|
|
39
|
+
"_updatedBy",
|
|
40
|
+
"_createdBy",
|
|
41
|
+
]
|
|
42
|
+
KONECTY_CREATE_IGNORE_FIELDS = ["_updatedAt", "_createdAt", "_updatedBy", "_createdBy"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_first_dict(items: List[Any]) -> Optional[KonectyDict]:
|
|
46
|
+
"""Retorna o primeiro item de uma lista como dicionário ou None se estiver vazia."""
|
|
47
|
+
if not items:
|
|
48
|
+
return None
|
|
49
|
+
first = items[0]
|
|
50
|
+
if isinstance(first, dict):
|
|
51
|
+
return cast(KonectyDict, first)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class KonectyClient:
|
|
56
|
+
def __init__(self, base_url: str, token: str = "") -> None:
|
|
57
|
+
self.base_url = base_url
|
|
58
|
+
self.headers = {"Authorization": f"{token}"}
|
|
59
|
+
self.file_manager = FileManager(base_url=base_url, headers=self.headers)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def auth_id(self) -> Optional[str]:
|
|
63
|
+
"""Current session token (authId) sent in the Authorization header, or None."""
|
|
64
|
+
return self.headers.get("Authorization") or None
|
|
65
|
+
|
|
66
|
+
def set_auth_id(self, auth_id: str) -> None:
|
|
67
|
+
"""Adopt a session token so following requests are authenticated."""
|
|
68
|
+
# headers is shared with file_manager: mutate in place so both see the token.
|
|
69
|
+
self.headers["Authorization"] = auth_id
|
|
70
|
+
|
|
71
|
+
async def _request(
|
|
72
|
+
self,
|
|
73
|
+
method: str,
|
|
74
|
+
path: str,
|
|
75
|
+
*,
|
|
76
|
+
params: Optional[Dict[str, Any]] = None,
|
|
77
|
+
json: Optional[Dict[str, Any]] = None,
|
|
78
|
+
stream: bool = False,
|
|
79
|
+
return_bytes: bool = False,
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""Internal HTTP request. Used by client methods and services."""
|
|
82
|
+
return await _http_request(
|
|
83
|
+
self,
|
|
84
|
+
method,
|
|
85
|
+
path,
|
|
86
|
+
params=params,
|
|
87
|
+
json=json,
|
|
88
|
+
stream=stream,
|
|
89
|
+
return_bytes=return_bytes,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def _stream(self) -> StreamService:
|
|
94
|
+
if not hasattr(self, "_stream_service"):
|
|
95
|
+
self._stream_service = StreamService(self)
|
|
96
|
+
return self._stream_service
|
|
97
|
+
|
|
98
|
+
async def find_stream(
|
|
99
|
+
self,
|
|
100
|
+
module: str,
|
|
101
|
+
options: KonectyFindParams,
|
|
102
|
+
*,
|
|
103
|
+
include_total: bool = False,
|
|
104
|
+
) -> FindStreamResult:
|
|
105
|
+
"""
|
|
106
|
+
Stream records as NDJSON. Returns result with .stream (async generator) and .total (when include_total).
|
|
107
|
+
|
|
108
|
+
Example:
|
|
109
|
+
result = await client.find_stream("Contact", options, include_total=True)
|
|
110
|
+
async for record in result.stream:
|
|
111
|
+
...
|
|
112
|
+
total = result.total
|
|
113
|
+
"""
|
|
114
|
+
return await self._stream.find_stream(
|
|
115
|
+
module, options, include_total=include_total
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def count_stream(
|
|
119
|
+
self,
|
|
120
|
+
module: str,
|
|
121
|
+
filter_params: Optional[KonectyFilter] = None,
|
|
122
|
+
**kwargs: Any,
|
|
123
|
+
) -> int:
|
|
124
|
+
"""Return total count for the module (GET /rest/stream/{module}/count)."""
|
|
125
|
+
return await self._stream.count_stream(module, filter_params, **kwargs)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def _files(self) -> FilesService:
|
|
129
|
+
if not hasattr(self, "_files_service"):
|
|
130
|
+
self._files_service = FilesService(self)
|
|
131
|
+
return self._files_service
|
|
132
|
+
|
|
133
|
+
async def download_file(
|
|
134
|
+
self,
|
|
135
|
+
module: str,
|
|
136
|
+
record_code: str,
|
|
137
|
+
field_name: str,
|
|
138
|
+
file_name: str,
|
|
139
|
+
) -> bytes:
|
|
140
|
+
"""Download a file from a record field. Returns bytes."""
|
|
141
|
+
return await self._files.download_file(
|
|
142
|
+
module, record_code, field_name, file_name
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
async def download_image(
|
|
146
|
+
self,
|
|
147
|
+
module: str,
|
|
148
|
+
record_id: str,
|
|
149
|
+
field_name: str,
|
|
150
|
+
file_name: str,
|
|
151
|
+
*,
|
|
152
|
+
style: Optional[Literal["full", "thumb", "wm"]] = None,
|
|
153
|
+
) -> bytes:
|
|
154
|
+
"""Download an image (optional style: full, thumb, wm). Returns bytes."""
|
|
155
|
+
return await self._files.download_image(
|
|
156
|
+
module, record_id, field_name, file_name, style=style
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def _aggregation(self) -> AggregationService:
|
|
161
|
+
if not hasattr(self, "_aggregation_service"):
|
|
162
|
+
self._aggregation_service = AggregationService(self)
|
|
163
|
+
return self._aggregation_service
|
|
164
|
+
|
|
165
|
+
async def get_kpi(
|
|
166
|
+
self,
|
|
167
|
+
module: str,
|
|
168
|
+
kpi_config: KpiConfig,
|
|
169
|
+
*,
|
|
170
|
+
filter_params: Optional[KonectyFilter] = None,
|
|
171
|
+
**kwargs: Any,
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Run KPI aggregation. Returns dict with value and count."""
|
|
174
|
+
return await self._aggregation.get_kpi(
|
|
175
|
+
module, kpi_config, filter_params=filter_params, **kwargs
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def get_graph(
|
|
179
|
+
self,
|
|
180
|
+
module: str,
|
|
181
|
+
graph_config: Any,
|
|
182
|
+
*,
|
|
183
|
+
filter_params: Optional[KonectyFilter] = None,
|
|
184
|
+
**kwargs: Any,
|
|
185
|
+
) -> str:
|
|
186
|
+
"""Get graph as SVG string. graph_config: type, xAxis, yAxis, series, etc."""
|
|
187
|
+
return await self._aggregation.get_graph(
|
|
188
|
+
module, graph_config, filter_params=filter_params, **kwargs
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
async def get_pivot(
|
|
192
|
+
self,
|
|
193
|
+
module: str,
|
|
194
|
+
pivot_config: Any,
|
|
195
|
+
*,
|
|
196
|
+
filter_params: Optional[KonectyFilter] = None,
|
|
197
|
+
**kwargs: Any,
|
|
198
|
+
) -> Dict[str, Any]:
|
|
199
|
+
"""Get pivot table result. pivot_config: rows, columns, values."""
|
|
200
|
+
return await self._aggregation.get_pivot(
|
|
201
|
+
module, pivot_config, filter_params=filter_params, **kwargs
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def _export(self) -> ExportService:
|
|
206
|
+
if not hasattr(self, "_export_service"):
|
|
207
|
+
self._export_service = ExportService(self)
|
|
208
|
+
return self._export_service
|
|
209
|
+
|
|
210
|
+
async def export_list(
|
|
211
|
+
self,
|
|
212
|
+
module: str,
|
|
213
|
+
list_name: str,
|
|
214
|
+
format: Literal["csv", "xlsx", "json", "xls"],
|
|
215
|
+
*,
|
|
216
|
+
filter_params: Optional[KonectyFilter] = None,
|
|
217
|
+
**kwargs: Any,
|
|
218
|
+
) -> bytes:
|
|
219
|
+
"""Export list as CSV, XLSX, or JSON. Returns file bytes."""
|
|
220
|
+
return await self._export.export_list(
|
|
221
|
+
module, list_name, format, filter_params=filter_params, **kwargs
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def _comments(self) -> CommentsService:
|
|
226
|
+
if not hasattr(self, "_comments_service"):
|
|
227
|
+
self._comments_service = CommentsService(self)
|
|
228
|
+
return self._comments_service
|
|
229
|
+
|
|
230
|
+
async def get_comments(self, module: str, data_id: str) -> Any:
|
|
231
|
+
"""Get comments for a record."""
|
|
232
|
+
return await self._comments.get_comments(module, data_id)
|
|
233
|
+
|
|
234
|
+
async def create_comment(
|
|
235
|
+
self, module: str, data_id: str, text: str, parent_id: Optional[str] = None
|
|
236
|
+
) -> Any:
|
|
237
|
+
"""Create a comment (optionally reply via parent_id)."""
|
|
238
|
+
return await self._comments.create_comment(
|
|
239
|
+
module, data_id, text, parent_id=parent_id
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
async def update_comment(
|
|
243
|
+
self, module: str, data_id: str, comment_id: str, text: str
|
|
244
|
+
) -> Any:
|
|
245
|
+
"""Update a comment."""
|
|
246
|
+
return await self._comments.update_comment(module, data_id, comment_id, text)
|
|
247
|
+
|
|
248
|
+
async def delete_comment(self, module: str, data_id: str, comment_id: str) -> Any:
|
|
249
|
+
"""Delete (soft) a comment."""
|
|
250
|
+
return await self._comments.delete_comment(module, data_id, comment_id)
|
|
251
|
+
|
|
252
|
+
async def search_comment_users(
|
|
253
|
+
self, module: str, data_id: str, query: str = ""
|
|
254
|
+
) -> Any:
|
|
255
|
+
"""Search users for @mention autocomplete."""
|
|
256
|
+
return await self._comments.search_comment_users(module, data_id, query)
|
|
257
|
+
|
|
258
|
+
async def search_comments(
|
|
259
|
+
self,
|
|
260
|
+
module: str,
|
|
261
|
+
data_id: str,
|
|
262
|
+
*,
|
|
263
|
+
query: Optional[str] = None,
|
|
264
|
+
author_id: Optional[str] = None,
|
|
265
|
+
start_date: Optional[str] = None,
|
|
266
|
+
end_date: Optional[str] = None,
|
|
267
|
+
page: Optional[int] = None,
|
|
268
|
+
limit: Optional[int] = None,
|
|
269
|
+
) -> Any:
|
|
270
|
+
"""Search comments with filters."""
|
|
271
|
+
return await self._comments.search_comments(
|
|
272
|
+
module,
|
|
273
|
+
data_id,
|
|
274
|
+
query=query,
|
|
275
|
+
author_id=author_id,
|
|
276
|
+
start_date=start_date,
|
|
277
|
+
end_date=end_date,
|
|
278
|
+
page=page,
|
|
279
|
+
limit=limit,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def _subscriptions(self) -> SubscriptionsService:
|
|
284
|
+
if not hasattr(self, "_subscriptions_service"):
|
|
285
|
+
self._subscriptions_service = SubscriptionsService(self)
|
|
286
|
+
return self._subscriptions_service
|
|
287
|
+
|
|
288
|
+
async def get_subscription_status(self, module: str, data_id: str) -> Any:
|
|
289
|
+
"""Get subscription status for a record."""
|
|
290
|
+
return await self._subscriptions.get_subscription_status(module, data_id)
|
|
291
|
+
|
|
292
|
+
async def subscribe(self, module: str, data_id: str) -> Any:
|
|
293
|
+
"""Subscribe to record notifications."""
|
|
294
|
+
return await self._subscriptions.subscribe(module, data_id)
|
|
295
|
+
|
|
296
|
+
async def unsubscribe(self, module: str, data_id: str) -> Any:
|
|
297
|
+
"""Unsubscribe from record."""
|
|
298
|
+
return await self._subscriptions.unsubscribe(module, data_id)
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def _notifications(self) -> NotificationsService:
|
|
302
|
+
if not hasattr(self, "_notifications_service"):
|
|
303
|
+
self._notifications_service = NotificationsService(self)
|
|
304
|
+
return self._notifications_service
|
|
305
|
+
|
|
306
|
+
async def list_notifications(
|
|
307
|
+
self,
|
|
308
|
+
*,
|
|
309
|
+
read: Optional[bool] = None,
|
|
310
|
+
page: Optional[int] = None,
|
|
311
|
+
limit: Optional[int] = None,
|
|
312
|
+
) -> Any:
|
|
313
|
+
"""List user notifications."""
|
|
314
|
+
return await self._notifications.list_notifications(
|
|
315
|
+
read=read, page=page, limit=limit
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def get_unread_notifications_count(self) -> Any:
|
|
319
|
+
"""Get unread notifications count."""
|
|
320
|
+
return await self._notifications.get_unread_count()
|
|
321
|
+
|
|
322
|
+
async def mark_notification_read(self, notification_id: str) -> Any:
|
|
323
|
+
"""Mark a notification as read."""
|
|
324
|
+
return await self._notifications.mark_notification_read(notification_id)
|
|
325
|
+
|
|
326
|
+
async def mark_all_notifications_read(self) -> Any:
|
|
327
|
+
"""Mark all notifications as read."""
|
|
328
|
+
return await self._notifications.mark_all_notifications_read()
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def _change_user(self) -> ChangeUserService:
|
|
332
|
+
if not hasattr(self, "_change_user_service"):
|
|
333
|
+
self._change_user_service = ChangeUserService(self)
|
|
334
|
+
return self._change_user_service
|
|
335
|
+
|
|
336
|
+
async def change_user_add(self, module: str, ids: List[Any], users: Any) -> Any:
|
|
337
|
+
"""Add users to records."""
|
|
338
|
+
return await self._change_user.add_users(module, ids, users)
|
|
339
|
+
|
|
340
|
+
async def change_user_remove(self, module: str, ids: List[Any], users: Any) -> Any:
|
|
341
|
+
"""Remove users from records."""
|
|
342
|
+
return await self._change_user.remove_users(module, ids, users)
|
|
343
|
+
|
|
344
|
+
async def change_user_define(self, module: str, ids: List[Any], users: Any) -> Any:
|
|
345
|
+
"""Define users on records."""
|
|
346
|
+
return await self._change_user.define_users(module, ids, users)
|
|
347
|
+
|
|
348
|
+
async def change_user_replace(
|
|
349
|
+
self,
|
|
350
|
+
module: str,
|
|
351
|
+
ids: List[Any],
|
|
352
|
+
*,
|
|
353
|
+
from_user: Any = None,
|
|
354
|
+
to_user: Any = None,
|
|
355
|
+
) -> Any:
|
|
356
|
+
"""Replace user on records."""
|
|
357
|
+
return await self._change_user.replace_users(
|
|
358
|
+
module, ids, from_user=from_user, to_user=to_user
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
async def change_user_count_inactive(self, module: str, ids: List[Any]) -> Any:
|
|
362
|
+
"""Count inactive users on records."""
|
|
363
|
+
return await self._change_user.count_inactive(module, ids)
|
|
364
|
+
|
|
365
|
+
async def change_user_remove_inactive(self, module: str, ids: List[Any]) -> Any:
|
|
366
|
+
"""Remove inactive users from records."""
|
|
367
|
+
return await self._change_user.remove_inactive(module, ids)
|
|
368
|
+
|
|
369
|
+
async def change_user_set_queue(
|
|
370
|
+
self, module: str, ids: List[Any], queue: Any
|
|
371
|
+
) -> Any:
|
|
372
|
+
"""Set queue on records."""
|
|
373
|
+
return await self._change_user.set_queue(module, ids, queue)
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def _auth(self) -> AuthService:
|
|
377
|
+
if not hasattr(self, "_auth_service"):
|
|
378
|
+
self._auth_service = AuthService(self)
|
|
379
|
+
return self._auth_service
|
|
380
|
+
|
|
381
|
+
def google_login_url(
|
|
382
|
+
self,
|
|
383
|
+
client_id: str,
|
|
384
|
+
redirect_uri: Optional[str] = None,
|
|
385
|
+
state: Optional[str] = None,
|
|
386
|
+
) -> str:
|
|
387
|
+
"""Build the URL of GET /api/auth/google/start. Pure, performs no request."""
|
|
388
|
+
return self._auth.google_login_url(
|
|
389
|
+
client_id, redirect_uri=redirect_uri, state=state
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
async def exchange_google_code(
|
|
393
|
+
self,
|
|
394
|
+
code: str,
|
|
395
|
+
*,
|
|
396
|
+
geolocation: Optional[Union[Dict[str, Any], str]] = None,
|
|
397
|
+
resolution: Optional[Union[Dict[str, Any], str]] = None,
|
|
398
|
+
source: Optional[str] = None,
|
|
399
|
+
fingerprint: Optional[str] = None,
|
|
400
|
+
) -> Dict[str, Any]:
|
|
401
|
+
"""
|
|
402
|
+
Exchange the single-use code from the callback for a session
|
|
403
|
+
(POST /api/auth/google/session).
|
|
404
|
+
|
|
405
|
+
On success adopts the returned authId in this client. On error raises
|
|
406
|
+
KonectyAPIError with errors[0].message and keeps the client unauthenticated.
|
|
407
|
+
"""
|
|
408
|
+
return await self._auth.exchange_google_code(
|
|
409
|
+
code,
|
|
410
|
+
geolocation=geolocation,
|
|
411
|
+
resolution=resolution,
|
|
412
|
+
source=source,
|
|
413
|
+
fingerprint=fingerprint,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
async def get_login_options(self) -> Dict[str, Any]:
|
|
417
|
+
"""Get login method flags for the namespace (GET /api/auth/login-options)."""
|
|
418
|
+
return await self._auth.get_login_options()
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def _query(self) -> QueryService:
|
|
422
|
+
if not hasattr(self, "_query_service"):
|
|
423
|
+
self._query_service = QueryService(self)
|
|
424
|
+
return self._query_service
|
|
425
|
+
|
|
426
|
+
async def execute_query_json(
|
|
427
|
+
self,
|
|
428
|
+
body: Any,
|
|
429
|
+
*,
|
|
430
|
+
include_total: bool = True,
|
|
431
|
+
include_meta: bool = False,
|
|
432
|
+
) -> QueryResult:
|
|
433
|
+
"""Execute cross-module query (POST /rest/query/json). Returns QueryResult with .stream, .total, .meta."""
|
|
434
|
+
return await self._query.execute_query_json(
|
|
435
|
+
body, include_total=include_total, include_meta=include_meta
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
async def execute_query_sql(
|
|
439
|
+
self,
|
|
440
|
+
sql: str,
|
|
441
|
+
*,
|
|
442
|
+
include_total: bool = True,
|
|
443
|
+
include_meta: bool = False,
|
|
444
|
+
) -> QueryResult:
|
|
445
|
+
"""Execute SQL query (POST /rest/query/sql). Returns QueryResult. SQL parse errors return 400."""
|
|
446
|
+
return await self._query.execute_query_sql(
|
|
447
|
+
sql, include_total=include_total, include_meta=include_meta
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
async def list_saved_queries(self) -> Any:
|
|
451
|
+
"""List saved queries."""
|
|
452
|
+
return await self._query.list_saved_queries()
|
|
453
|
+
|
|
454
|
+
async def get_saved_query(self, query_id: str) -> Any:
|
|
455
|
+
"""Get a saved query by id."""
|
|
456
|
+
return await self._query.get_saved_query(query_id)
|
|
457
|
+
|
|
458
|
+
async def create_saved_query(
|
|
459
|
+
self,
|
|
460
|
+
name: str,
|
|
461
|
+
query: Dict[str, Any],
|
|
462
|
+
description: Optional[str] = None,
|
|
463
|
+
) -> Any:
|
|
464
|
+
"""Create a saved query."""
|
|
465
|
+
return await self._query.create_saved_query(
|
|
466
|
+
name, query, description=description
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
async def update_saved_query(
|
|
470
|
+
self,
|
|
471
|
+
query_id: str,
|
|
472
|
+
*,
|
|
473
|
+
name: Optional[str] = None,
|
|
474
|
+
description: Optional[str] = None,
|
|
475
|
+
query: Optional[Dict[str, Any]] = None,
|
|
476
|
+
) -> Any:
|
|
477
|
+
"""Update a saved query."""
|
|
478
|
+
return await self._query.update_saved_query(
|
|
479
|
+
query_id, name=name, description=description, query=query
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
async def delete_saved_query(self, query_id: str) -> Any:
|
|
483
|
+
"""Delete a saved query."""
|
|
484
|
+
return await self._query.delete_saved_query(query_id)
|
|
485
|
+
|
|
486
|
+
async def share_saved_query(
|
|
487
|
+
self,
|
|
488
|
+
query_id: str,
|
|
489
|
+
shared_with: List[Dict[str, Any]],
|
|
490
|
+
is_public: Optional[bool] = None,
|
|
491
|
+
) -> Any:
|
|
492
|
+
"""Share a saved query."""
|
|
493
|
+
return await self._query.share_saved_query(
|
|
494
|
+
query_id, shared_with, is_public=is_public
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
async def find(self, module: str, options: KonectyFindParams) -> List[KonectyDict]:
|
|
498
|
+
params: Dict[str, str] = {}
|
|
499
|
+
for key, value in options.model_dump(exclude_none=True).items():
|
|
500
|
+
params[key] = (
|
|
501
|
+
json.dumps(value, default=json_serial)
|
|
502
|
+
if key != "fields"
|
|
503
|
+
else ",".join(value)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
async with (
|
|
507
|
+
aiohttp.ClientSession() as session,
|
|
508
|
+
session.get(
|
|
509
|
+
f"{self.base_url}/rest/data/{module}/find",
|
|
510
|
+
params=params,
|
|
511
|
+
headers={"Authorization": self.headers["Authorization"]},
|
|
512
|
+
) as response,
|
|
513
|
+
):
|
|
514
|
+
response.raise_for_status()
|
|
515
|
+
result = await response.json()
|
|
516
|
+
if not result.get("success", False):
|
|
517
|
+
errors = result.get("errors", [])
|
|
518
|
+
logger.error(errors)
|
|
519
|
+
raise KonectyAPIError(errors)
|
|
520
|
+
data = result.get("data", [])
|
|
521
|
+
return cast(List[KonectyDict], data)
|
|
522
|
+
|
|
523
|
+
async def lookup(self, module: str, lookup_field: str, options: KonectyFindParams, search: str = "") -> List[KonectyDict]:
|
|
524
|
+
params: Dict[str, str] = {}
|
|
525
|
+
for key, value in options.model_dump(exclude_none=True).items():
|
|
526
|
+
params[key] = (
|
|
527
|
+
json.dumps(value, default=json_serial)
|
|
528
|
+
if key != "fields"
|
|
529
|
+
else ",".join(value)
|
|
530
|
+
)
|
|
531
|
+
if search:
|
|
532
|
+
params["search"] = search
|
|
533
|
+
|
|
534
|
+
print(f"params: {params}")
|
|
535
|
+
async with (
|
|
536
|
+
aiohttp.ClientSession() as session,
|
|
537
|
+
session.get(
|
|
538
|
+
f"{self.base_url}/rest/data/{module}/lookup/{lookup_field}",
|
|
539
|
+
params=params,
|
|
540
|
+
headers={"Authorization": self.headers["Authorization"]},
|
|
541
|
+
) as response,
|
|
542
|
+
):
|
|
543
|
+
response.raise_for_status()
|
|
544
|
+
result = await response.json()
|
|
545
|
+
if not result.get("success", False):
|
|
546
|
+
errors = result.get("errors", [])
|
|
547
|
+
logger.error(errors)
|
|
548
|
+
raise KonectyAPIError(errors)
|
|
549
|
+
data = result.get("data", [])
|
|
550
|
+
return cast(List[KonectyDict], data)
|
|
551
|
+
|
|
552
|
+
def find_sync(self, module: str, options: KonectyFindParams) -> List[KonectyDict]:
|
|
553
|
+
"""Versão síncrona de find."""
|
|
554
|
+
params: Dict[str, str] = {}
|
|
555
|
+
for key, value in options.model_dump(exclude_none=True).items():
|
|
556
|
+
params[key] = (
|
|
557
|
+
json.dumps(value, default=json_serial)
|
|
558
|
+
if key != "fields"
|
|
559
|
+
else ",".join(value)
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
import requests
|
|
563
|
+
|
|
564
|
+
response = requests.get(
|
|
565
|
+
f"{self.base_url}/rest/data/{module}/find",
|
|
566
|
+
params=params,
|
|
567
|
+
headers={"Authorization": self.headers["Authorization"]},
|
|
568
|
+
)
|
|
569
|
+
response.raise_for_status()
|
|
570
|
+
result = response.json()
|
|
571
|
+
if not result.get("success", False):
|
|
572
|
+
errors = result.get("errors", [])
|
|
573
|
+
logger.error(errors)
|
|
574
|
+
raise KonectyAPIError(errors)
|
|
575
|
+
data = result.get("data", [])
|
|
576
|
+
return cast(List[KonectyDict], data)
|
|
577
|
+
|
|
578
|
+
def find_one_sync(
|
|
579
|
+
self, module: str, filter_params: KonectyFilter
|
|
580
|
+
) -> Optional[KonectyDict]:
|
|
581
|
+
"""Versão síncrona de find_one."""
|
|
582
|
+
find_params = KonectyFindParams(filter=filter_params, limit=1)
|
|
583
|
+
result = self.find_sync(module, find_params)
|
|
584
|
+
if not result:
|
|
585
|
+
return None
|
|
586
|
+
return cast(KonectyDict, result[0]) if isinstance(result[0], dict) else None
|
|
587
|
+
|
|
588
|
+
async def find_by_id(self, module: str, id: str) -> Optional[KonectyDict]:
|
|
589
|
+
async with (
|
|
590
|
+
aiohttp.ClientSession() as session,
|
|
591
|
+
session.get(
|
|
592
|
+
f"{self.base_url}/rest/data/{module}/{id}",
|
|
593
|
+
headers={"Authorization": self.headers["Authorization"]},
|
|
594
|
+
) as response,
|
|
595
|
+
):
|
|
596
|
+
response.raise_for_status()
|
|
597
|
+
result = await response.json()
|
|
598
|
+
if not result.get("success", False):
|
|
599
|
+
errors = result.get("errors", [])
|
|
600
|
+
logger.error(errors)
|
|
601
|
+
raise KonectyAPIError(errors)
|
|
602
|
+
data = result.get("data", [None])
|
|
603
|
+
return get_first_dict(data)
|
|
604
|
+
|
|
605
|
+
async def find_one(
|
|
606
|
+
self, module: str, filter_params: KonectyFilter
|
|
607
|
+
) -> Optional[KonectyDict]:
|
|
608
|
+
find_params = KonectyFindParams(filter=filter_params, limit=1)
|
|
609
|
+
result = await self.find(module, find_params)
|
|
610
|
+
if not result:
|
|
611
|
+
return None
|
|
612
|
+
return cast(KonectyDict, result[0]) if isinstance(result[0], dict) else None
|
|
613
|
+
|
|
614
|
+
async def create(self, module: str, data: KonectyDict) -> Optional[KonectyDict]:
|
|
615
|
+
endpoint = f"/rest/data/{module}"
|
|
616
|
+
cleaned_data = {
|
|
617
|
+
k: v for k, v in data.items() if k not in KONECTY_CREATE_IGNORE_FIELDS
|
|
618
|
+
}
|
|
619
|
+
async with (
|
|
620
|
+
aiohttp.ClientSession(base_url=self.base_url) as session,
|
|
621
|
+
session.post(
|
|
622
|
+
endpoint,
|
|
623
|
+
headers=self.headers,
|
|
624
|
+
json=json.loads(json.dumps(cleaned_data, default=json_serial)),
|
|
625
|
+
) as response,
|
|
626
|
+
):
|
|
627
|
+
result = await response.json()
|
|
628
|
+
if not result.get("success", False):
|
|
629
|
+
errors = result.get("errors", [])
|
|
630
|
+
raise KonectyAPIError(errors)
|
|
631
|
+
result_data: list[KonectyDict] = result.get("data", [])
|
|
632
|
+
if not result_data:
|
|
633
|
+
return None
|
|
634
|
+
return result_data[0]
|
|
635
|
+
|
|
636
|
+
async def update_one(
|
|
637
|
+
self, module: str, id: str, updatedAt: datetime, data: KonectyDict
|
|
638
|
+
) -> Optional[KonectyDict]:
|
|
639
|
+
endpoint = f"/rest/data/{module}"
|
|
640
|
+
cleaned_data = {
|
|
641
|
+
k: v for k, v in data.items() if k not in KONECTY_UPDATE_IGNORE_FIELDS
|
|
642
|
+
}
|
|
643
|
+
payload = {
|
|
644
|
+
"ids": [
|
|
645
|
+
{
|
|
646
|
+
"_id": id,
|
|
647
|
+
"_updatedAt": KonectyDateTime.from_datetime(updatedAt).to_json(),
|
|
648
|
+
}
|
|
649
|
+
],
|
|
650
|
+
"data": json.loads(json.dumps(cleaned_data, default=json_serial)),
|
|
651
|
+
}
|
|
652
|
+
async with (
|
|
653
|
+
aiohttp.ClientSession(base_url=self.base_url) as session,
|
|
654
|
+
session.put(
|
|
655
|
+
endpoint,
|
|
656
|
+
headers=self.headers,
|
|
657
|
+
json=json.loads(json.dumps(payload, default=json_serial)),
|
|
658
|
+
) as response,
|
|
659
|
+
):
|
|
660
|
+
result = await response.json()
|
|
661
|
+
if not result.get("success", False):
|
|
662
|
+
errors = result.get("errors", [])
|
|
663
|
+
raise KonectyAPIError(errors)
|
|
664
|
+
return result.get("data", [None])[0]
|
|
665
|
+
|
|
666
|
+
async def update(
|
|
667
|
+
self, module: str, ids: list[KonectyUpdateId], data: KonectyDict
|
|
668
|
+
) -> list[KonectyDict]:
|
|
669
|
+
endpoint = f"/rest/data/{module}"
|
|
670
|
+
cleaned_data = {
|
|
671
|
+
k: v for k, v in data.items() if k not in KONECTY_UPDATE_IGNORE_FIELDS
|
|
672
|
+
}
|
|
673
|
+
payload = {
|
|
674
|
+
"ids": [id.to_dict() for id in ids],
|
|
675
|
+
"data": json.loads(json.dumps(cleaned_data, default=json_serial)),
|
|
676
|
+
}
|
|
677
|
+
async with (
|
|
678
|
+
aiohttp.ClientSession(base_url=self.base_url) as session,
|
|
679
|
+
session.put(endpoint, headers=self.headers, json=payload) as response,
|
|
680
|
+
):
|
|
681
|
+
result = await response.json()
|
|
682
|
+
if not result.get("success", False):
|
|
683
|
+
errors = result.get("errors", [])
|
|
684
|
+
raise KonectyAPIError(errors)
|
|
685
|
+
return result.get("data", [])
|
|
686
|
+
|
|
687
|
+
async def delete_one(
|
|
688
|
+
self, module: str, id: str, updatedAt: datetime
|
|
689
|
+
) -> Optional[KonectyDict]:
|
|
690
|
+
endpoint = f"/rest/data/{module}"
|
|
691
|
+
payload = {
|
|
692
|
+
"ids": [
|
|
693
|
+
{
|
|
694
|
+
"_id": id,
|
|
695
|
+
"_updatedAt": KonectyDateTime.from_datetime(updatedAt).to_json(),
|
|
696
|
+
}
|
|
697
|
+
],
|
|
698
|
+
}
|
|
699
|
+
async with (
|
|
700
|
+
aiohttp.ClientSession(base_url=self.base_url) as session,
|
|
701
|
+
session.delete(endpoint, headers=self.headers, json=payload) as response,
|
|
702
|
+
):
|
|
703
|
+
result = await response.json()
|
|
704
|
+
return result.get("data", [None])[0]
|
|
705
|
+
|
|
706
|
+
async def get_document(self, document_id: str) -> Optional[KonectyDict]:
|
|
707
|
+
"""Obtém o documento do Konecty."""
|
|
708
|
+
endpoint = f"/rest/menu/documents/{document_id}"
|
|
709
|
+
async with (
|
|
710
|
+
aiohttp.ClientSession(base_url=self.base_url) as session,
|
|
711
|
+
session.get(endpoint, headers=self.headers) as response,
|
|
712
|
+
):
|
|
713
|
+
result = await response.json()
|
|
714
|
+
if result is None:
|
|
715
|
+
logger.error(f"Documento {document_id} não encontrado")
|
|
716
|
+
return None
|
|
717
|
+
if isinstance(result, dict):
|
|
718
|
+
return cast(KonectyDict, result)
|
|
719
|
+
logger.error(f"Documento {document_id} retornou formato inválido")
|
|
720
|
+
return None
|
|
721
|
+
|
|
722
|
+
async def get_schema(self, document_id: str) -> Optional[KonectyDict]:
|
|
723
|
+
"""Obtém o schema do documento e gera um modelo Pydantic."""
|
|
724
|
+
try:
|
|
725
|
+
document = await self.get_document(document_id)
|
|
726
|
+
if document is None:
|
|
727
|
+
return None
|
|
728
|
+
return document
|
|
729
|
+
except Exception as e:
|
|
730
|
+
logger.error(f"Erro ao obter schema do documento {document_id}: {e}")
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
async def get_setting(self, key: str) -> Optional[str]:
|
|
734
|
+
"""Obtém uma configuração do Konecty."""
|
|
735
|
+
setting = await self.find_one(
|
|
736
|
+
"Setting", KonectyFilter.create().add_condition("name", "equals", key)
|
|
737
|
+
)
|
|
738
|
+
if setting is None:
|
|
739
|
+
return None
|
|
740
|
+
return cast(str, setting.get("value"))
|
|
741
|
+
|
|
742
|
+
def get_setting_sync(self, key: str) -> Optional[str]:
|
|
743
|
+
"""Versão síncrona de get_setting."""
|
|
744
|
+
setting = self.find_one_sync(
|
|
745
|
+
"Setting", KonectyFilter.create().add_condition("name", "equals", key)
|
|
746
|
+
)
|
|
747
|
+
if setting is None:
|
|
748
|
+
return None
|
|
749
|
+
return cast(str, setting.get("value"))
|
|
750
|
+
|
|
751
|
+
async def get_settings(
|
|
752
|
+
self, keys: List[str], *, module: str | None = None
|
|
753
|
+
) -> Dict[str, str]:
|
|
754
|
+
"""Obtém múltiplas configurações do Konecty.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
keys: Lista de chaves das configurações a serem obtidas
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
Dicionário com as chaves e seus respectivos valores. Chaves não encontradas terão valor None.
|
|
761
|
+
"""
|
|
762
|
+
if not keys:
|
|
763
|
+
return {}
|
|
764
|
+
|
|
765
|
+
filter_params = KonectyFilter.create().add_condition("name", "in", keys)
|
|
766
|
+
if module:
|
|
767
|
+
filter_params.add_condition("module", "equals", module)
|
|
768
|
+
find_params = KonectyFindParams(filter=filter_params)
|
|
769
|
+
|
|
770
|
+
settings = await self.find("Setting", find_params)
|
|
771
|
+
|
|
772
|
+
result = {}
|
|
773
|
+
|
|
774
|
+
for setting in settings:
|
|
775
|
+
key = setting.get("name")
|
|
776
|
+
value = setting.get("value")
|
|
777
|
+
result[key] = cast(str, value)
|
|
778
|
+
|
|
779
|
+
return result
|
|
780
|
+
|
|
781
|
+
def get_settings_sync(
|
|
782
|
+
self, keys: List[str], *, module: str | None = None
|
|
783
|
+
) -> Dict[str, str]:
|
|
784
|
+
"""Versão síncrona de get_settings.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
keys: Lista de chaves das configurações a serem obtidas
|
|
788
|
+
|
|
789
|
+
Returns:
|
|
790
|
+
Dicionário com as chaves e seus respectivos valores. Chaves não encontradas terão valor None.
|
|
791
|
+
"""
|
|
792
|
+
if not keys:
|
|
793
|
+
return {}
|
|
794
|
+
|
|
795
|
+
filter_params = KonectyFilter.create().add_condition("name", "in", keys)
|
|
796
|
+
if module:
|
|
797
|
+
filter_params.add_condition("module", "equals", module)
|
|
798
|
+
find_params = KonectyFindParams(filter=filter_params)
|
|
799
|
+
|
|
800
|
+
settings = self.find_sync("Setting", find_params)
|
|
801
|
+
|
|
802
|
+
result = {}
|
|
803
|
+
|
|
804
|
+
for setting in settings:
|
|
805
|
+
key = setting.get("name")
|
|
806
|
+
value = setting.get("value")
|
|
807
|
+
result[key] = cast(str, value)
|
|
808
|
+
|
|
809
|
+
return result
|
|
810
|
+
|
|
811
|
+
async def count_documents(self, module: str, filter_params: KonectyFilter) -> int:
|
|
812
|
+
params: Dict[str, str] = {}
|
|
813
|
+
options = KonectyFindParams(
|
|
814
|
+
filter=filter_params,
|
|
815
|
+
fields=["_id"],
|
|
816
|
+
limit=1,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
for key, value in options.model_dump(exclude_none=True).items():
|
|
820
|
+
params[key] = (
|
|
821
|
+
json.dumps(value, default=json_serial)
|
|
822
|
+
if key != "fields"
|
|
823
|
+
else ",".join(value)
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
async with (
|
|
827
|
+
aiohttp.ClientSession() as session,
|
|
828
|
+
session.get(
|
|
829
|
+
f"{self.base_url}/rest/data/{module}/find",
|
|
830
|
+
params=params,
|
|
831
|
+
headers={"Authorization": self.headers["Authorization"]},
|
|
832
|
+
) as response,
|
|
833
|
+
):
|
|
834
|
+
response.raise_for_status()
|
|
835
|
+
result = await response.json()
|
|
836
|
+
if not result.get("success", False):
|
|
837
|
+
errors = result.get("errors", [])
|
|
838
|
+
logger.error(errors)
|
|
839
|
+
raise KonectyAPIError(errors)
|
|
840
|
+
count = result.get("total", 0)
|
|
841
|
+
return count
|
|
842
|
+
|
|
843
|
+
async def upload_file(
|
|
844
|
+
self,
|
|
845
|
+
module: str,
|
|
846
|
+
record_code: str,
|
|
847
|
+
field_name: str,
|
|
848
|
+
file: Union[bytes, str, AsyncGenerator[bytes, None]],
|
|
849
|
+
file_name: Optional[str] = None,
|
|
850
|
+
file_type: Optional[str] = None,
|
|
851
|
+
) -> str:
|
|
852
|
+
"""
|
|
853
|
+
Upload a file to a specific record field in Konecty.
|
|
854
|
+
|
|
855
|
+
Parameters
|
|
856
|
+
----------
|
|
857
|
+
module : str
|
|
858
|
+
The module name where the record is located (e.g., 'Contact', 'User').
|
|
859
|
+
record_code : str
|
|
860
|
+
The unique identifier code of the record.
|
|
861
|
+
field_name : str
|
|
862
|
+
The name of the field where the file will be uploaded.
|
|
863
|
+
file : Union[bytes, str]
|
|
864
|
+
The file to upload. Can be:
|
|
865
|
+
- bytes: Raw file content (file_name is required)
|
|
866
|
+
- str: URL to the file (file_name is optional; if not provided, will use the last segment of the URL)
|
|
867
|
+
file_name : Optional[str], default=None
|
|
868
|
+
The name to use for the file when uploaded. Required when 'file' is bytes, optional otherwise.
|
|
869
|
+
|
|
870
|
+
Returns
|
|
871
|
+
-------
|
|
872
|
+
str
|
|
873
|
+
The file key (ID) assigned by Konecty, which can be used for referencing the file in future operations.
|
|
874
|
+
|
|
875
|
+
Raises
|
|
876
|
+
------
|
|
877
|
+
ValueError
|
|
878
|
+
If file_name is not provided when file is bytes, or if the file is empty or invalid.
|
|
879
|
+
TypeError
|
|
880
|
+
If the file argument is not bytes or a URL string.
|
|
881
|
+
KonectyError
|
|
882
|
+
If the API returns an error response.
|
|
883
|
+
HTTPError
|
|
884
|
+
If there is an HTTP connection error.
|
|
885
|
+
|
|
886
|
+
Limitations
|
|
887
|
+
-----------
|
|
888
|
+
- Maximum file size: 20 MB (enforced by server configuration; see nginx.conf).
|
|
889
|
+
- Only one file per call is supported.
|
|
890
|
+
- Progress tracking is not available in this version.
|
|
891
|
+
|
|
892
|
+
Examples
|
|
893
|
+
--------
|
|
894
|
+
Upload a file using a file path as string:
|
|
895
|
+
>>> file_id = await client.upload_file(
|
|
896
|
+
... module='Contact',
|
|
897
|
+
... record_code='ABC123',
|
|
898
|
+
... field_name='attachments',
|
|
899
|
+
... file='/path/to/document.pdf'
|
|
900
|
+
... )
|
|
901
|
+
|
|
902
|
+
Upload a file using a Path object:
|
|
903
|
+
>>> from pathlib import Path
|
|
904
|
+
>>> file_path = Path('/path/to/image.jpg')
|
|
905
|
+
>>> file_id = await client.upload_file(
|
|
906
|
+
... module='Contact',
|
|
907
|
+
... record_code='ABC123',
|
|
908
|
+
... field_name='photo',
|
|
909
|
+
... file=file_path
|
|
910
|
+
... )
|
|
911
|
+
|
|
912
|
+
Upload file content from bytes with a custom filename:
|
|
913
|
+
>>> with open('document.pdf', 'rb') as f:
|
|
914
|
+
... file_content = f.read()
|
|
915
|
+
>>> file_id = await client.upload_file(
|
|
916
|
+
... module='Document',
|
|
917
|
+
... record_code='XYZ789',
|
|
918
|
+
... field_name='file',
|
|
919
|
+
... file=file_content,
|
|
920
|
+
... file_name='important_document.pdf'
|
|
921
|
+
... )
|
|
922
|
+
|
|
923
|
+
Handling errors:
|
|
924
|
+
>>> try:
|
|
925
|
+
... file_id = await client.upload_file(
|
|
926
|
+
... module='Contact',
|
|
927
|
+
... record_code='INVALID',
|
|
928
|
+
... field_name='attachments',
|
|
929
|
+
... file='/invalid/path/to/file.pdf'
|
|
930
|
+
... )
|
|
931
|
+
... except FileNotFoundError as e:
|
|
932
|
+
... print(f"File not found: {e}")
|
|
933
|
+
... except ValueError as e:
|
|
934
|
+
... print(f"Validation error: {e}")
|
|
935
|
+
... except KonectyError as e:
|
|
936
|
+
... print(f"API error: {e}")
|
|
937
|
+
|
|
938
|
+
"""
|
|
939
|
+
result = await self.file_manager.upload_file(
|
|
940
|
+
module=module,
|
|
941
|
+
record_code=record_code,
|
|
942
|
+
field_name=field_name,
|
|
943
|
+
file=file,
|
|
944
|
+
file_name=file_name,
|
|
945
|
+
file_type=file_type,
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
if not result.get("success", False):
|
|
949
|
+
self.file_manager.handle_error_response(result)
|
|
950
|
+
return result.get("key", "")
|