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,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)