collibra-connector 1.0.19__py3-none-any.whl → 1.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.
@@ -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:
@@ -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)
@@ -10,15 +10,26 @@ class Workflow(BaseAPI):
10
10
  def start_workflow_instance(
11
11
  self,
12
12
  workflow_definition_id: str,
13
- asset_id: str = None,
13
+ business_item_ids: list[str] = None,
14
+ business_item_type: str = None,
15
+ form_properties: dict[str, str] = None,
16
+ guest_user_id: str = None,
17
+ send_notification: bool = False,
14
18
  return_all: bool = False
15
19
  ):
16
20
  """
17
- Start a workflow instance.
18
- :param workflow_definition_id: The ID of the workflow definition.
19
- :param asset_id: Optional asset ID to associate with the workflow.
21
+ Start multiple workflow instances based on the provided request.
22
+
23
+ :param workflow_definition_id: The ID of the workflow definition (required).
24
+ :param business_item_ids: The list of IDs for the business items (optional).
25
+ :param business_item_type: The resource type of the passed in business items.
26
+ Allowed values: ASSET, DOMAIN, COMMUNITY, GLOBAL, USER (optional).
27
+ :param form_properties: The properties of the workflow as a dictionary (optional).
28
+ :param guest_user_id: The ID of the guest user starting the workflow (optional).
29
+ :param send_notification: Whether a mail notification on starting the workflows should be sent.
30
+ This notification is only used in the asynchronous api version (optional).
20
31
  :param return_all: Whether to return all workflow data or just the ID.
21
- :return: Workflow instance ID or full response data.
32
+ :return: Workflow instance ID or full response data (array of workflow instances).
22
33
  """
23
34
  if not workflow_definition_id:
24
35
  raise ValueError("workflow_definition_id is required")
@@ -32,21 +43,44 @@ class Workflow(BaseAPI):
32
43
 
33
44
  data = {
34
45
  "workflowDefinitionId": workflow_definition_id,
35
- "sendNotification": True
46
+ "sendNotification": send_notification
36
47
  }
37
48
 
38
- if asset_id:
39
- if not isinstance(asset_id, str):
40
- raise ValueError("asset_id must be a string")
49
+ if business_item_ids is not None:
50
+ if not isinstance(business_item_ids, list):
51
+ raise ValueError("business_item_ids must be a list")
52
+ if not all(isinstance(item_id, str) for item_id in business_item_ids):
53
+ raise ValueError("All business_item_ids must be strings")
54
+ for item_id in business_item_ids:
55
+ try:
56
+ uuid.UUID(item_id)
57
+ except ValueError as exc:
58
+ raise ValueError(f"business_item_id '{item_id}' must be a valid UUID") from exc
59
+ data["businessItemIds"] = business_item_ids
60
+
61
+ if business_item_type is not None:
62
+ if not isinstance(business_item_type, str):
63
+ raise ValueError("business_item_type must be a string")
64
+ valid_types = ["ASSET", "DOMAIN", "COMMUNITY", "GLOBAL", "USER"]
65
+ if business_item_type not in valid_types:
66
+ raise ValueError(f"business_item_type must be one of: {', '.join(valid_types)}")
67
+ data["businessItemType"] = business_item_type
68
+
69
+ if form_properties is not None:
70
+ if not isinstance(form_properties, dict):
71
+ raise ValueError("form_properties must be a dictionary")
72
+ if not all(isinstance(k, str) and isinstance(v, str) for k, v in form_properties.items()):
73
+ raise ValueError("All form_properties keys and values must be strings")
74
+ data["formProperties"] = form_properties
75
+
76
+ if guest_user_id is not None:
77
+ if not isinstance(guest_user_id, str):
78
+ raise ValueError("guest_user_id must be a string")
41
79
  try:
42
- uuid.UUID(asset_id)
80
+ uuid.UUID(guest_user_id)
43
81
  except ValueError as exc:
44
- raise ValueError("asset_id must be a valid UUID") from exc
45
-
46
- data.update({
47
- "businessItemIds": [asset_id],
48
- "businessItemType": "ASSET",
49
- })
82
+ raise ValueError("guest_user_id must be a valid UUID") from exc
83
+ data["guestUserId"] = guest_user_id
50
84
 
51
85
  response = self._post(url=f"{self.__base_api}/workflowInstances", data=data)
52
86
  result = self._handle_response(response)