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.
- collibra_connector/__init__.py +284 -4
- collibra_connector/api/Asset.py +301 -3
- collibra_connector/api/Attribute.py +204 -0
- collibra_connector/api/Base.py +2 -2
- collibra_connector/api/Community.py +1 -1
- collibra_connector/api/Relation.py +216 -0
- collibra_connector/api/Responsibility.py +5 -5
- collibra_connector/api/Search.py +102 -0
- collibra_connector/api/__init__.py +23 -13
- collibra_connector/async_connector.py +930 -0
- collibra_connector/cli.py +597 -0
- collibra_connector/connector.py +270 -48
- collibra_connector/helpers.py +845 -0
- collibra_connector/lineage.py +716 -0
- collibra_connector/models.py +897 -0
- collibra_connector/py.typed +0 -0
- collibra_connector/telemetry.py +576 -0
- collibra_connector/testing.py +806 -0
- collibra_connector-1.1.0.dist-info/METADATA +540 -0
- collibra_connector-1.1.0.dist-info/RECORD +32 -0
- collibra_connector-1.1.0.dist-info/entry_points.txt +2 -0
- collibra_connector-1.0.18.dist-info/METADATA +0 -157
- collibra_connector-1.0.18.dist-info/RECORD +0 -21
- {collibra_connector-1.0.18.dist-info → collibra_connector-1.1.0.dist-info}/WHEEL +0 -0
- {collibra_connector-1.0.18.dist-info → collibra_connector-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {collibra_connector-1.0.18.dist-info → collibra_connector-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mocking Engine for Collibra Connector Testing.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for testing code that uses the Collibra Connector
|
|
5
|
+
without making actual API calls. Perfect for unit tests and CI/CD pipelines.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from collibra_connector.testing import mock_collibra, MockCollibraConnector
|
|
9
|
+
>>>
|
|
10
|
+
>>> @mock_collibra
|
|
11
|
+
... def test_my_function():
|
|
12
|
+
... conn = CollibraConnector(api="mock://", username="test", password="test")
|
|
13
|
+
... asset = conn.asset.get_asset("any-uuid")
|
|
14
|
+
... assert asset["name"] == "Mock Asset"
|
|
15
|
+
|
|
16
|
+
Or with custom responses:
|
|
17
|
+
>>> from collibra_connector.testing import MockCollibraConnector
|
|
18
|
+
>>>
|
|
19
|
+
>>> def test_with_custom_data():
|
|
20
|
+
... mock = MockCollibraConnector()
|
|
21
|
+
... mock.asset.add_mock_asset({
|
|
22
|
+
... "id": "custom-id",
|
|
23
|
+
... "name": "Custom Asset",
|
|
24
|
+
... "type": {"id": "type-1", "name": "Business Term"}
|
|
25
|
+
... })
|
|
26
|
+
...
|
|
27
|
+
... asset = mock.asset.get_asset("custom-id")
|
|
28
|
+
... assert asset.name == "Custom Asset"
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import functools
|
|
33
|
+
import re
|
|
34
|
+
import uuid
|
|
35
|
+
from contextlib import contextmanager
|
|
36
|
+
from dataclasses import dataclass, field
|
|
37
|
+
from datetime import datetime
|
|
38
|
+
from typing import Any, Callable, Dict, Generator, List, Optional, TypeVar, Union
|
|
39
|
+
from unittest.mock import MagicMock, patch
|
|
40
|
+
|
|
41
|
+
from .models import (
|
|
42
|
+
AssetModel,
|
|
43
|
+
AssetList,
|
|
44
|
+
DomainModel,
|
|
45
|
+
DomainList,
|
|
46
|
+
CommunityModel,
|
|
47
|
+
CommunityList,
|
|
48
|
+
UserModel,
|
|
49
|
+
AttributeModel,
|
|
50
|
+
AttributeList,
|
|
51
|
+
RelationModel,
|
|
52
|
+
RelationList,
|
|
53
|
+
SearchResultModel,
|
|
54
|
+
SearchResults,
|
|
55
|
+
AssetProfileModel,
|
|
56
|
+
ResourceReference,
|
|
57
|
+
RelationsGrouped,
|
|
58
|
+
RelationSummary,
|
|
59
|
+
ResponsibilitySummary,
|
|
60
|
+
PaginatedResponseModel,
|
|
61
|
+
parse_asset,
|
|
62
|
+
parse_assets,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
T = TypeVar('T')
|
|
67
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def generate_uuid() -> str:
|
|
71
|
+
"""Generate a random UUID string."""
|
|
72
|
+
return str(uuid.uuid4())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def generate_timestamp() -> int:
|
|
76
|
+
"""Generate current timestamp in milliseconds."""
|
|
77
|
+
return int(datetime.now().timestamp() * 1000)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class MockResponse:
|
|
82
|
+
"""Mock HTTP response."""
|
|
83
|
+
status_code: int = 200
|
|
84
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
def json(self) -> Dict[str, Any]:
|
|
87
|
+
return self.data
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def text(self) -> str:
|
|
91
|
+
import json
|
|
92
|
+
return json.dumps(self.data)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class MockDataStore:
|
|
97
|
+
"""In-memory data store for mock data."""
|
|
98
|
+
assets: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
99
|
+
domains: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
100
|
+
communities: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
101
|
+
users: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
102
|
+
attributes: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
103
|
+
relations: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
104
|
+
asset_types: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
105
|
+
statuses: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
106
|
+
|
|
107
|
+
def clear(self) -> None:
|
|
108
|
+
"""Clear all data."""
|
|
109
|
+
self.assets.clear()
|
|
110
|
+
self.domains.clear()
|
|
111
|
+
self.communities.clear()
|
|
112
|
+
self.users.clear()
|
|
113
|
+
self.attributes.clear()
|
|
114
|
+
self.relations.clear()
|
|
115
|
+
self.asset_types.clear()
|
|
116
|
+
self.statuses.clear()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# Global mock data store
|
|
120
|
+
_mock_store = MockDataStore()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _create_default_asset(
|
|
124
|
+
asset_id: Optional[str] = None,
|
|
125
|
+
name: str = "Mock Asset",
|
|
126
|
+
**kwargs: Any
|
|
127
|
+
) -> Dict[str, Any]:
|
|
128
|
+
"""Create a default mock asset."""
|
|
129
|
+
aid = asset_id or generate_uuid()
|
|
130
|
+
return {
|
|
131
|
+
"id": aid,
|
|
132
|
+
"resourceType": "Asset",
|
|
133
|
+
"name": name,
|
|
134
|
+
"displayName": kwargs.get("display_name", name),
|
|
135
|
+
"description": kwargs.get("description"),
|
|
136
|
+
"type": kwargs.get("type", {
|
|
137
|
+
"id": generate_uuid(),
|
|
138
|
+
"resourceType": "AssetType",
|
|
139
|
+
"name": "Business Term"
|
|
140
|
+
}),
|
|
141
|
+
"status": kwargs.get("status", {
|
|
142
|
+
"id": generate_uuid(),
|
|
143
|
+
"resourceType": "Status",
|
|
144
|
+
"name": "Approved"
|
|
145
|
+
}),
|
|
146
|
+
"domain": kwargs.get("domain", {
|
|
147
|
+
"id": generate_uuid(),
|
|
148
|
+
"resourceType": "Domain",
|
|
149
|
+
"name": "Mock Domain"
|
|
150
|
+
}),
|
|
151
|
+
"avgRating": kwargs.get("avg_rating", 0.0),
|
|
152
|
+
"ratingsCount": kwargs.get("ratings_count", 0),
|
|
153
|
+
"createdOn": kwargs.get("created_on", generate_timestamp()),
|
|
154
|
+
"lastModifiedOn": kwargs.get("last_modified_on", generate_timestamp()),
|
|
155
|
+
"excludedFromAutoHyperlinking": kwargs.get("excluded_from_auto_hyperlinking", False)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _create_default_domain(
|
|
160
|
+
domain_id: Optional[str] = None,
|
|
161
|
+
name: str = "Mock Domain",
|
|
162
|
+
**kwargs: Any
|
|
163
|
+
) -> Dict[str, Any]:
|
|
164
|
+
"""Create a default mock domain."""
|
|
165
|
+
did = domain_id or generate_uuid()
|
|
166
|
+
return {
|
|
167
|
+
"id": did,
|
|
168
|
+
"resourceType": "Domain",
|
|
169
|
+
"name": name,
|
|
170
|
+
"description": kwargs.get("description"),
|
|
171
|
+
"type": kwargs.get("type", {
|
|
172
|
+
"id": generate_uuid(),
|
|
173
|
+
"resourceType": "DomainType",
|
|
174
|
+
"name": "Business Glossary"
|
|
175
|
+
}),
|
|
176
|
+
"community": kwargs.get("community", {
|
|
177
|
+
"id": generate_uuid(),
|
|
178
|
+
"resourceType": "Community",
|
|
179
|
+
"name": "Mock Community"
|
|
180
|
+
}),
|
|
181
|
+
"createdOn": generate_timestamp(),
|
|
182
|
+
"lastModifiedOn": generate_timestamp()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _create_default_community(
|
|
187
|
+
community_id: Optional[str] = None,
|
|
188
|
+
name: str = "Mock Community",
|
|
189
|
+
**kwargs: Any
|
|
190
|
+
) -> Dict[str, Any]:
|
|
191
|
+
"""Create a default mock community."""
|
|
192
|
+
cid = community_id or generate_uuid()
|
|
193
|
+
return {
|
|
194
|
+
"id": cid,
|
|
195
|
+
"resourceType": "Community",
|
|
196
|
+
"name": name,
|
|
197
|
+
"description": kwargs.get("description"),
|
|
198
|
+
"parent": kwargs.get("parent"),
|
|
199
|
+
"createdOn": generate_timestamp(),
|
|
200
|
+
"lastModifiedOn": generate_timestamp()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class MockAssetAPI:
|
|
205
|
+
"""Mock Asset API with typed returns."""
|
|
206
|
+
|
|
207
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
208
|
+
self._store = store
|
|
209
|
+
|
|
210
|
+
def add_mock_asset(self, data: Dict[str, Any]) -> str:
|
|
211
|
+
"""Add a mock asset to the store."""
|
|
212
|
+
asset_id = data.get("id", generate_uuid())
|
|
213
|
+
self._store.assets[asset_id] = {**_create_default_asset(asset_id), **data}
|
|
214
|
+
return asset_id
|
|
215
|
+
|
|
216
|
+
def get_asset(self, asset_id: str) -> AssetModel:
|
|
217
|
+
"""Get an asset by ID."""
|
|
218
|
+
if asset_id in self._store.assets:
|
|
219
|
+
return parse_asset(self._store.assets[asset_id])
|
|
220
|
+
|
|
221
|
+
# Return default mock asset
|
|
222
|
+
return parse_asset(_create_default_asset(asset_id))
|
|
223
|
+
|
|
224
|
+
def find_assets(
|
|
225
|
+
self,
|
|
226
|
+
community_id: Optional[str] = None,
|
|
227
|
+
domain_id: Optional[str] = None,
|
|
228
|
+
asset_type_ids: Optional[List[str]] = None,
|
|
229
|
+
limit: int = 100,
|
|
230
|
+
offset: int = 0,
|
|
231
|
+
**kwargs: Any
|
|
232
|
+
) -> AssetList:
|
|
233
|
+
"""Find assets with filters."""
|
|
234
|
+
results = list(self._store.assets.values())
|
|
235
|
+
|
|
236
|
+
# Apply filters
|
|
237
|
+
if domain_id:
|
|
238
|
+
results = [a for a in results if a.get("domain", {}).get("id") == domain_id]
|
|
239
|
+
if community_id:
|
|
240
|
+
results = [a for a in results if a.get("community", {}).get("id") == community_id]
|
|
241
|
+
if asset_type_ids:
|
|
242
|
+
results = [a for a in results if a.get("type", {}).get("id") in asset_type_ids]
|
|
243
|
+
|
|
244
|
+
# If no assets in store, return mock data
|
|
245
|
+
if not results:
|
|
246
|
+
results = [_create_default_asset() for _ in range(min(3, limit))]
|
|
247
|
+
|
|
248
|
+
# Apply pagination
|
|
249
|
+
total = len(results)
|
|
250
|
+
results = results[offset:offset + limit]
|
|
251
|
+
|
|
252
|
+
return parse_assets({
|
|
253
|
+
"results": results,
|
|
254
|
+
"total": total,
|
|
255
|
+
"offset": offset,
|
|
256
|
+
"limit": limit
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
def add_asset(
|
|
260
|
+
self,
|
|
261
|
+
name: str,
|
|
262
|
+
domain_id: str,
|
|
263
|
+
type_id: Optional[str] = None,
|
|
264
|
+
status_id: Optional[str] = None,
|
|
265
|
+
display_name: Optional[str] = None,
|
|
266
|
+
**kwargs: Any
|
|
267
|
+
) -> AssetModel:
|
|
268
|
+
"""Create a new mock asset."""
|
|
269
|
+
asset_id = generate_uuid()
|
|
270
|
+
asset = _create_default_asset(
|
|
271
|
+
asset_id=asset_id,
|
|
272
|
+
name=name,
|
|
273
|
+
display_name=display_name or name
|
|
274
|
+
)
|
|
275
|
+
if type_id:
|
|
276
|
+
asset["type"]["id"] = type_id
|
|
277
|
+
if status_id:
|
|
278
|
+
asset["status"]["id"] = status_id
|
|
279
|
+
asset["domain"]["id"] = domain_id
|
|
280
|
+
|
|
281
|
+
self._store.assets[asset_id] = asset
|
|
282
|
+
return parse_asset(asset)
|
|
283
|
+
|
|
284
|
+
def change_asset(
|
|
285
|
+
self,
|
|
286
|
+
asset_id: str,
|
|
287
|
+
**kwargs: Any
|
|
288
|
+
) -> AssetModel:
|
|
289
|
+
"""Update a mock asset."""
|
|
290
|
+
if asset_id in self._store.assets:
|
|
291
|
+
asset = self._store.assets[asset_id]
|
|
292
|
+
if "name" in kwargs and kwargs["name"]:
|
|
293
|
+
asset["name"] = kwargs["name"]
|
|
294
|
+
if "display_name" in kwargs and kwargs["display_name"]:
|
|
295
|
+
asset["displayName"] = kwargs["display_name"]
|
|
296
|
+
if "status_id" in kwargs and kwargs["status_id"]:
|
|
297
|
+
asset["status"]["id"] = kwargs["status_id"]
|
|
298
|
+
return parse_asset(asset)
|
|
299
|
+
|
|
300
|
+
return parse_asset(_create_default_asset(asset_id))
|
|
301
|
+
|
|
302
|
+
def remove_asset(self, asset_id: str) -> None:
|
|
303
|
+
"""Remove a mock asset."""
|
|
304
|
+
self._store.assets.pop(asset_id, None)
|
|
305
|
+
|
|
306
|
+
def get_full_profile(
|
|
307
|
+
self,
|
|
308
|
+
asset_id: str,
|
|
309
|
+
include_attributes: bool = True,
|
|
310
|
+
include_relations: bool = True,
|
|
311
|
+
include_responsibilities: bool = True,
|
|
312
|
+
**kwargs: Any
|
|
313
|
+
) -> AssetProfileModel:
|
|
314
|
+
"""Get full profile for an asset."""
|
|
315
|
+
asset = self.get_asset(asset_id)
|
|
316
|
+
|
|
317
|
+
return AssetProfileModel(
|
|
318
|
+
asset=asset,
|
|
319
|
+
attributes={"Description": "Mock description", "Definition": "Mock definition"},
|
|
320
|
+
relations=RelationsGrouped(
|
|
321
|
+
outgoing={"contains": [RelationSummary(id=generate_uuid(), target_id=generate_uuid(), target_name="Related Asset")]},
|
|
322
|
+
incoming={},
|
|
323
|
+
outgoing_count=1,
|
|
324
|
+
incoming_count=0
|
|
325
|
+
),
|
|
326
|
+
responsibilities=[ResponsibilitySummary(role="Data Steward", owner="Mock User", owner_id=generate_uuid())]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class MockAttributeAPI:
|
|
331
|
+
"""Mock Attribute API."""
|
|
332
|
+
|
|
333
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
334
|
+
self._store = store
|
|
335
|
+
|
|
336
|
+
def get_attributes(
|
|
337
|
+
self,
|
|
338
|
+
asset_id: str,
|
|
339
|
+
limit: int = 100,
|
|
340
|
+
offset: int = 0,
|
|
341
|
+
**kwargs: Any
|
|
342
|
+
) -> AttributeList:
|
|
343
|
+
"""Get attributes for an asset."""
|
|
344
|
+
# Return mock attributes
|
|
345
|
+
attrs = [
|
|
346
|
+
{
|
|
347
|
+
"id": generate_uuid(),
|
|
348
|
+
"resourceType": "Attribute",
|
|
349
|
+
"type": {"id": generate_uuid(), "name": "Description"},
|
|
350
|
+
"value": "Mock description",
|
|
351
|
+
"asset": {"id": asset_id}
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
"id": generate_uuid(),
|
|
355
|
+
"resourceType": "Attribute",
|
|
356
|
+
"type": {"id": generate_uuid(), "name": "Definition"},
|
|
357
|
+
"value": "Mock definition",
|
|
358
|
+
"asset": {"id": asset_id}
|
|
359
|
+
}
|
|
360
|
+
]
|
|
361
|
+
return PaginatedResponseModel[AttributeModel](
|
|
362
|
+
results=[AttributeModel.model_validate(a) for a in attrs],
|
|
363
|
+
total=len(attrs),
|
|
364
|
+
offset=offset,
|
|
365
|
+
limit=limit
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def get_attributes_as_dict(self, asset_id: str) -> Dict[str, Any]:
|
|
369
|
+
"""Get attributes as dict."""
|
|
370
|
+
return {
|
|
371
|
+
"Description": "Mock description",
|
|
372
|
+
"Definition": "Mock definition"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
def add_attribute(
|
|
376
|
+
self,
|
|
377
|
+
asset_id: str,
|
|
378
|
+
type_id: str,
|
|
379
|
+
value: Any
|
|
380
|
+
) -> AttributeModel:
|
|
381
|
+
"""Add an attribute."""
|
|
382
|
+
attr = {
|
|
383
|
+
"id": generate_uuid(),
|
|
384
|
+
"resourceType": "Attribute",
|
|
385
|
+
"type": {"id": type_id, "name": "Custom"},
|
|
386
|
+
"value": value,
|
|
387
|
+
"asset": {"id": asset_id}
|
|
388
|
+
}
|
|
389
|
+
return AttributeModel.model_validate(attr)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class MockDomainAPI:
|
|
393
|
+
"""Mock Domain API."""
|
|
394
|
+
|
|
395
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
396
|
+
self._store = store
|
|
397
|
+
|
|
398
|
+
def add_mock_domain(self, data: Dict[str, Any]) -> str:
|
|
399
|
+
"""Add a mock domain to the store."""
|
|
400
|
+
domain_id = data.get("id", generate_uuid())
|
|
401
|
+
self._store.domains[domain_id] = {**_create_default_domain(domain_id), **data}
|
|
402
|
+
return domain_id
|
|
403
|
+
|
|
404
|
+
def get_domain(self, domain_id: str) -> DomainModel:
|
|
405
|
+
"""Get a domain by ID."""
|
|
406
|
+
if domain_id in self._store.domains:
|
|
407
|
+
return DomainModel.model_validate(self._store.domains[domain_id])
|
|
408
|
+
return DomainModel.model_validate(_create_default_domain(domain_id))
|
|
409
|
+
|
|
410
|
+
def find_domains(
|
|
411
|
+
self,
|
|
412
|
+
community_id: Optional[str] = None,
|
|
413
|
+
limit: int = 100,
|
|
414
|
+
offset: int = 0,
|
|
415
|
+
**kwargs: Any
|
|
416
|
+
) -> DomainList:
|
|
417
|
+
"""Find domains."""
|
|
418
|
+
results = list(self._store.domains.values())
|
|
419
|
+
if community_id:
|
|
420
|
+
results = [d for d in results if d.get("community", {}).get("id") == community_id]
|
|
421
|
+
|
|
422
|
+
if not results:
|
|
423
|
+
results = [_create_default_domain() for _ in range(min(2, limit))]
|
|
424
|
+
|
|
425
|
+
return PaginatedResponseModel[DomainModel](
|
|
426
|
+
results=[DomainModel.model_validate(d) for d in results[offset:offset + limit]],
|
|
427
|
+
total=len(results),
|
|
428
|
+
offset=offset,
|
|
429
|
+
limit=limit
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class MockCommunityAPI:
|
|
434
|
+
"""Mock Community API."""
|
|
435
|
+
|
|
436
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
437
|
+
self._store = store
|
|
438
|
+
|
|
439
|
+
def get_community(self, community_id: str) -> CommunityModel:
|
|
440
|
+
"""Get a community by ID."""
|
|
441
|
+
if community_id in self._store.communities:
|
|
442
|
+
return CommunityModel.model_validate(self._store.communities[community_id])
|
|
443
|
+
return CommunityModel.model_validate(_create_default_community(community_id))
|
|
444
|
+
|
|
445
|
+
def find_communities(
|
|
446
|
+
self,
|
|
447
|
+
limit: int = 100,
|
|
448
|
+
offset: int = 0,
|
|
449
|
+
**kwargs: Any
|
|
450
|
+
) -> CommunityList:
|
|
451
|
+
"""Find communities."""
|
|
452
|
+
results = list(self._store.communities.values())
|
|
453
|
+
if not results:
|
|
454
|
+
results = [_create_default_community() for _ in range(min(2, limit))]
|
|
455
|
+
|
|
456
|
+
return PaginatedResponseModel[CommunityModel](
|
|
457
|
+
results=[CommunityModel.model_validate(c) for c in results[offset:offset + limit]],
|
|
458
|
+
total=len(results),
|
|
459
|
+
offset=offset,
|
|
460
|
+
limit=limit
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
class MockRelationAPI:
|
|
465
|
+
"""Mock Relation API."""
|
|
466
|
+
|
|
467
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
468
|
+
self._store = store
|
|
469
|
+
|
|
470
|
+
def add_relation(
|
|
471
|
+
self,
|
|
472
|
+
source_id: str,
|
|
473
|
+
target_id: str,
|
|
474
|
+
type_id: str,
|
|
475
|
+
**kwargs: Any
|
|
476
|
+
) -> RelationModel:
|
|
477
|
+
"""Create a relation."""
|
|
478
|
+
rel = {
|
|
479
|
+
"id": generate_uuid(),
|
|
480
|
+
"resourceType": "Relation",
|
|
481
|
+
"type": {"id": type_id, "name": "is source for"},
|
|
482
|
+
"source": {"id": source_id, "name": "Source Asset"},
|
|
483
|
+
"target": {"id": target_id, "name": "Target Asset"}
|
|
484
|
+
}
|
|
485
|
+
self._store.relations[rel["id"]] = rel
|
|
486
|
+
return RelationModel.model_validate(rel)
|
|
487
|
+
|
|
488
|
+
def find_relations(
|
|
489
|
+
self,
|
|
490
|
+
source_id: Optional[str] = None,
|
|
491
|
+
target_id: Optional[str] = None,
|
|
492
|
+
limit: int = 100,
|
|
493
|
+
offset: int = 0,
|
|
494
|
+
**kwargs: Any
|
|
495
|
+
) -> RelationList:
|
|
496
|
+
"""Find relations."""
|
|
497
|
+
results = list(self._store.relations.values())
|
|
498
|
+
if source_id:
|
|
499
|
+
results = [r for r in results if r.get("source", {}).get("id") == source_id]
|
|
500
|
+
if target_id:
|
|
501
|
+
results = [r for r in results if r.get("target", {}).get("id") == target_id]
|
|
502
|
+
|
|
503
|
+
return PaginatedResponseModel[RelationModel](
|
|
504
|
+
results=[RelationModel.model_validate(r) for r in results[offset:offset + limit]],
|
|
505
|
+
total=len(results),
|
|
506
|
+
offset=offset,
|
|
507
|
+
limit=limit
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
def get_asset_relations(
|
|
511
|
+
self,
|
|
512
|
+
asset_id: str,
|
|
513
|
+
**kwargs: Any
|
|
514
|
+
) -> Dict[str, Any]:
|
|
515
|
+
"""Get relations for an asset."""
|
|
516
|
+
return {
|
|
517
|
+
"outgoing": {},
|
|
518
|
+
"incoming": {},
|
|
519
|
+
"outgoing_count": 0,
|
|
520
|
+
"incoming_count": 0
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class MockSearchAPI:
|
|
525
|
+
"""Mock Search API."""
|
|
526
|
+
|
|
527
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
528
|
+
self._store = store
|
|
529
|
+
|
|
530
|
+
def find(
|
|
531
|
+
self,
|
|
532
|
+
query: str,
|
|
533
|
+
limit: int = 10,
|
|
534
|
+
offset: int = 0,
|
|
535
|
+
**kwargs: Any
|
|
536
|
+
) -> SearchResults:
|
|
537
|
+
"""Search for assets."""
|
|
538
|
+
# Simple matching on asset names
|
|
539
|
+
results = []
|
|
540
|
+
pattern = query.replace("*", ".*")
|
|
541
|
+
|
|
542
|
+
for asset in self._store.assets.values():
|
|
543
|
+
if re.search(pattern, asset.get("name", ""), re.IGNORECASE):
|
|
544
|
+
results.append({
|
|
545
|
+
"resource": {
|
|
546
|
+
"id": asset["id"],
|
|
547
|
+
"resourceType": "Asset",
|
|
548
|
+
"name": asset["name"],
|
|
549
|
+
"displayName": asset.get("displayName")
|
|
550
|
+
},
|
|
551
|
+
"score": 1.0
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
# Add default results if empty
|
|
555
|
+
if not results:
|
|
556
|
+
results = [{
|
|
557
|
+
"resource": {
|
|
558
|
+
"id": generate_uuid(),
|
|
559
|
+
"resourceType": "Asset",
|
|
560
|
+
"name": f"Result for: {query}",
|
|
561
|
+
"displayName": f"Result for: {query}"
|
|
562
|
+
},
|
|
563
|
+
"score": 0.9
|
|
564
|
+
}]
|
|
565
|
+
|
|
566
|
+
return PaginatedResponseModel[SearchResultModel](
|
|
567
|
+
results=[SearchResultModel.model_validate(r) for r in results[offset:offset + limit]],
|
|
568
|
+
total=len(results),
|
|
569
|
+
offset=offset,
|
|
570
|
+
limit=limit
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def find_assets(self, query: str, **kwargs: Any) -> SearchResults:
|
|
574
|
+
"""Search specifically for assets."""
|
|
575
|
+
return self.find(query, **kwargs)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
class MockMetadataAPI:
|
|
579
|
+
"""Mock Metadata API."""
|
|
580
|
+
|
|
581
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
582
|
+
self._store = store
|
|
583
|
+
|
|
584
|
+
def get_asset_types(self, name: Optional[str] = None, limit: int = 100, **kwargs: Any) -> Dict[str, Any]:
|
|
585
|
+
"""Get asset types."""
|
|
586
|
+
types = [
|
|
587
|
+
{"id": generate_uuid(), "name": "Business Term", "publicId": "BusinessTerm"},
|
|
588
|
+
{"id": generate_uuid(), "name": "Table", "publicId": "Table"},
|
|
589
|
+
{"id": generate_uuid(), "name": "Column", "publicId": "Column"},
|
|
590
|
+
{"id": generate_uuid(), "name": "Data Pipeline", "publicId": "DataPipeline"}
|
|
591
|
+
]
|
|
592
|
+
if name:
|
|
593
|
+
types = [t for t in types if name.lower() in t["name"].lower()]
|
|
594
|
+
return {"results": types[:limit], "total": len(types)}
|
|
595
|
+
|
|
596
|
+
def get_statuses(self, limit: int = 100, **kwargs: Any) -> Dict[str, Any]:
|
|
597
|
+
"""Get statuses."""
|
|
598
|
+
statuses = [
|
|
599
|
+
{"id": generate_uuid(), "name": "Approved"},
|
|
600
|
+
{"id": generate_uuid(), "name": "Pending"},
|
|
601
|
+
{"id": generate_uuid(), "name": "Rejected"},
|
|
602
|
+
{"id": generate_uuid(), "name": "Draft"}
|
|
603
|
+
]
|
|
604
|
+
return {"results": statuses[:limit], "total": len(statuses)}
|
|
605
|
+
|
|
606
|
+
def get_relation_types(self, role: Optional[str] = None, limit: int = 100, **kwargs: Any) -> Dict[str, Any]:
|
|
607
|
+
"""Get relation types."""
|
|
608
|
+
types = [
|
|
609
|
+
{"id": generate_uuid(), "name": "contains", "role": "contains", "coRole": "is part of"},
|
|
610
|
+
{"id": generate_uuid(), "name": "is source for", "role": "is source for", "coRole": "is target for"}
|
|
611
|
+
]
|
|
612
|
+
if role:
|
|
613
|
+
types = [t for t in types if role.lower() in (t.get("role") or "").lower()]
|
|
614
|
+
return {"results": types[:limit], "total": len(types)}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
class MockResponsibilityAPI:
|
|
618
|
+
"""Mock Responsibility API."""
|
|
619
|
+
|
|
620
|
+
def __init__(self, store: MockDataStore) -> None:
|
|
621
|
+
self._store = store
|
|
622
|
+
|
|
623
|
+
def get_asset_responsibilities(self, asset_id: str, **kwargs: Any) -> List[Dict[str, Any]]:
|
|
624
|
+
"""Get responsibilities for an asset."""
|
|
625
|
+
return [
|
|
626
|
+
{"role": "Data Steward", "owner": "Mock User", "owner_id": generate_uuid()},
|
|
627
|
+
{"role": "Technical Owner", "owner": "Tech User", "owner_id": generate_uuid()}
|
|
628
|
+
]
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
class MockCollibraConnector:
|
|
632
|
+
"""
|
|
633
|
+
Mock Collibra Connector for testing.
|
|
634
|
+
|
|
635
|
+
This class provides the same interface as CollibraConnector
|
|
636
|
+
but returns mock data instead of making API calls.
|
|
637
|
+
|
|
638
|
+
Example:
|
|
639
|
+
>>> mock = MockCollibraConnector()
|
|
640
|
+
>>>
|
|
641
|
+
>>> # Add custom test data
|
|
642
|
+
>>> mock.asset.add_mock_asset({
|
|
643
|
+
... "id": "test-id",
|
|
644
|
+
... "name": "Test Asset",
|
|
645
|
+
... "type": {"id": "type-1", "name": "Business Term"}
|
|
646
|
+
... })
|
|
647
|
+
>>>
|
|
648
|
+
>>> # Use like regular connector
|
|
649
|
+
>>> asset = mock.asset.get_asset("test-id")
|
|
650
|
+
>>> assert asset.name == "Test Asset"
|
|
651
|
+
>>>
|
|
652
|
+
>>> # Find assets
|
|
653
|
+
>>> results = mock.asset.find_assets(limit=10)
|
|
654
|
+
>>> for asset in results:
|
|
655
|
+
... print(asset.name)
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
def __init__(
|
|
659
|
+
self,
|
|
660
|
+
api: str = "mock://collibra.test",
|
|
661
|
+
username: str = "test",
|
|
662
|
+
password: str = "test",
|
|
663
|
+
**kwargs: Any
|
|
664
|
+
) -> None:
|
|
665
|
+
"""Initialize mock connector."""
|
|
666
|
+
self._api = api
|
|
667
|
+
self._base_url = api
|
|
668
|
+
self._store = MockDataStore()
|
|
669
|
+
|
|
670
|
+
# Initialize mock APIs
|
|
671
|
+
self.asset = MockAssetAPI(self._store)
|
|
672
|
+
self.attribute = MockAttributeAPI(self._store)
|
|
673
|
+
self.domain = MockDomainAPI(self._store)
|
|
674
|
+
self.community = MockCommunityAPI(self._store)
|
|
675
|
+
self.relation = MockRelationAPI(self._store)
|
|
676
|
+
self.search = MockSearchAPI(self._store)
|
|
677
|
+
self.metadata = MockMetadataAPI(self._store)
|
|
678
|
+
self.responsibility = MockResponsibilityAPI(self._store)
|
|
679
|
+
|
|
680
|
+
@property
|
|
681
|
+
def api(self) -> str:
|
|
682
|
+
return self._api
|
|
683
|
+
|
|
684
|
+
@property
|
|
685
|
+
def base_url(self) -> str:
|
|
686
|
+
return self._base_url
|
|
687
|
+
|
|
688
|
+
def test_connection(self) -> bool:
|
|
689
|
+
"""Always returns True for mock."""
|
|
690
|
+
return True
|
|
691
|
+
|
|
692
|
+
def get_version(self) -> str:
|
|
693
|
+
"""Get mock version."""
|
|
694
|
+
return "1.1.0-mock"
|
|
695
|
+
|
|
696
|
+
def clear_all_data(self) -> None:
|
|
697
|
+
"""Clear all mock data."""
|
|
698
|
+
self._store.clear()
|
|
699
|
+
|
|
700
|
+
def __enter__(self) -> "MockCollibraConnector":
|
|
701
|
+
return self
|
|
702
|
+
|
|
703
|
+
def __exit__(self, *args: Any) -> None:
|
|
704
|
+
pass
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@contextmanager
|
|
708
|
+
def mock_collibra_context(
|
|
709
|
+
custom_data: Optional[Dict[str, Any]] = None
|
|
710
|
+
) -> Generator[MockCollibraConnector, None, None]:
|
|
711
|
+
"""
|
|
712
|
+
Context manager for mocking Collibra in tests.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
custom_data: Optional dict with custom mock data.
|
|
716
|
+
|
|
717
|
+
Yields:
|
|
718
|
+
MockCollibraConnector instance.
|
|
719
|
+
|
|
720
|
+
Example:
|
|
721
|
+
>>> with mock_collibra_context() as mock_conn:
|
|
722
|
+
... mock_conn.asset.add_mock_asset({"name": "Test"})
|
|
723
|
+
... result = mock_conn.asset.find_assets()
|
|
724
|
+
... assert len(result) > 0
|
|
725
|
+
"""
|
|
726
|
+
mock = MockCollibraConnector()
|
|
727
|
+
|
|
728
|
+
if custom_data:
|
|
729
|
+
for asset in custom_data.get("assets", []):
|
|
730
|
+
mock.asset.add_mock_asset(asset)
|
|
731
|
+
for domain in custom_data.get("domains", []):
|
|
732
|
+
mock.domain.add_mock_domain(domain)
|
|
733
|
+
|
|
734
|
+
yield mock
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def mock_collibra(func: Optional[F] = None, **kwargs: Any) -> Union[F, Callable[[F], F]]:
|
|
738
|
+
"""
|
|
739
|
+
Decorator for mocking Collibra in tests.
|
|
740
|
+
|
|
741
|
+
Can be used with or without arguments.
|
|
742
|
+
|
|
743
|
+
Examples:
|
|
744
|
+
>>> @mock_collibra
|
|
745
|
+
... def test_something():
|
|
746
|
+
... conn = CollibraConnector(api="mock://", username="x", password="y")
|
|
747
|
+
... # This will use mock data
|
|
748
|
+
... asset = conn.asset.get_asset("any-id")
|
|
749
|
+
>>>
|
|
750
|
+
>>> @mock_collibra(custom_assets=[{"name": "Custom"}])
|
|
751
|
+
... def test_with_data():
|
|
752
|
+
... pass
|
|
753
|
+
"""
|
|
754
|
+
def decorator(f: F) -> F:
|
|
755
|
+
@functools.wraps(f)
|
|
756
|
+
def wrapper(*args: Any, **kw: Any) -> Any:
|
|
757
|
+
# Patch CollibraConnector to return MockCollibraConnector
|
|
758
|
+
with patch('collibra_connector.CollibraConnector', MockCollibraConnector):
|
|
759
|
+
with patch('collibra_connector.connector.CollibraConnector', MockCollibraConnector):
|
|
760
|
+
return f(*args, **kw)
|
|
761
|
+
return wrapper # type: ignore
|
|
762
|
+
|
|
763
|
+
if func is not None:
|
|
764
|
+
return decorator(func)
|
|
765
|
+
return decorator
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class CollibraTestCase:
|
|
769
|
+
"""
|
|
770
|
+
Base test case class for Collibra tests.
|
|
771
|
+
|
|
772
|
+
Provides setUp/tearDown with automatic mocking.
|
|
773
|
+
|
|
774
|
+
Example:
|
|
775
|
+
>>> class TestMyFeature(CollibraTestCase):
|
|
776
|
+
... def test_asset_creation(self):
|
|
777
|
+
... asset = self.connector.asset.add_asset(
|
|
778
|
+
... name="Test",
|
|
779
|
+
... domain_id="domain-uuid"
|
|
780
|
+
... )
|
|
781
|
+
... assert asset.name == "Test"
|
|
782
|
+
"""
|
|
783
|
+
|
|
784
|
+
def setUp(self) -> None:
|
|
785
|
+
"""Set up mock connector."""
|
|
786
|
+
self.connector = MockCollibraConnector()
|
|
787
|
+
|
|
788
|
+
def tearDown(self) -> None:
|
|
789
|
+
"""Clean up mock data."""
|
|
790
|
+
self.connector.clear_all_data()
|
|
791
|
+
|
|
792
|
+
def add_test_asset(self, **kwargs: Any) -> str:
|
|
793
|
+
"""Helper to add a test asset."""
|
|
794
|
+
data = {
|
|
795
|
+
"name": kwargs.get("name", "Test Asset"),
|
|
796
|
+
**kwargs
|
|
797
|
+
}
|
|
798
|
+
return self.connector.asset.add_mock_asset(data)
|
|
799
|
+
|
|
800
|
+
def add_test_domain(self, **kwargs: Any) -> str:
|
|
801
|
+
"""Helper to add a test domain."""
|
|
802
|
+
data = {
|
|
803
|
+
"name": kwargs.get("name", "Test Domain"),
|
|
804
|
+
**kwargs
|
|
805
|
+
}
|
|
806
|
+
return self.connector.domain.add_mock_domain(data)
|