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.
Files changed (35) hide show
  1. KonectySdkPython/__init__.py +41 -0
  2. KonectySdkPython/cli/__init__.py +38 -0
  3. KonectySdkPython/cli/apply.py +555 -0
  4. KonectySdkPython/cli/backup.py +161 -0
  5. KonectySdkPython/cli/pull.py +309 -0
  6. KonectySdkPython/lib/__init__.py +43 -0
  7. KonectySdkPython/lib/client.py +950 -0
  8. KonectySdkPython/lib/exceptions.py +26 -0
  9. KonectySdkPython/lib/feature_types/__init__.py +46 -0
  10. KonectySdkPython/lib/feature_types/cross_module_query.py +78 -0
  11. KonectySdkPython/lib/feature_types/kpi.py +21 -0
  12. KonectySdkPython/lib/feature_types/query_json.py +242 -0
  13. KonectySdkPython/lib/file_manager.py +248 -0
  14. KonectySdkPython/lib/filters.py +155 -0
  15. KonectySdkPython/lib/http.py +95 -0
  16. KonectySdkPython/lib/model.py +88 -0
  17. KonectySdkPython/lib/serialization.py +13 -0
  18. KonectySdkPython/lib/services/__init__.py +5 -0
  19. KonectySdkPython/lib/services/aggregation.py +142 -0
  20. KonectySdkPython/lib/services/auth.py +105 -0
  21. KonectySdkPython/lib/services/base.py +63 -0
  22. KonectySdkPython/lib/services/change_user.py +64 -0
  23. KonectySdkPython/lib/services/comments.py +89 -0
  24. KonectySdkPython/lib/services/export.py +54 -0
  25. KonectySdkPython/lib/services/files.py +59 -0
  26. KonectySdkPython/lib/services/notifications.py +41 -0
  27. KonectySdkPython/lib/services/query.py +192 -0
  28. KonectySdkPython/lib/services/stream.py +133 -0
  29. KonectySdkPython/lib/services/subscriptions.py +26 -0
  30. KonectySdkPython/lib/settings.py +152 -0
  31. KonectySdkPython/lib/types.py +418 -0
  32. konecty_sdk_python-2.1.1.dist-info/METADATA +155 -0
  33. konecty_sdk_python-2.1.1.dist-info/RECORD +35 -0
  34. konecty_sdk_python-2.1.1.dist-info/WHEEL +4 -0
  35. konecty_sdk_python-2.1.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,64 @@
1
+ """Change User API: add, remove, define, replace, countInactive, removeInactive, setQueue."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from .base import BaseService
6
+
7
+
8
+ class ChangeUserService(BaseService):
9
+ """Service for changing user/queue on records."""
10
+
11
+ async def add_users(
12
+ self, module: str, ids: List[Any], users: Any
13
+ ) -> Dict[str, Any]:
14
+ """POST /rest/changeUser/{module}/add. Body: ids, data (users)."""
15
+ path = f"/rest/changeUser/{module}/add"
16
+ return await self._post(path, json={"ids": ids, "data": users})
17
+
18
+ async def remove_users(
19
+ self, module: str, ids: List[Any], users: Any
20
+ ) -> Dict[str, Any]:
21
+ """POST /rest/changeUser/{module}/remove."""
22
+ path = f"/rest/changeUser/{module}/remove"
23
+ return await self._post(path, json={"ids": ids, "data": users})
24
+
25
+ async def define_users(
26
+ self, module: str, ids: List[Any], users: Any
27
+ ) -> Dict[str, Any]:
28
+ """POST /rest/changeUser/{module}/define."""
29
+ path = f"/rest/changeUser/{module}/define"
30
+ return await self._post(path, json={"ids": ids, "data": users})
31
+
32
+ async def replace_users(
33
+ self,
34
+ module: str,
35
+ ids: List[Any],
36
+ *,
37
+ from_user: Any = None,
38
+ to_user: Any = None,
39
+ ) -> Dict[str, Any]:
40
+ """POST /rest/changeUser/{module}/replace. Body: ids, data: { from?, to? }."""
41
+ path = f"/rest/changeUser/{module}/replace"
42
+ data: Dict[str, Any] = {}
43
+ if from_user is not None:
44
+ data["from"] = from_user
45
+ if to_user is not None:
46
+ data["to"] = to_user
47
+ return await self._post(path, json={"ids": ids, "data": data})
48
+
49
+ async def count_inactive(self, module: str, ids: List[Any]) -> Dict[str, Any]:
50
+ """POST /rest/changeUser/{module}/countInactive. Body: ids."""
51
+ path = f"/rest/changeUser/{module}/countInactive"
52
+ return await self._post(path, json={"ids": ids})
53
+
54
+ async def remove_inactive(self, module: str, ids: List[Any]) -> Dict[str, Any]:
55
+ """POST /rest/changeUser/{module}/removeInactive. Body: ids."""
56
+ path = f"/rest/changeUser/{module}/removeInactive"
57
+ return await self._post(path, json={"ids": ids})
58
+
59
+ async def set_queue(
60
+ self, module: str, ids: List[Any], queue: Any
61
+ ) -> Dict[str, Any]:
62
+ """POST /rest/changeUser/{module}/setQueue. Body: ids, data (queue config)."""
63
+ path = f"/rest/changeUser/{module}/setQueue"
64
+ return await self._post(path, json={"ids": ids, "data": queue})
@@ -0,0 +1,89 @@
1
+ """Comments API: list, create, update, delete, search users, search comments."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .base import BaseService
6
+
7
+
8
+ class CommentsService(BaseService):
9
+ """Service for record comments."""
10
+
11
+ async def get_comments(self, module: str, data_id: str) -> Dict[str, Any]:
12
+ """GET /rest/comment/{module}/{data_id}. Returns API result (success, data)."""
13
+ path = f"/rest/comment/{module}/{data_id}"
14
+ return await self._get(path)
15
+
16
+ async def create_comment(
17
+ self,
18
+ module: str,
19
+ data_id: str,
20
+ text: str,
21
+ parent_id: Optional[str] = None,
22
+ ) -> Dict[str, Any]:
23
+ """POST /rest/comment/{module}/{data_id}. Body: text, parentId optional."""
24
+ path = f"/rest/comment/{module}/{data_id}"
25
+ body: Dict[str, Any] = {"text": text}
26
+ if parent_id is not None:
27
+ body["parentId"] = parent_id
28
+ return await self._post(path, json=body)
29
+
30
+ async def update_comment(
31
+ self,
32
+ module: str,
33
+ data_id: str,
34
+ comment_id: str,
35
+ text: str,
36
+ ) -> Dict[str, Any]:
37
+ """PUT /rest/comment/{module}/{data_id}/{comment_id}. Body: text."""
38
+ path = f"/rest/comment/{module}/{data_id}/{comment_id}"
39
+ return await self._put(path, json={"text": text})
40
+
41
+ async def delete_comment(
42
+ self,
43
+ module: str,
44
+ data_id: str,
45
+ comment_id: str,
46
+ ) -> Dict[str, Any]:
47
+ """DELETE /rest/comment/{module}/{data_id}/{comment_id}."""
48
+ path = f"/rest/comment/{module}/{data_id}/{comment_id}"
49
+ return await self._delete(path)
50
+
51
+ async def search_comment_users(
52
+ self,
53
+ module: str,
54
+ data_id: str,
55
+ query: str = "",
56
+ ) -> Dict[str, Any]:
57
+ """GET /rest/comment/{module}/{data_id}/users/search?q=."""
58
+ path = f"/rest/comment/{module}/{data_id}/users/search"
59
+ params = {"q": query} if query else {}
60
+ return await self._get(path, params=params or None)
61
+
62
+ async def search_comments(
63
+ self,
64
+ module: str,
65
+ data_id: str,
66
+ *,
67
+ query: Optional[str] = None,
68
+ author_id: Optional[str] = None,
69
+ start_date: Optional[str] = None,
70
+ end_date: Optional[str] = None,
71
+ page: Optional[int] = None,
72
+ limit: Optional[int] = None,
73
+ ) -> Dict[str, Any]:
74
+ """GET /rest/comment/{module}/{data_id}/search with query params."""
75
+ path = f"/rest/comment/{module}/{data_id}/search"
76
+ params: Dict[str, str] = {}
77
+ if query is not None:
78
+ params["q"] = query
79
+ if author_id is not None:
80
+ params["authorId"] = author_id
81
+ if start_date is not None:
82
+ params["startDate"] = start_date
83
+ if end_date is not None:
84
+ params["endDate"] = end_date
85
+ if page is not None:
86
+ params["page"] = str(page)
87
+ if limit is not None:
88
+ params["limit"] = str(limit)
89
+ return await self._get(path, params=params or None)
@@ -0,0 +1,54 @@
1
+ """Export list as CSV, XLSX, or JSON (bytes)."""
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Literal, Optional, Union
5
+
6
+ from ..serialization import json_serial
7
+ from ..filters import KonectyFilter
8
+ from .base import BaseService
9
+
10
+ ExportFormat = Literal["csv", "xlsx", "json", "xls"]
11
+
12
+
13
+ class ExportService(BaseService):
14
+ """Service for exporting lists (GET returns file bytes)."""
15
+
16
+ async def export_list(
17
+ self,
18
+ module: str,
19
+ list_name: str,
20
+ format: ExportFormat,
21
+ *,
22
+ filter_params: Optional[KonectyFilter] = None,
23
+ sort: Optional[Any] = None,
24
+ fields: Optional[List[Union[str, int]]] = None,
25
+ start: Optional[int] = None,
26
+ limit: Optional[int] = None,
27
+ display_name: Optional[str] = None,
28
+ display_type: Optional[str] = None,
29
+ ) -> bytes:
30
+ """
31
+ Export list as file. GET /rest/data/{module}/list/{list_name}/{type}.
32
+ type is csv, xlsx, or json (xls is normalized to xlsx).
33
+ Returns file content as bytes.
34
+ """
35
+ export_type = "xlsx" if format == "xls" else format
36
+ if export_type not in ("csv", "xlsx", "json"):
37
+ raise ValueError("format must be csv, xlsx, json, or xls")
38
+ params: Dict[str, str] = {}
39
+ if filter_params is not None:
40
+ params["filter"] = json.dumps(filter_params.to_json(), default=json_serial)
41
+ if sort is not None:
42
+ params["sort"] = json.dumps(sort, default=json_serial) if not isinstance(sort, str) else sort
43
+ if fields is not None:
44
+ params["fields"] = ",".join(str(f) for f in fields)
45
+ if start is not None:
46
+ params["start"] = str(start)
47
+ if limit is not None:
48
+ params["limit"] = str(limit)
49
+ if display_name is not None:
50
+ params["displayName"] = display_name
51
+ if display_type is not None:
52
+ params["displayType"] = display_type
53
+ path = f"/rest/data/{module}/list/{list_name}/{export_type}"
54
+ return await self._get(path, params=params, return_bytes=True)
@@ -0,0 +1,59 @@
1
+ """File download and image download (binary responses)."""
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from .base import BaseService
6
+
7
+
8
+ class FilesService(BaseService):
9
+ """Service for downloading files and images (GET returning bytes)."""
10
+
11
+ async def download_file(
12
+ self,
13
+ module: str,
14
+ record_code: str,
15
+ field_name: str,
16
+ file_name: str,
17
+ ) -> bytes:
18
+ """
19
+ Download a file from a record field. GET /rest/file/{module}/{record_code}/{field_name}/{file_name}.
20
+
21
+ Args:
22
+ module: Document/module name.
23
+ record_code: Record code (id).
24
+ field_name: Field name holding the file.
25
+ file_name: File name.
26
+
27
+ Returns:
28
+ File content as bytes.
29
+ """
30
+ path = f"/rest/file/{module}/{record_code}/{field_name}/{file_name}"
31
+ return await self._get(path, return_bytes=True)
32
+
33
+ async def download_image(
34
+ self,
35
+ module: str,
36
+ record_id: str,
37
+ field_name: str,
38
+ file_name: str,
39
+ *,
40
+ style: Optional[Literal["full", "thumb", "wm"]] = None,
41
+ ) -> bytes:
42
+ """
43
+ Download an image from a record field. GET /rest/image/... with optional style (full, thumb, watermark).
44
+
45
+ Args:
46
+ module: Document/module name.
47
+ record_id: Record id.
48
+ field_name: Field name holding the image.
49
+ file_name: File name.
50
+ style: None = full file; "thumb" = thumbnail; "wm" = watermark.
51
+
52
+ Returns:
53
+ Image content as bytes.
54
+ """
55
+ if style is not None:
56
+ path = f"/rest/image/{style}/{module}/{record_id}/{field_name}/{file_name}"
57
+ else:
58
+ path = f"/rest/image/{module}/{record_id}/{field_name}/{file_name}"
59
+ return await self._get(path, return_bytes=True)
@@ -0,0 +1,41 @@
1
+ """Notifications API: list, unread count, mark read, mark all read."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .base import BaseService
6
+
7
+
8
+ class NotificationsService(BaseService):
9
+ """Service for user notifications."""
10
+
11
+ async def list_notifications(
12
+ self,
13
+ *,
14
+ read: Optional[bool] = None,
15
+ page: Optional[int] = None,
16
+ limit: Optional[int] = None,
17
+ ) -> Dict[str, Any]:
18
+ """GET /rest/notifications. Query: read, page, limit."""
19
+ params: Dict[str, str] = {}
20
+ if read is not None:
21
+ params["read"] = "true" if read else "false"
22
+ if page is not None:
23
+ params["page"] = str(page)
24
+ if limit is not None:
25
+ params["limit"] = str(limit)
26
+ return await self._get(
27
+ "/rest/notifications", params=params if params else None
28
+ )
29
+
30
+ async def get_unread_count(self) -> Dict[str, Any]:
31
+ """GET /rest/notifications/unread-count."""
32
+ return await self._get("/rest/notifications/unread-count")
33
+
34
+ async def mark_notification_read(self, notification_id: str) -> Dict[str, Any]:
35
+ """PUT /rest/notifications/{id}/read."""
36
+ path = f"/rest/notifications/{notification_id}/read"
37
+ return await self._put(path)
38
+
39
+ async def mark_all_notifications_read(self) -> Dict[str, Any]:
40
+ """PUT /rest/notifications/read-all."""
41
+ return await self._put("/rest/notifications/read-all")
@@ -0,0 +1,192 @@
1
+ """Custom query (query/json, query/sql) and Saved Queries CRUD."""
2
+
3
+ import json
4
+ from typing import Any, AsyncGenerator, Dict, List, Optional, Union
5
+
6
+ from ..types import KonectyDict
7
+ from ..exceptions import KonectyAPIError
8
+ from ..http import StreamResponse
9
+ from .base import BaseService
10
+
11
+
12
+ class QueryResult:
13
+ """Result of execute_query_json or execute_query_sql. Stream of records + optional total and meta."""
14
+
15
+ __slots__ = ("stream", "total", "meta")
16
+
17
+ def __init__(
18
+ self,
19
+ stream: AsyncGenerator[KonectyDict, None],
20
+ total: Optional[int] = None,
21
+ meta: Optional[Dict[str, Any]] = None,
22
+ ) -> None:
23
+ self.stream = stream
24
+ self.total = total
25
+ self.meta = meta
26
+
27
+
28
+ async def _build_query_result(
29
+ raw: StreamResponse,
30
+ include_meta: bool,
31
+ include_total: bool,
32
+ ) -> QueryResult:
33
+ """Read first line (maybe _meta), then return QueryResult. Generator consumes the rest."""
34
+ total: Optional[int] = None
35
+ meta: Optional[Dict[str, Any]] = None
36
+ if include_total:
37
+ tc = raw.headers.get("x-total-count")
38
+ if tc is not None:
39
+ try:
40
+ total = int(tc)
41
+ except ValueError:
42
+ pass
43
+
44
+ first_record: Optional[KonectyDict] = None
45
+ while True:
46
+ line = await raw.readline()
47
+ if not line:
48
+ break
49
+ line = line.strip()
50
+ if not line:
51
+ continue
52
+ obj = json.loads(line.decode("utf-8"))
53
+ if isinstance(obj, dict) and "_meta" in obj:
54
+ m = obj["_meta"]
55
+ if m.get("success") is False:
56
+ errors = m.get("errors", [])
57
+ raise KonectyAPIError(errors)
58
+ if include_meta:
59
+ meta = m
60
+ if total is None and "total" in m:
61
+ total = m["total"]
62
+ continue
63
+ first_record = obj
64
+ break
65
+
66
+ async def gen() -> AsyncGenerator[KonectyDict, None]:
67
+ if first_record is not None:
68
+ yield first_record
69
+ async with raw:
70
+ while True:
71
+ line = await raw.readline()
72
+ if not line:
73
+ break
74
+ line = line.strip()
75
+ if not line:
76
+ continue
77
+ yield json.loads(line.decode("utf-8"))
78
+
79
+ return QueryResult(stream=gen(), total=total, meta=meta)
80
+
81
+
82
+ class QueryService(BaseService):
83
+ """Service for custom query (JSON/SQL) and saved queries."""
84
+
85
+ async def execute_query_json(
86
+ self,
87
+ body: Union[Dict[str, Any], Any],
88
+ *,
89
+ include_total: bool = True,
90
+ include_meta: bool = False,
91
+ ) -> QueryResult:
92
+ """
93
+ Execute a cross-module query. POST /rest/query/json. Body is CrossModuleQuery (dict or model).
94
+ Returns QueryResult with .stream (async generator), .total, .meta.
95
+ """
96
+ if hasattr(body, "to_dict"):
97
+ payload = body.to_dict()
98
+ elif hasattr(body, "model_dump"):
99
+ payload = body.model_dump(by_alias=True, exclude_none=True)
100
+ else:
101
+ payload = dict(body)
102
+ path = "/rest/query/json"
103
+ raw = await self._post(path, json=payload, stream=True)
104
+ if not isinstance(raw, StreamResponse):
105
+ raise KonectyAPIError(["Unexpected non-stream response"])
106
+ return await _build_query_result(
107
+ raw, include_meta=include_meta, include_total=include_total
108
+ )
109
+
110
+ async def execute_query_sql(
111
+ self,
112
+ sql: str,
113
+ *,
114
+ include_total: bool = True,
115
+ include_meta: bool = False,
116
+ ) -> QueryResult:
117
+ """
118
+ Execute SQL (converted to cross-module query server-side). POST /rest/query/sql.
119
+ Returns QueryResult. On SQL parse error the server returns 400 with _meta.errors.
120
+ """
121
+ payload: Dict[str, Any] = {"sql": sql}
122
+ if include_total is not None:
123
+ payload["includeTotal"] = include_total
124
+ if include_meta is not None:
125
+ payload["includeMeta"] = include_meta
126
+ path = "/rest/query/sql"
127
+ raw = await self._post(path, json=payload, stream=True)
128
+ if not isinstance(raw, StreamResponse):
129
+ raise KonectyAPIError(["Unexpected non-stream response"])
130
+ return await _build_query_result(
131
+ raw, include_meta=include_meta, include_total=include_total
132
+ )
133
+
134
+ # --- Saved queries ---
135
+
136
+ async def list_saved_queries(self) -> Dict[str, Any]:
137
+ """GET /rest/query/saved."""
138
+ return await self._get("/rest/query/saved")
139
+
140
+ async def get_saved_query(self, query_id: str) -> Dict[str, Any]:
141
+ """GET /rest/query/saved/{id}."""
142
+ path = f"/rest/query/saved/{query_id}"
143
+ return await self._get(path)
144
+
145
+ async def create_saved_query(
146
+ self,
147
+ name: str,
148
+ query: Dict[str, Any],
149
+ description: Optional[str] = None,
150
+ ) -> Dict[str, Any]:
151
+ """POST /rest/query/saved. Body: name, query (CrossModuleQuery), description?."""
152
+ body: Dict[str, Any] = {"name": name, "query": query}
153
+ if description is not None:
154
+ body["description"] = description
155
+ return await self._post("/rest/query/saved", json=body)
156
+
157
+ async def update_saved_query(
158
+ self,
159
+ query_id: str,
160
+ *,
161
+ name: Optional[str] = None,
162
+ description: Optional[str] = None,
163
+ query: Optional[Dict[str, Any]] = None,
164
+ ) -> Dict[str, Any]:
165
+ """PUT /rest/query/saved/{id}. Body: name?, description?, query?."""
166
+ body: Dict[str, Any] = {}
167
+ if name is not None:
168
+ body["name"] = name
169
+ if description is not None:
170
+ body["description"] = description
171
+ if query is not None:
172
+ body["query"] = query
173
+ path = f"/rest/query/saved/{query_id}"
174
+ return await self._put(path, json=body)
175
+
176
+ async def delete_saved_query(self, query_id: str) -> Dict[str, Any]:
177
+ """DELETE /rest/query/saved/{id}."""
178
+ path = f"/rest/query/saved/{query_id}"
179
+ return await self._delete(path)
180
+
181
+ async def share_saved_query(
182
+ self,
183
+ query_id: str,
184
+ shared_with: List[Dict[str, Any]],
185
+ is_public: Optional[bool] = None,
186
+ ) -> Dict[str, Any]:
187
+ """PATCH /rest/query/saved/{id}/share. Body: sharedWith (array of { type, _id, name }), isPublic?."""
188
+ body: Dict[str, Any] = {"sharedWith": shared_with}
189
+ if is_public is not None:
190
+ body["isPublic"] = is_public
191
+ path = f"/rest/query/saved/{query_id}/share"
192
+ return await self._patch(path, json=body)
@@ -0,0 +1,133 @@
1
+ """Stream API: find_stream (NDJSON) and count_stream."""
2
+
3
+ import json
4
+ from typing import Any, AsyncGenerator, Dict, Optional
5
+
6
+ from ..serialization import json_serial
7
+ from ..types import KonectyDict
8
+ from ..filters import KonectyFilter, KonectyFindParams
9
+ from ..http import StreamResponse
10
+ from .base import BaseService
11
+
12
+
13
+ def _find_params_to_query(options: KonectyFindParams) -> Dict[str, str]:
14
+ """Build query params dict from KonectyFindParams (same as client.find)."""
15
+ params: Dict[str, str] = {}
16
+ for key, value in options.model_dump(exclude_none=True).items():
17
+ params[key] = (
18
+ json.dumps(value, default=json_serial)
19
+ if key != "fields"
20
+ else ",".join(str(f) for f in value)
21
+ )
22
+ return params
23
+
24
+
25
+ class FindStreamResult:
26
+ """Result of find_stream. Has .stream (async generator of records) and .total (int | None)."""
27
+
28
+ __slots__ = ("stream", "total")
29
+
30
+ def __init__(
31
+ self,
32
+ stream: AsyncGenerator[KonectyDict, None],
33
+ total: Optional[int],
34
+ ) -> None:
35
+ self.stream = stream
36
+ self.total = total
37
+
38
+
39
+ class StreamService(BaseService):
40
+ """Service for stream endpoints: findStream and count."""
41
+
42
+ async def find_stream(
43
+ self,
44
+ module: str,
45
+ options: KonectyFindParams,
46
+ *,
47
+ include_total: bool = False,
48
+ ) -> FindStreamResult:
49
+ """
50
+ Stream records as NDJSON (one JSON object per line). Uses GET /rest/stream/{module}/findStream.
51
+
52
+ Args:
53
+ module: Document/module name.
54
+ options: Find params (filter, sort, limit, start, fields).
55
+ include_total: If True, request total count; it will be in result.total and X-Total-Count header.
56
+
57
+ Returns:
58
+ FindStreamResult with .stream (async generator of dicts) and .total (int | None when include_total).
59
+ """
60
+ params = _find_params_to_query(options)
61
+ if include_total:
62
+ params["includeTotal"] = "1"
63
+ path = f"/rest/stream/{module}/findStream"
64
+ raw = await self._get(path, params=params, stream=True)
65
+ assert isinstance(raw, StreamResponse)
66
+ total: Optional[int] = None
67
+ if include_total:
68
+ tc = raw.headers.get("x-total-count")
69
+ if tc is not None:
70
+ try:
71
+ total = int(tc)
72
+ except ValueError:
73
+ pass
74
+
75
+ async def gen() -> AsyncGenerator[KonectyDict, None]:
76
+ async with raw:
77
+ while True:
78
+ line = await raw.readline()
79
+ if not line:
80
+ break
81
+ line = line.strip()
82
+ if not line:
83
+ continue
84
+ yield json.loads(line.decode("utf-8"))
85
+
86
+ return FindStreamResult(stream=gen(), total=total)
87
+
88
+ async def count_stream(
89
+ self,
90
+ module: str,
91
+ filter_params: Optional[KonectyFilter] = None,
92
+ *,
93
+ display_name: Optional[str] = None,
94
+ display_type: Optional[str] = None,
95
+ sort: Optional[Any] = None,
96
+ with_detail_fields: Optional[bool] = None,
97
+ ) -> int:
98
+ """
99
+ Return total count for the module matching the filter. GET /rest/stream/{module}/count.
100
+
101
+ Args:
102
+ module: Document/module name.
103
+ filter_params: Optional filter. If None, uses empty filter (count all, subject to permissions).
104
+ display_name: Optional.
105
+ display_type: Optional.
106
+ sort: Optional sort (string or list).
107
+ with_detail_fields: Optional.
108
+
109
+ Returns:
110
+ Total count.
111
+ """
112
+ filter_obj = filter_params or KonectyFilter.create()
113
+ options = KonectyFindParams(filter=filter_obj)
114
+ params: Dict[str, str] = {}
115
+ for key, value in options.model_dump(exclude_none=True).items():
116
+ params[key] = (
117
+ json.dumps(value, default=json_serial)
118
+ if key != "fields"
119
+ else ",".join(str(f) for f in value)
120
+ )
121
+ if display_name is not None:
122
+ params["displayName"] = str(display_name)
123
+ if display_type is not None:
124
+ params["displayType"] = str(display_type)
125
+ if sort is not None:
126
+ params["sort"] = json.dumps(sort, default=json_serial) if not isinstance(sort, str) else sort
127
+ if with_detail_fields is not None:
128
+ params["withDetailFields"] = "1" if with_detail_fields else "0"
129
+ path = f"/rest/stream/{module}/count"
130
+ result = await self._get(path, params=params)
131
+ if isinstance(result, dict) and "total" in result:
132
+ return int(result["total"])
133
+ return 0
@@ -0,0 +1,26 @@
1
+ """Subscriptions API: get status, subscribe, unsubscribe."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ from .base import BaseService
6
+
7
+
8
+ class SubscriptionsService(BaseService):
9
+ """Service for record subscription (watch) status."""
10
+
11
+ async def get_subscription_status(
12
+ self, module: str, data_id: str
13
+ ) -> Dict[str, Any]:
14
+ """GET /rest/subscriptions/{module}/{data_id}. Returns success and subscription data."""
15
+ path = f"/rest/subscriptions/{module}/{data_id}"
16
+ return await self._get(path)
17
+
18
+ async def subscribe(self, module: str, data_id: str) -> Dict[str, Any]:
19
+ """POST /rest/subscriptions/{module}/{data_id}. Subscribe to record notifications."""
20
+ path = f"/rest/subscriptions/{module}/{data_id}"
21
+ return await self._post(path)
22
+
23
+ async def unsubscribe(self, module: str, data_id: str) -> Dict[str, Any]:
24
+ """DELETE /rest/subscriptions/{module}/{data_id}."""
25
+ path = f"/rest/subscriptions/{module}/{data_id}"
26
+ return await self._delete(path)