collibra-connector 1.0.18__py3-none-any.whl → 1.1.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.
@@ -0,0 +1,204 @@
1
+ """
2
+ Attribute API module for Collibra Connector.
3
+
4
+ Provides methods for working with asset attributes.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import uuid
9
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
10
+
11
+ from .Base import BaseAPI
12
+
13
+ if TYPE_CHECKING:
14
+ pass
15
+
16
+
17
+ class Attribute(BaseAPI):
18
+ """API class for attribute operations."""
19
+
20
+ def __init__(self, connector: Any) -> None:
21
+ """Initialize the Attribute API."""
22
+ super().__init__(connector)
23
+ self.__base_api = connector.api + "/attributes"
24
+
25
+ def get_attributes(
26
+ self,
27
+ asset_id: str,
28
+ type_ids: Optional[List[str]] = None,
29
+ limit: int = 100,
30
+ offset: int = 0
31
+ ) -> Dict[str, Any]:
32
+ """
33
+ Get all attributes for an asset.
34
+
35
+ Args:
36
+ asset_id: The UUID of the asset.
37
+ type_ids: Optional list of attribute type IDs to filter by.
38
+ limit: Maximum number of results.
39
+ offset: Offset for pagination.
40
+
41
+ Returns:
42
+ Dictionary with 'results' list of attributes.
43
+
44
+ Example:
45
+ >>> attrs = connector.attribute.get_attributes("asset-uuid")
46
+ >>> for attr in attrs['results']:
47
+ ... print(f"{attr['type']['name']}: {attr['value']}")
48
+ """
49
+ if not asset_id:
50
+ raise ValueError("asset_id is required")
51
+ if not isinstance(asset_id, str):
52
+ raise ValueError("asset_id must be a string")
53
+
54
+ try:
55
+ uuid.UUID(asset_id)
56
+ except ValueError as exc:
57
+ raise ValueError("asset_id must be a valid UUID") from exc
58
+
59
+ params: Dict[str, Any] = {
60
+ "assetId": asset_id,
61
+ "limit": limit,
62
+ "offset": offset
63
+ }
64
+
65
+ if type_ids:
66
+ params["typeIds"] = type_ids
67
+
68
+ response = self._get(url=self.__base_api, params=params)
69
+ return self._handle_response(response)
70
+
71
+ def get_attribute(self, attribute_id: str) -> Dict[str, Any]:
72
+ """
73
+ Get a specific attribute by ID.
74
+
75
+ Args:
76
+ attribute_id: The UUID of the attribute.
77
+
78
+ Returns:
79
+ Attribute details.
80
+ """
81
+ if not attribute_id:
82
+ raise ValueError("attribute_id is required")
83
+
84
+ try:
85
+ uuid.UUID(attribute_id)
86
+ except ValueError as exc:
87
+ raise ValueError("attribute_id must be a valid UUID") from exc
88
+
89
+ response = self._get(url=f"{self.__base_api}/{attribute_id}")
90
+ return self._handle_response(response)
91
+
92
+ def add_attribute(
93
+ self,
94
+ asset_id: str,
95
+ type_id: str,
96
+ value: Any
97
+ ) -> Dict[str, Any]:
98
+ """
99
+ Add an attribute to an asset.
100
+
101
+ Args:
102
+ asset_id: The UUID of the asset.
103
+ type_id: The UUID of the attribute type.
104
+ value: The value of the attribute.
105
+
106
+ Returns:
107
+ Created attribute details.
108
+ """
109
+ if not asset_id or not type_id:
110
+ raise ValueError("asset_id and type_id are required")
111
+
112
+ for param_name, param_value in [("asset_id", asset_id), ("type_id", type_id)]:
113
+ try:
114
+ uuid.UUID(param_value)
115
+ except ValueError as exc:
116
+ raise ValueError(f"{param_name} must be a valid UUID") from exc
117
+
118
+ data = {
119
+ "assetId": asset_id,
120
+ "typeId": type_id,
121
+ "value": value
122
+ }
123
+
124
+ response = self._post(url=self.__base_api, data=data)
125
+ return self._handle_response(response)
126
+
127
+ def change_attribute(
128
+ self,
129
+ attribute_id: str,
130
+ value: Any
131
+ ) -> Dict[str, Any]:
132
+ """
133
+ Update an attribute value.
134
+
135
+ Args:
136
+ attribute_id: The UUID of the attribute.
137
+ value: The new value.
138
+
139
+ Returns:
140
+ Updated attribute details.
141
+ """
142
+ if not attribute_id:
143
+ raise ValueError("attribute_id is required")
144
+
145
+ try:
146
+ uuid.UUID(attribute_id)
147
+ except ValueError as exc:
148
+ raise ValueError("attribute_id must be a valid UUID") from exc
149
+
150
+ data = {
151
+ "id": attribute_id,
152
+ "value": value
153
+ }
154
+
155
+ response = self._patch(url=f"{self.__base_api}/{attribute_id}", data=data)
156
+ return self._handle_response(response)
157
+
158
+ def remove_attribute(self, attribute_id: str) -> Dict[str, Any]:
159
+ """
160
+ Remove an attribute.
161
+
162
+ Args:
163
+ attribute_id: The UUID of the attribute to remove.
164
+
165
+ Returns:
166
+ Response from the removal operation.
167
+ """
168
+ if not attribute_id:
169
+ raise ValueError("attribute_id is required")
170
+
171
+ try:
172
+ uuid.UUID(attribute_id)
173
+ except ValueError as exc:
174
+ raise ValueError("attribute_id must be a valid UUID") from exc
175
+
176
+ response = self._delete(url=f"{self.__base_api}/{attribute_id}")
177
+ return self._handle_response(response)
178
+
179
+ def get_attributes_as_dict(self, asset_id: str) -> Dict[str, Any]:
180
+ """
181
+ Get all attributes for an asset as a simple dictionary.
182
+
183
+ Convenience method that returns attribute values keyed by attribute type name.
184
+
185
+ Args:
186
+ asset_id: The UUID of the asset.
187
+
188
+ Returns:
189
+ Dictionary mapping attribute type names to values.
190
+
191
+ Example:
192
+ >>> attrs = connector.attribute.get_attributes_as_dict("asset-uuid")
193
+ >>> print(attrs['Description'])
194
+ >>> print(attrs['Personal Data Classification'])
195
+ """
196
+ result = self.get_attributes(asset_id, limit=500)
197
+ attrs_dict: Dict[str, Any] = {}
198
+
199
+ for attr in result.get('results', []):
200
+ type_name = attr.get('type', {}).get('name', 'Unknown')
201
+ value = attr.get('value')
202
+ attrs_dict[type_name] = value
203
+
204
+ return attrs_dict
@@ -123,9 +123,9 @@ class BaseAPI:
123
123
  :param response: The response object from the API request.
124
124
  :return: The JSON content of the response if successful, otherwise raises an error.
125
125
  """
126
- if response.status_code in [200, 201]:
126
+ if response.status_code in [200, 201, 204]:
127
127
  # Check if response has content before trying to parse JSON
128
- if response.text.strip():
128
+ if response.status_code != 204 and response.text.strip():
129
129
  try:
130
130
  return response.json()
131
131
  except ValueError as e:
@@ -108,7 +108,7 @@ class Community(BaseAPI):
108
108
  if sort_order != "ASC":
109
109
  params["sortOrder"] = sort_order
110
110
 
111
- response = self._get(params=params)
111
+ response = self._get(url=self.__base_api, params=params)
112
112
  return self._handle_response(response)
113
113
 
114
114
  def add_community(
@@ -204,3 +204,219 @@ class Relation(BaseAPI):
204
204
 
205
205
  response = self._patch(url=f"{self.__base_api}/{relation_id}", data=data)
206
206
  return self._handle_response(response)
207
+
208
+ def find_relations(
209
+ self,
210
+ source_id: str = None,
211
+ target_id: str = None,
212
+ type_id: str = None,
213
+ source_type_id: str = None,
214
+ target_type_id: str = None,
215
+ limit: int = 100,
216
+ offset: int = 0
217
+ ):
218
+ """
219
+ Find relations matching the given criteria.
220
+
221
+ Args:
222
+ source_id: Filter by source asset ID.
223
+ target_id: Filter by target asset ID.
224
+ type_id: Filter by relation type ID.
225
+ source_type_id: Filter by source asset type ID.
226
+ target_type_id: Filter by target asset type ID.
227
+ limit: Maximum results to retrieve (max 1000).
228
+ offset: First result to retrieve.
229
+
230
+ Returns:
231
+ List of relations matching criteria.
232
+
233
+ Example:
234
+ >>> # Get all outgoing relations from an asset
235
+ >>> relations = connector.relation.find_relations(source_id="asset-uuid")
236
+ >>> # Get all incoming relations to an asset
237
+ >>> relations = connector.relation.find_relations(target_id="asset-uuid")
238
+ """
239
+ params = {
240
+ "limit": limit,
241
+ "offset": offset
242
+ }
243
+
244
+ # Validate and add source_id
245
+ if source_id is not None:
246
+ if not isinstance(source_id, str):
247
+ raise ValueError("source_id must be a string")
248
+ try:
249
+ uuid.UUID(source_id)
250
+ except ValueError as exc:
251
+ raise ValueError("source_id must be a valid UUID") from exc
252
+ params["sourceId"] = source_id
253
+
254
+ # Validate and add target_id
255
+ if target_id is not None:
256
+ if not isinstance(target_id, str):
257
+ raise ValueError("target_id must be a string")
258
+ try:
259
+ uuid.UUID(target_id)
260
+ except ValueError as exc:
261
+ raise ValueError("target_id must be a valid UUID") from exc
262
+ params["targetId"] = target_id
263
+
264
+ # Validate and add type_id
265
+ if type_id is not None:
266
+ if not isinstance(type_id, str):
267
+ raise ValueError("type_id must be a string")
268
+ try:
269
+ uuid.UUID(type_id)
270
+ except ValueError as exc:
271
+ raise ValueError("type_id must be a valid UUID") from exc
272
+ params["typeId"] = type_id
273
+
274
+ # Add optional type filters
275
+ if source_type_id is not None:
276
+ params["sourceTypeId"] = source_type_id
277
+ if target_type_id is not None:
278
+ params["targetTypeId"] = target_type_id
279
+
280
+ response = self._get(url=self.__base_api, params=params)
281
+ return self._handle_response(response)
282
+
283
+ def get_relation_type(self, relation_type_id: str):
284
+ """
285
+ Get relation type details by ID.
286
+
287
+ Args:
288
+ relation_type_id: The UUID of the relation type.
289
+
290
+ Returns:
291
+ Relation type details including role and coRole names.
292
+ """
293
+ if not relation_type_id:
294
+ raise ValueError("relation_type_id is required")
295
+
296
+ try:
297
+ uuid.UUID(relation_type_id)
298
+ except ValueError as exc:
299
+ raise ValueError("relation_type_id must be a valid UUID") from exc
300
+
301
+ url = self._BaseAPI__connector.api + f"/relationTypes/{relation_type_id}"
302
+ response = self._get(url=url)
303
+ return self._handle_response(response)
304
+
305
+ def get_asset_relations(
306
+ self,
307
+ asset_id: str,
308
+ direction: str = "BOTH",
309
+ limit: int = 500,
310
+ include_type_details: bool = True
311
+ ):
312
+ """
313
+ Get all relations for an asset, grouped by direction and type.
314
+
315
+ Convenience method that returns a structured view of all relations.
316
+
317
+ Args:
318
+ asset_id: The UUID of the asset.
319
+ direction: "OUTGOING", "INCOMING", or "BOTH".
320
+ limit: Maximum relations per direction.
321
+ include_type_details: If True, fetches relation type names (slower but more informative).
322
+
323
+ Returns:
324
+ Dictionary with 'outgoing' and 'incoming' keys, each containing
325
+ relations grouped by relation type name.
326
+
327
+ Example:
328
+ >>> relations = connector.relation.get_asset_relations("asset-uuid")
329
+ >>> print(relations['outgoing']) # Relations where asset is source
330
+ >>> print(relations['incoming']) # Relations where asset is target
331
+ """
332
+ if not asset_id:
333
+ raise ValueError("asset_id is required")
334
+
335
+ try:
336
+ uuid.UUID(asset_id)
337
+ except ValueError as exc:
338
+ raise ValueError("asset_id must be a valid UUID") from exc
339
+
340
+ result = {
341
+ "outgoing": {},
342
+ "incoming": {},
343
+ "outgoing_count": 0,
344
+ "incoming_count": 0
345
+ }
346
+
347
+ # Cache for relation type details
348
+ type_cache = {}
349
+
350
+ def get_type_name(type_id, is_source=True):
351
+ """Get relation type name, using cache."""
352
+ if not include_type_details or not type_id:
353
+ return "Unknown"
354
+
355
+ if type_id not in type_cache:
356
+ try:
357
+ type_details = self.get_relation_type(type_id)
358
+ type_cache[type_id] = type_details
359
+ except Exception:
360
+ type_cache[type_id] = {}
361
+
362
+ cached = type_cache.get(type_id, {})
363
+ if is_source:
364
+ return cached.get("role", "Unknown")
365
+ else:
366
+ return cached.get("coRole", "Unknown")
367
+
368
+ # Get outgoing relations
369
+ if direction in ("BOTH", "OUTGOING"):
370
+ outgoing = self.find_relations(source_id=asset_id, limit=limit)
371
+ result["outgoing_count"] = outgoing.get("total", 0)
372
+
373
+ for rel in outgoing.get("results", []):
374
+ rel_type = rel.get("type", {})
375
+ type_id = rel_type.get("id")
376
+ type_name = get_type_name(type_id, is_source=True)
377
+ target = rel.get("target", {})
378
+
379
+ # Get target asset type by looking up the relation type
380
+ target_type_name = None
381
+ if type_id in type_cache:
382
+ target_type_name = type_cache[type_id].get("targetType", {}).get("name")
383
+
384
+ if type_name not in result["outgoing"]:
385
+ result["outgoing"][type_name] = []
386
+
387
+ result["outgoing"][type_name].append({
388
+ "id": rel.get("id"),
389
+ "target_id": target.get("id"),
390
+ "target_name": target.get("name"),
391
+ "target_type": target_type_name or target.get("type", {}).get("name"),
392
+ "target_status": target.get("status", {}).get("name") if target.get("status") else "N/A"
393
+ })
394
+
395
+ # Get incoming relations
396
+ if direction in ("BOTH", "INCOMING"):
397
+ incoming = self.find_relations(target_id=asset_id, limit=limit)
398
+ result["incoming_count"] = incoming.get("total", 0)
399
+
400
+ for rel in incoming.get("results", []):
401
+ rel_type = rel.get("type", {})
402
+ type_id = rel_type.get("id")
403
+ type_name = get_type_name(type_id, is_source=False)
404
+ source = rel.get("source", {})
405
+
406
+ # Get source asset type by looking up the relation type
407
+ source_type_name = None
408
+ if type_id in type_cache:
409
+ source_type_name = type_cache[type_id].get("sourceType", {}).get("name")
410
+
411
+ if type_name not in result["incoming"]:
412
+ result["incoming"][type_name] = []
413
+
414
+ result["incoming"][type_name].append({
415
+ "id": rel.get("id"),
416
+ "source_id": source.get("id"),
417
+ "source_name": source.get("name"),
418
+ "source_type": source_type_name or source.get("type", {}).get("name"),
419
+ "source_status": source.get("status", {}).get("name") if source.get("status") else "N/A"
420
+ })
421
+
422
+ return result
@@ -182,13 +182,13 @@ class Responsibility(BaseAPI):
182
182
  raise ValueError(f"type must be one of: {', '.join(valid_types)}")
183
183
 
184
184
  # Validate that globalOnly and type are mutually exclusive
185
- if global_only is not None and type is not None:
185
+ if global_only is not None and _type is not None:
186
186
  raise ValueError("globalOnly and type parameters are mutually exclusive")
187
187
 
188
188
  # Validate owner_ids if provided
189
189
  if owner_ids is not None:
190
190
  if not isinstance(owner_ids, list):
191
- raise ValueError("owner_ids must be a list")
191
+ raise ValueError("owner_id must be a list")
192
192
  for owner_id in owner_ids:
193
193
  if not isinstance(owner_id, str):
194
194
  raise ValueError("owner_id must be a string")
@@ -246,10 +246,10 @@ class Responsibility(BaseAPI):
246
246
  params["sortField"] = sort_field
247
247
  if sort_order != "DESC":
248
248
  params["sortOrder"] = sort_order
249
- if type is not None:
250
- params["type"] = type
249
+ if _type is not None:
250
+ params["type"] = _type
251
251
 
252
- response = self._get(params=params)
252
+ response = self._get(url=self.__base_api, params=params)
253
253
  return self._handle_response(response)
254
254
 
255
255
  def get_asset_responsibilities(self, asset_id: str, role_ids: list = None):
@@ -0,0 +1,102 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from .Base import BaseAPI
3
+
4
+
5
+ class Search(BaseAPI):
6
+ """
7
+ Search API implementation.
8
+ Wraps the /search endpoint of Collibra DGC.
9
+ """
10
+
11
+ def __init__(self, connector):
12
+ super().__init__(connector)
13
+ self.__base_api = connector.api + "/search"
14
+
15
+ def find(
16
+ self,
17
+ query: str,
18
+ limit: int = 10,
19
+ offset: int = 0,
20
+ filter_options: Optional[Dict[str, Any]] = None,
21
+ sort_options: Optional[Dict[str, Any]] = None,
22
+ highlight_options: Optional[Dict[str, Any]] = None,
23
+ search_fields: Optional[List[str]] = None,
24
+ ) -> Dict[str, Any]:
25
+ """
26
+ Perform a search query.
27
+
28
+ Args:
29
+ query: The search query string (can use wildcards like *).
30
+ limit: Max number of results.
31
+ offset: Offset for pagination.
32
+ filter_options: Dictionary of filters (e.g., {'category': 'ASSET', 'typeIds': [...]}).
33
+ sort_options: Dictionary of sort options (e.g., {'field': 'name', 'order': 'ASC'}).
34
+ highlight_options: Dictionary of highlight options.
35
+ search_fields: List of fields to search in.
36
+
37
+ Returns:
38
+ Search results dictionary with 'results', 'total', etc.
39
+ """
40
+ data = {
41
+ "keywords": query,
42
+ "limit": limit,
43
+ "offset": offset,
44
+ }
45
+
46
+ if filter_options:
47
+ data.update(filter_options)
48
+
49
+ if sort_options:
50
+ data["sort"] = [sort_options] # API expects a list of sorts
51
+
52
+ if highlight_options:
53
+ data["highlights"] = [highlight_options]
54
+
55
+ if search_fields:
56
+ data["searchFields"] = search_fields
57
+
58
+ # Default to ASSET category if not specified, usually what users want
59
+ # But maybe better to leave it open. The API defaults to all.
60
+
61
+ response = self._post(url=self.__base_api, data=data)
62
+ return self._handle_response(response)
63
+
64
+ def find_assets(
65
+ self,
66
+ query: str,
67
+ limit: int = 10,
68
+ offset: int = 0,
69
+ type_ids: Optional[List[str]] = None,
70
+ domain_ids: Optional[List[str]] = None,
71
+ community_ids: Optional[List[str]] = None,
72
+ status_ids: Optional[List[str]] = None,
73
+ ) -> Dict[str, Any]:
74
+ """
75
+ Helper specifically for searching assets.
76
+
77
+ Args:
78
+ query: Search text.
79
+ limit: Max results.
80
+ offset: Pagination offset.
81
+ type_ids: Filter by asset type IDs.
82
+ domain_ids: Filter by domain IDs.
83
+ community_ids: Filter by community IDs.
84
+ status_ids: Filter by status IDs.
85
+
86
+ Returns:
87
+ Search results.
88
+ """
89
+ filters = {
90
+ "category": "ASSET"
91
+ }
92
+
93
+ if type_ids:
94
+ filters["typeIds"] = type_ids
95
+ if domain_ids:
96
+ filters["domainIds"] = domain_ids
97
+ if community_ids:
98
+ filters["communityIds"] = community_ids
99
+ if status_ids:
100
+ filters["statusIds"] = status_ids
101
+
102
+ return self.find(query=query, limit=limit, offset=offset, filter_options=filters)
@@ -1,19 +1,29 @@
1
1
  from .Asset import Asset
2
- from .Base import BaseAPI
2
+ from .Attribute import Attribute
3
3
  from .Community import Community
4
- from .Comment import Comment
5
4
  from .Domain import Domain
6
- from .Exceptions import (
7
- CollibraAPIError,
8
- UnauthorizedError,
9
- ForbiddenError,
10
- NotFoundError,
11
- ServerError
12
- )
5
+ from .User import User
6
+ from .Responsibility import Responsibility
7
+ from .Workflow import Workflow
13
8
  from .Metadata import Metadata
14
- from .OutputModule import OutputModule
9
+ from .Comment import Comment
15
10
  from .Relation import Relation
16
- from .Responsibility import Responsibility
17
- from .User import User
11
+ from .OutputModule import OutputModule
18
12
  from .Utils import Utils
19
- from .Workflow import Workflow
13
+ from .Search import Search
14
+
15
+ __all__ = [
16
+ "Asset",
17
+ "Attribute",
18
+ "Community",
19
+ "Domain",
20
+ "User",
21
+ "Responsibility",
22
+ "Workflow",
23
+ "Metadata",
24
+ "Comment",
25
+ "Relation",
26
+ "OutputModule",
27
+ "Utils",
28
+ "Search"
29
+ ]