konecty-sdk-python 1.2.2__tar.gz → 2.0.0__tar.gz
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.
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/lib/client.py +408 -24
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/exceptions.py +26 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/feature_types/__init__.py +22 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/feature_types/cross_module_query.py +72 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/feature_types/kpi.py +21 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/http.py +95 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/__init__.py +5 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/aggregation.py +142 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/base.py +63 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/change_user.py +64 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/comments.py +89 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/export.py +54 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/files.py +59 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/notifications.py +41 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/query.py +190 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/stream.py +132 -0
- konecty_sdk_python-2.0.0/KonectySdkPython/lib/services/subscriptions.py +26 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/lib/settings.py +16 -16
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/PKG-INFO +1 -1
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/pyproject.toml +1 -1
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/.gitignore +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/__init__.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/cli/__init__.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/cli/apply.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/cli/backup.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/cli/pull.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/lib/__init__.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/lib/file_manager.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/lib/filters.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/lib/model.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/KonectySdkPython/lib/types.py +0 -0
- {konecty_sdk_python-1.2.2 → konecty_sdk_python-2.0.0}/README.md +0 -0
|
@@ -3,12 +3,30 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
from datetime import datetime
|
|
6
|
-
from typing import Any, AsyncGenerator, Dict, List, Optional, Union, cast
|
|
6
|
+
from typing import Any, AsyncGenerator, Dict, List, Literal, Optional, Union, cast
|
|
7
7
|
|
|
8
8
|
import aiohttp
|
|
9
9
|
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
KonectyAPIError,
|
|
12
|
+
KonectyError,
|
|
13
|
+
KonectySerializationError,
|
|
14
|
+
KonectyValidationError,
|
|
15
|
+
)
|
|
10
16
|
from .file_manager import FileManager
|
|
11
17
|
from .filters import KonectyFilter, KonectyFindParams
|
|
18
|
+
from .http import request as _http_request
|
|
19
|
+
from .http import StreamResponse
|
|
20
|
+
from .feature_types.kpi import KpiConfig
|
|
21
|
+
from .services.aggregation import AggregationService
|
|
22
|
+
from .services.change_user import ChangeUserService
|
|
23
|
+
from .services.comments import CommentsService
|
|
24
|
+
from .services.export import ExportService
|
|
25
|
+
from .services.files import FilesService
|
|
26
|
+
from .services.notifications import NotificationsService
|
|
27
|
+
from .services.query import QueryResult, QueryService
|
|
28
|
+
from .services.stream import FindStreamResult, StreamService
|
|
29
|
+
from .services.subscriptions import SubscriptionsService
|
|
12
30
|
from .types import KonectyDateTime, KonectyUpdateId
|
|
13
31
|
|
|
14
32
|
# Configura o logger do urllib3 para mostrar apenas erros
|
|
@@ -39,43 +57,409 @@ def get_first_dict(items: List[Any]) -> Optional[KonectyDict]:
|
|
|
39
57
|
return None
|
|
40
58
|
|
|
41
59
|
|
|
42
|
-
|
|
43
|
-
"""
|
|
60
|
+
def json_serial(obj: Any) -> str:
|
|
61
|
+
"""Serializa objetos para JSON."""
|
|
62
|
+
if isinstance(obj, datetime):
|
|
63
|
+
return {"$date": obj.isoformat()}
|
|
64
|
+
raise KonectySerializationError()
|
|
44
65
|
|
|
45
|
-
pass
|
|
46
66
|
|
|
67
|
+
class KonectyClient:
|
|
68
|
+
def __init__(self, base_url: str, token: str) -> None:
|
|
69
|
+
self.base_url = base_url
|
|
70
|
+
self.headers = {"Authorization": f"{token}"}
|
|
71
|
+
self.file_manager = FileManager(base_url=base_url, headers=self.headers)
|
|
47
72
|
|
|
48
|
-
|
|
49
|
-
|
|
73
|
+
async def _request(
|
|
74
|
+
self,
|
|
75
|
+
method: str,
|
|
76
|
+
path: str,
|
|
77
|
+
*,
|
|
78
|
+
params: Optional[Dict[str, Any]] = None,
|
|
79
|
+
json: Optional[Dict[str, Any]] = None,
|
|
80
|
+
stream: bool = False,
|
|
81
|
+
return_bytes: bool = False,
|
|
82
|
+
) -> Any:
|
|
83
|
+
"""Internal HTTP request. Used by client methods and services."""
|
|
84
|
+
return await _http_request(
|
|
85
|
+
self,
|
|
86
|
+
method,
|
|
87
|
+
path,
|
|
88
|
+
params=params,
|
|
89
|
+
json=json,
|
|
90
|
+
stream=stream,
|
|
91
|
+
return_bytes=return_bytes,
|
|
92
|
+
)
|
|
50
93
|
|
|
51
|
-
|
|
94
|
+
@property
|
|
95
|
+
def _stream(self) -> StreamService:
|
|
96
|
+
if not hasattr(self, "_stream_service"):
|
|
97
|
+
self._stream_service = StreamService(self)
|
|
98
|
+
return self._stream_service
|
|
52
99
|
|
|
100
|
+
async def find_stream(
|
|
101
|
+
self,
|
|
102
|
+
module: str,
|
|
103
|
+
options: KonectyFindParams,
|
|
104
|
+
*,
|
|
105
|
+
include_total: bool = False,
|
|
106
|
+
) -> FindStreamResult:
|
|
107
|
+
"""
|
|
108
|
+
Stream records as NDJSON. Returns result with .stream (async generator) and .total (when include_total).
|
|
53
109
|
|
|
54
|
-
|
|
55
|
-
|
|
110
|
+
Example:
|
|
111
|
+
result = await client.find_stream("Contact", options, include_total=True)
|
|
112
|
+
async for record in result.stream:
|
|
113
|
+
...
|
|
114
|
+
total = result.total
|
|
115
|
+
"""
|
|
116
|
+
return await self._stream.find_stream(module, options, include_total=include_total)
|
|
56
117
|
|
|
57
|
-
|
|
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
|
+
)
|
|
58
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
|
+
)
|
|
59
158
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
62
164
|
|
|
63
|
-
def
|
|
64
|
-
|
|
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
|
+
)
|
|
65
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
|
+
)
|
|
66
190
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
)
|
|
72
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
|
|
73
209
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
247
|
+
module, data_id, comment_id, text
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def delete_comment(
|
|
251
|
+
self, module: str, data_id: str, comment_id: str
|
|
252
|
+
) -> Any:
|
|
253
|
+
"""Delete (soft) a comment."""
|
|
254
|
+
return await self._comments.delete_comment(module, data_id, comment_id)
|
|
255
|
+
|
|
256
|
+
async def search_comment_users(
|
|
257
|
+
self, module: str, data_id: str, query: str = ""
|
|
258
|
+
) -> Any:
|
|
259
|
+
"""Search users for @mention autocomplete."""
|
|
260
|
+
return await self._comments.search_comment_users(
|
|
261
|
+
module, data_id, query
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
async def search_comments(
|
|
265
|
+
self,
|
|
266
|
+
module: str,
|
|
267
|
+
data_id: str,
|
|
268
|
+
*,
|
|
269
|
+
query: Optional[str] = None,
|
|
270
|
+
author_id: Optional[str] = None,
|
|
271
|
+
start_date: Optional[str] = None,
|
|
272
|
+
end_date: Optional[str] = None,
|
|
273
|
+
page: Optional[int] = None,
|
|
274
|
+
limit: Optional[int] = None,
|
|
275
|
+
) -> Any:
|
|
276
|
+
"""Search comments with filters."""
|
|
277
|
+
return await self._comments.search_comments(
|
|
278
|
+
module,
|
|
279
|
+
data_id,
|
|
280
|
+
query=query,
|
|
281
|
+
author_id=author_id,
|
|
282
|
+
start_date=start_date,
|
|
283
|
+
end_date=end_date,
|
|
284
|
+
page=page,
|
|
285
|
+
limit=limit,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def _subscriptions(self) -> SubscriptionsService:
|
|
290
|
+
if not hasattr(self, "_subscriptions_service"):
|
|
291
|
+
self._subscriptions_service = SubscriptionsService(self)
|
|
292
|
+
return self._subscriptions_service
|
|
293
|
+
|
|
294
|
+
async def get_subscription_status(self, module: str, data_id: str) -> Any:
|
|
295
|
+
"""Get subscription status for a record."""
|
|
296
|
+
return await self._subscriptions.get_subscription_status(module, data_id)
|
|
297
|
+
|
|
298
|
+
async def subscribe(self, module: str, data_id: str) -> Any:
|
|
299
|
+
"""Subscribe to record notifications."""
|
|
300
|
+
return await self._subscriptions.subscribe(module, data_id)
|
|
301
|
+
|
|
302
|
+
async def unsubscribe(self, module: str, data_id: str) -> Any:
|
|
303
|
+
"""Unsubscribe from record."""
|
|
304
|
+
return await self._subscriptions.unsubscribe(module, data_id)
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def _notifications(self) -> NotificationsService:
|
|
308
|
+
if not hasattr(self, "_notifications_service"):
|
|
309
|
+
self._notifications_service = NotificationsService(self)
|
|
310
|
+
return self._notifications_service
|
|
311
|
+
|
|
312
|
+
async def list_notifications(
|
|
313
|
+
self,
|
|
314
|
+
*,
|
|
315
|
+
read: Optional[bool] = None,
|
|
316
|
+
page: Optional[int] = None,
|
|
317
|
+
limit: Optional[int] = None,
|
|
318
|
+
) -> Any:
|
|
319
|
+
"""List user notifications."""
|
|
320
|
+
return await self._notifications.list_notifications(
|
|
321
|
+
read=read, page=page, limit=limit
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
async def get_unread_notifications_count(self) -> Any:
|
|
325
|
+
"""Get unread notifications count."""
|
|
326
|
+
return await self._notifications.get_unread_count()
|
|
327
|
+
|
|
328
|
+
async def mark_notification_read(self, notification_id: str) -> Any:
|
|
329
|
+
"""Mark a notification as read."""
|
|
330
|
+
return await self._notifications.mark_notification_read(notification_id)
|
|
331
|
+
|
|
332
|
+
async def mark_all_notifications_read(self) -> Any:
|
|
333
|
+
"""Mark all notifications as read."""
|
|
334
|
+
return await self._notifications.mark_all_notifications_read()
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def _change_user(self) -> ChangeUserService:
|
|
338
|
+
if not hasattr(self, "_change_user_service"):
|
|
339
|
+
self._change_user_service = ChangeUserService(self)
|
|
340
|
+
return self._change_user_service
|
|
341
|
+
|
|
342
|
+
async def change_user_add(
|
|
343
|
+
self, module: str, ids: List[Any], users: Any
|
|
344
|
+
) -> Any:
|
|
345
|
+
"""Add users to records."""
|
|
346
|
+
return await self._change_user.add_users(module, ids, users)
|
|
347
|
+
|
|
348
|
+
async def change_user_remove(
|
|
349
|
+
self, module: str, ids: List[Any], users: Any
|
|
350
|
+
) -> Any:
|
|
351
|
+
"""Remove users from records."""
|
|
352
|
+
return await self._change_user.remove_users(module, ids, users)
|
|
353
|
+
|
|
354
|
+
async def change_user_define(
|
|
355
|
+
self, module: str, ids: List[Any], users: Any
|
|
356
|
+
) -> Any:
|
|
357
|
+
"""Define users on records."""
|
|
358
|
+
return await self._change_user.define_users(module, ids, users)
|
|
359
|
+
|
|
360
|
+
async def change_user_replace(
|
|
361
|
+
self,
|
|
362
|
+
module: str,
|
|
363
|
+
ids: List[Any],
|
|
364
|
+
*,
|
|
365
|
+
from_user: Any = None,
|
|
366
|
+
to_user: Any = None,
|
|
367
|
+
) -> Any:
|
|
368
|
+
"""Replace user on records."""
|
|
369
|
+
return await self._change_user.replace_users(
|
|
370
|
+
module, ids, from_user=from_user, to_user=to_user
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
async def change_user_count_inactive(self, module: str, ids: List[Any]) -> Any:
|
|
374
|
+
"""Count inactive users on records."""
|
|
375
|
+
return await self._change_user.count_inactive(module, ids)
|
|
376
|
+
|
|
377
|
+
async def change_user_remove_inactive(self, module: str, ids: List[Any]) -> Any:
|
|
378
|
+
"""Remove inactive users from records."""
|
|
379
|
+
return await self._change_user.remove_inactive(module, ids)
|
|
380
|
+
|
|
381
|
+
async def change_user_set_queue(
|
|
382
|
+
self, module: str, ids: List[Any], queue: Any
|
|
383
|
+
) -> Any:
|
|
384
|
+
"""Set queue on records."""
|
|
385
|
+
return await self._change_user.set_queue(module, ids, queue)
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def _query(self) -> QueryService:
|
|
389
|
+
if not hasattr(self, "_query_service"):
|
|
390
|
+
self._query_service = QueryService(self)
|
|
391
|
+
return self._query_service
|
|
392
|
+
|
|
393
|
+
async def execute_query_json(
|
|
394
|
+
self,
|
|
395
|
+
body: Any,
|
|
396
|
+
*,
|
|
397
|
+
include_total: bool = True,
|
|
398
|
+
include_meta: bool = False,
|
|
399
|
+
) -> QueryResult:
|
|
400
|
+
"""Execute cross-module query (POST /rest/query/json). Returns QueryResult with .stream, .total, .meta."""
|
|
401
|
+
return await self._query.execute_query_json(
|
|
402
|
+
body, include_total=include_total, include_meta=include_meta
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
async def execute_query_sql(
|
|
406
|
+
self,
|
|
407
|
+
sql: str,
|
|
408
|
+
*,
|
|
409
|
+
include_total: bool = True,
|
|
410
|
+
include_meta: bool = False,
|
|
411
|
+
) -> QueryResult:
|
|
412
|
+
"""Execute SQL query (POST /rest/query/sql). Returns QueryResult. SQL parse errors return 400."""
|
|
413
|
+
return await self._query.execute_query_sql(
|
|
414
|
+
sql, include_total=include_total, include_meta=include_meta
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
async def list_saved_queries(self) -> Any:
|
|
418
|
+
"""List saved queries."""
|
|
419
|
+
return await self._query.list_saved_queries()
|
|
420
|
+
|
|
421
|
+
async def get_saved_query(self, query_id: str) -> Any:
|
|
422
|
+
"""Get a saved query by id."""
|
|
423
|
+
return await self._query.get_saved_query(query_id)
|
|
424
|
+
|
|
425
|
+
async def create_saved_query(
|
|
426
|
+
self,
|
|
427
|
+
name: str,
|
|
428
|
+
query: Dict[str, Any],
|
|
429
|
+
description: Optional[str] = None,
|
|
430
|
+
) -> Any:
|
|
431
|
+
"""Create a saved query."""
|
|
432
|
+
return await self._query.create_saved_query(
|
|
433
|
+
name, query, description=description
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
async def update_saved_query(
|
|
437
|
+
self,
|
|
438
|
+
query_id: str,
|
|
439
|
+
*,
|
|
440
|
+
name: Optional[str] = None,
|
|
441
|
+
description: Optional[str] = None,
|
|
442
|
+
query: Optional[Dict[str, Any]] = None,
|
|
443
|
+
) -> Any:
|
|
444
|
+
"""Update a saved query."""
|
|
445
|
+
return await self._query.update_saved_query(
|
|
446
|
+
query_id, name=name, description=description, query=query
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
async def delete_saved_query(self, query_id: str) -> Any:
|
|
450
|
+
"""Delete a saved query."""
|
|
451
|
+
return await self._query.delete_saved_query(query_id)
|
|
452
|
+
|
|
453
|
+
async def share_saved_query(
|
|
454
|
+
self,
|
|
455
|
+
query_id: str,
|
|
456
|
+
shared_with: List[Dict[str, Any]],
|
|
457
|
+
is_public: Optional[bool] = None,
|
|
458
|
+
) -> Any:
|
|
459
|
+
"""Share a saved query."""
|
|
460
|
+
return await self._query.share_saved_query(
|
|
461
|
+
query_id, shared_with, is_public=is_public
|
|
462
|
+
)
|
|
79
463
|
|
|
80
464
|
async def find(self, module: str, options: KonectyFindParams) -> List[KonectyDict]:
|
|
81
465
|
params: Dict[str, str] = {}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Exceptions for the Konecty SDK."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KonectyError(Exception):
|
|
5
|
+
"""Base exception for Konecty errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KonectyAPIError(KonectyError):
|
|
11
|
+
"""Raised when the API returns success=false or a non-2xx status."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KonectyValidationError(KonectyError):
|
|
17
|
+
"""Raised for validation errors."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class KonectySerializationError(KonectyError):
|
|
23
|
+
"""Raised when a value is not serializable."""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
super().__init__("Tipo não serializável")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Feature-specific types (KPI, Graph, Pivot, CrossModuleQuery, etc.). Re-exported for convenience."""
|
|
2
|
+
|
|
3
|
+
from .cross_module_query import (
|
|
4
|
+
CrossModuleQuery,
|
|
5
|
+
CrossModuleRelation,
|
|
6
|
+
Aggregator,
|
|
7
|
+
DEFAULT_PRIMARY_LIMIT,
|
|
8
|
+
MAX_RELATION_LIMIT,
|
|
9
|
+
MAX_RELATIONS,
|
|
10
|
+
)
|
|
11
|
+
from .kpi import KpiConfig, KpiOperation
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Aggregator",
|
|
15
|
+
"CrossModuleQuery",
|
|
16
|
+
"CrossModuleRelation",
|
|
17
|
+
"DEFAULT_PRIMARY_LIMIT",
|
|
18
|
+
"KpiConfig",
|
|
19
|
+
"KpiOperation",
|
|
20
|
+
"MAX_RELATION_LIMIT",
|
|
21
|
+
"MAX_RELATIONS",
|
|
22
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Types for cross-module query (query/json and query/sql). Mirrors Konecty crossModuleQuery schema."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
MAX_RELATIONS = 10
|
|
8
|
+
MAX_NESTING_DEPTH = 2
|
|
9
|
+
MAX_RELATION_LIMIT = 100_000
|
|
10
|
+
DEFAULT_RELATION_LIMIT = 1000
|
|
11
|
+
DEFAULT_PRIMARY_LIMIT = 1000
|
|
12
|
+
|
|
13
|
+
AggregatorName = str # count, countDistinct, sum, avg, min, max, first, last, push, addToSet
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Aggregator(BaseModel):
|
|
17
|
+
"""Aggregator in a relation or root. Field required for some (e.g. countDistinct)."""
|
|
18
|
+
|
|
19
|
+
aggregator: str
|
|
20
|
+
field: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SortItem(BaseModel):
|
|
24
|
+
"""Sort item: property and direction."""
|
|
25
|
+
|
|
26
|
+
property: str
|
|
27
|
+
direction: str = "ASC" # ASC | DESC
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ExplicitJoinCondition(BaseModel):
|
|
31
|
+
"""Explicit join condition: left and right keys."""
|
|
32
|
+
|
|
33
|
+
left: str
|
|
34
|
+
right: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CrossModuleRelation(BaseModel):
|
|
38
|
+
"""Relation in a cross-module query. At least one aggregator required."""
|
|
39
|
+
|
|
40
|
+
document: str
|
|
41
|
+
lookup: str
|
|
42
|
+
on: Optional[ExplicitJoinCondition] = None
|
|
43
|
+
filter: Optional[Dict[str, Any]] = None
|
|
44
|
+
fields: Optional[str] = None
|
|
45
|
+
sort: Optional[Union[str, List[SortItem]]] = None
|
|
46
|
+
limit: int = Field(default=DEFAULT_RELATION_LIMIT, ge=1, le=MAX_RELATION_LIMIT)
|
|
47
|
+
start: int = Field(default=0, ge=0)
|
|
48
|
+
aggregators: Dict[str, Aggregator] = Field(..., min_length=1)
|
|
49
|
+
relations: Optional[List["CrossModuleRelation"]] = Field(default=None, max_length=MAX_RELATIONS)
|
|
50
|
+
|
|
51
|
+
model_config = {"extra": "allow"}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
CrossModuleRelation.model_rebuild()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CrossModuleQuery(BaseModel):
|
|
58
|
+
"""Body for POST /rest/query/json. Primary document + optional relations, groupBy, aggregators."""
|
|
59
|
+
|
|
60
|
+
document: str
|
|
61
|
+
filter: Optional[Dict[str, Any]] = None
|
|
62
|
+
fields: Optional[str] = None
|
|
63
|
+
sort: Optional[Union[str, List[SortItem]]] = None
|
|
64
|
+
limit: int = Field(default=DEFAULT_PRIMARY_LIMIT, ge=1, le=MAX_RELATION_LIMIT)
|
|
65
|
+
start: int = Field(default=0, ge=0)
|
|
66
|
+
relations: List[CrossModuleRelation] = Field(default_factory=list, max_length=MAX_RELATIONS)
|
|
67
|
+
groupBy: List[str] = Field(default_factory=list, alias="groupBy")
|
|
68
|
+
aggregators: Dict[str, Aggregator] = Field(default_factory=dict)
|
|
69
|
+
includeTotal: bool = Field(default=True, alias="includeTotal")
|
|
70
|
+
includeMeta: bool = Field(default=False, alias="includeMeta")
|
|
71
|
+
|
|
72
|
+
model_config = {"extra": "allow", "populate_by_name": True}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""KPI config for aggregation API."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal, Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
KpiOperation = Literal["count", "sum", "avg", "min", "max", "countDistinct"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class KpiConfig(BaseModel):
|
|
12
|
+
"""Config for KPI aggregation. Field is required for all operations except count."""
|
|
13
|
+
|
|
14
|
+
operation: KpiOperation
|
|
15
|
+
field: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
@model_validator(mode="after")
|
|
18
|
+
def count_distinct_requires_field(self) -> "KpiConfig":
|
|
19
|
+
if self.operation == "countDistinct" and not self.field:
|
|
20
|
+
raise ValueError("countDistinct requires field")
|
|
21
|
+
return self
|