pvw-cli 1.2.8__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.

Potentially problematic release.


This version of pvw-cli might be problematic. Click here for more details.

Files changed (60) hide show
  1. purviewcli/__init__.py +27 -0
  2. purviewcli/__main__.py +15 -0
  3. purviewcli/cli/__init__.py +5 -0
  4. purviewcli/cli/account.py +199 -0
  5. purviewcli/cli/cli.py +170 -0
  6. purviewcli/cli/collections.py +502 -0
  7. purviewcli/cli/domain.py +361 -0
  8. purviewcli/cli/entity.py +2436 -0
  9. purviewcli/cli/glossary.py +533 -0
  10. purviewcli/cli/health.py +250 -0
  11. purviewcli/cli/insight.py +113 -0
  12. purviewcli/cli/lineage.py +1103 -0
  13. purviewcli/cli/management.py +141 -0
  14. purviewcli/cli/policystore.py +103 -0
  15. purviewcli/cli/relationship.py +75 -0
  16. purviewcli/cli/scan.py +357 -0
  17. purviewcli/cli/search.py +527 -0
  18. purviewcli/cli/share.py +478 -0
  19. purviewcli/cli/types.py +831 -0
  20. purviewcli/cli/unified_catalog.py +3540 -0
  21. purviewcli/cli/workflow.py +402 -0
  22. purviewcli/client/__init__.py +21 -0
  23. purviewcli/client/_account.py +1877 -0
  24. purviewcli/client/_collections.py +1761 -0
  25. purviewcli/client/_domain.py +414 -0
  26. purviewcli/client/_entity.py +3545 -0
  27. purviewcli/client/_glossary.py +3233 -0
  28. purviewcli/client/_health.py +501 -0
  29. purviewcli/client/_insight.py +2873 -0
  30. purviewcli/client/_lineage.py +2138 -0
  31. purviewcli/client/_management.py +2202 -0
  32. purviewcli/client/_policystore.py +2915 -0
  33. purviewcli/client/_relationship.py +1351 -0
  34. purviewcli/client/_scan.py +2607 -0
  35. purviewcli/client/_search.py +1472 -0
  36. purviewcli/client/_share.py +272 -0
  37. purviewcli/client/_types.py +2708 -0
  38. purviewcli/client/_unified_catalog.py +5112 -0
  39. purviewcli/client/_workflow.py +2734 -0
  40. purviewcli/client/api_client.py +1295 -0
  41. purviewcli/client/business_rules.py +675 -0
  42. purviewcli/client/config.py +231 -0
  43. purviewcli/client/data_quality.py +433 -0
  44. purviewcli/client/endpoint.py +123 -0
  45. purviewcli/client/endpoints.py +554 -0
  46. purviewcli/client/exceptions.py +38 -0
  47. purviewcli/client/lineage_visualization.py +797 -0
  48. purviewcli/client/monitoring_dashboard.py +712 -0
  49. purviewcli/client/rate_limiter.py +30 -0
  50. purviewcli/client/retry_handler.py +125 -0
  51. purviewcli/client/scanning_operations.py +523 -0
  52. purviewcli/client/settings.py +1 -0
  53. purviewcli/client/sync_client.py +250 -0
  54. purviewcli/plugins/__init__.py +1 -0
  55. purviewcli/plugins/plugin_system.py +709 -0
  56. pvw_cli-1.2.8.dist-info/METADATA +1618 -0
  57. pvw_cli-1.2.8.dist-info/RECORD +60 -0
  58. pvw_cli-1.2.8.dist-info/WHEEL +5 -0
  59. pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
  60. pvw_cli-1.2.8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1103 @@
1
+ """
2
+ Manage lineage operations in Microsoft Purview using modular Click-based commands.
3
+
4
+ Usage:
5
+ lineage read Read lineage information for an entity
6
+ lineage impact Analyze impact of changes to an entity
7
+ lineage analyze-column Analyze column-level lineage
8
+ lineage get-metrics Get lineage metrics and statistics
9
+ lineage csv-process Process CSV lineage relationships
10
+ lineage csv-validate Validate CSV lineage file format
11
+ lineage csv-sample Generate sample CSV lineage file
12
+ lineage csv-templates Get available CSV lineage templates
13
+ lineage --help Show this help message and exit
14
+
15
+ Options:
16
+ -h --help Show this help message and exit
17
+ """
18
+
19
+ import json
20
+ import click
21
+ from rich.console import Console
22
+ from typing import Optional
23
+ from purviewcli.client._lineage import Lineage
24
+
25
+ console = Console()
26
+
27
+
28
+ @click.group(help="""
29
+ Manage lineage in Microsoft Purview.
30
+
31
+ Examples:
32
+ lineage read --guid <entity_guid> [--direction INPUT|OUTPUT|BOTH] [--depth N]
33
+ lineage import <csv_file>
34
+ lineage impact --guid <entity_guid>
35
+ lineage analyze-column --guid <entity_guid> --column <column_name>
36
+ lineage get-metrics
37
+ lineage csv-process <csv_file>
38
+ lineage csv-validate <csv_file>
39
+ lineage csv-sample
40
+ lineage csv-templates
41
+
42
+ Use 'lineage <command> --help' for more details on each command.
43
+ """)
44
+ @click.pass_context
45
+ def lineage(ctx):
46
+ """
47
+ Manage lineage in Microsoft Purview.
48
+ """
49
+ pass
50
+
51
+
52
+ @lineage.command(name="import")
53
+ @click.argument('csv_file', type=click.Path(exists=True))
54
+ @click.pass_context
55
+ def import_cmd(ctx, csv_file):
56
+ """Import lineage relationships from CSV file (calls client lineageCSVProcess)."""
57
+ try:
58
+ if ctx.obj and ctx.obj.get("mock"):
59
+ console.print("[yellow][MOCK] lineage import command[/yellow]")
60
+ console.print(f"[dim]File: {csv_file}[/dim]")
61
+ console.print("[green]MOCK lineage import completed successfully[/green]")
62
+ return
63
+
64
+ from purviewcli.client._lineage import Lineage
65
+ lineage_client = Lineage()
66
+ args = {"csv_file": csv_file}
67
+ result = lineage_client.lineageCSVProcess(args)
68
+ console.print("[green]SUCCESS: Lineage import completed successfully[/green]")
69
+ console.print(json.dumps(result, indent=2))
70
+ except Exception as e:
71
+ console.print(f"[red]ERROR: Error executing lineage import: {str(e)}[/red]")
72
+ import traceback
73
+ if ctx.obj and ctx.obj.get("debug"):
74
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
75
+
76
+
77
+ @lineage.command()
78
+ @click.argument('csv_file', type=click.Path(exists=True))
79
+ @click.pass_context
80
+ def validate(ctx, csv_file):
81
+ """Validate CSV lineage file format and content locally (no API call)"""
82
+ try:
83
+ if ctx.obj.get("mock"):
84
+ console.print("[yellow][MOCK] lineage validate command[/yellow]")
85
+ console.print(f"[dim]File: {csv_file}[/dim]")
86
+ console.print("[green]MOCK lineage validate completed successfully[/green]")
87
+ return
88
+
89
+ args = {"csv_file": csv_file}
90
+
91
+ from purviewcli.client._lineage import Lineage
92
+ lineage_client = Lineage()
93
+ result = lineage_client.lineageCSVValidate(args)
94
+
95
+ if isinstance(result, dict) and result.get("success"):
96
+ console.print(f"[green]SUCCESS: Lineage validation passed: {csv_file} ({result['rows']} rows, columns: {', '.join(result['columns'])})[/green]")
97
+ else:
98
+ error_msg = result.get('error') if isinstance(result, dict) else str(result)
99
+ console.print(f"[red]ERROR: Lineage validation failed: {error_msg}[/red]")
100
+
101
+ except Exception as e:
102
+ console.print(f"[red]ERROR: Error executing lineage validate: {str(e)}[/red]")
103
+
104
+
105
+ @lineage.command()
106
+ @click.argument('output_file', type=click.Path())
107
+ @click.option('--num-samples', type=int, default=10,
108
+ help='Number of sample rows to generate')
109
+ @click.option('--template', default='basic',
110
+ help='Template type: basic, etl, column-mapping')
111
+ @click.pass_context
112
+ def sample(ctx, output_file, num_samples, template):
113
+ """Generate sample CSV lineage file locally (no API call)"""
114
+ try:
115
+ if ctx.obj.get("mock"):
116
+ console.print("[yellow][MOCK] lineage sample command[/yellow]")
117
+ console.print(f"[dim]Output File: {output_file}[/dim]")
118
+ console.print(f"[dim]Samples: {num_samples}[/dim]")
119
+ console.print(f"[dim]Template: {template}[/dim]")
120
+ console.print("[green]MOCK lineage sample completed successfully[/green]")
121
+ return
122
+
123
+ args = {
124
+ "--output-file": output_file,
125
+ "--num-samples": num_samples,
126
+ "--template": template,
127
+ }
128
+
129
+ from purviewcli.client._lineage import Lineage
130
+ lineage_client = Lineage()
131
+ result = lineage_client.lineageCSVSample(args)
132
+
133
+ if isinstance(result, dict) and result.get("success"):
134
+ console.print(f"[green]SUCCESS: Sample lineage CSV generated: {output_file} ({num_samples} rows, template: {template})[/green]")
135
+ else:
136
+ error_msg = result.get('error') if isinstance(result, dict) else str(result)
137
+ console.print(f"[red]ERROR: Failed to generate sample lineage CSV: {error_msg}[/red]")
138
+
139
+ except Exception as e:
140
+ console.print(f"[red]ERROR: Error executing lineage sample: {str(e)}[/red]")
141
+
142
+
143
+ @lineage.command()
144
+ @click.pass_context
145
+ def templates(ctx):
146
+ """Get available CSV lineage templates"""
147
+ try:
148
+ if ctx.obj.get("mock"):
149
+ console.print("[yellow][MOCK] lineage templates command[/yellow]")
150
+ console.print("[green]MOCK lineage templates completed successfully[/green]")
151
+ return
152
+
153
+ args = {}
154
+
155
+ from purviewcli.client._lineage import Lineage
156
+ lineage_client = Lineage()
157
+ result = lineage_client.lineageCSVTemplates(args)
158
+
159
+ if result:
160
+ console.print("[green]SUCCESS: Lineage templates retrieved successfully[/green]")
161
+ console.print(json.dumps(result, indent=2))
162
+ else:
163
+ console.print("[yellow][!] Lineage templates completed with no result[/yellow]")
164
+
165
+ except Exception as e:
166
+ console.print(f"[red]ERROR: Error executing lineage templates: {str(e)}[/red]")
167
+
168
+
169
+ @lineage.command()
170
+ @click.option('--guid', required=True, help='The globally unique identifier of the entity')
171
+ @click.option('--depth', type=int, default=3, help='The number of hops for lineage')
172
+ @click.option('--width', type=int, default=6, help='The number of max expanding width in lineage')
173
+ @click.option('--direction', default='BOTH',
174
+ help='The direction of the lineage: INPUT, OUTPUT or BOTH')
175
+ @click.option('--output', default='json', help='Output format: json, table')
176
+ @click.pass_context
177
+ def read(ctx, guid, depth, width, direction, output):
178
+ """Read lineage for an entity"""
179
+ try:
180
+ if ctx.obj.get("mock"):
181
+ console.print("[yellow][MOCK] lineage read command[/yellow]")
182
+ console.print(f"[dim]GUID: {guid}[/dim]")
183
+ console.print(f"[dim]Depth: {depth}, Width: {width}, Direction: {direction}[/dim]")
184
+ console.print("[green]MOCK lineage read completed successfully[/green]")
185
+ return
186
+
187
+ args = {
188
+ "--guid": guid,
189
+ "--depth": depth,
190
+ "--width": width,
191
+ "--direction": direction,
192
+ "--output": output,
193
+ }
194
+
195
+ from purviewcli.client._lineage import Lineage
196
+ lineage_client = Lineage()
197
+ result = lineage_client.lineageRead(args)
198
+
199
+ if result:
200
+ console.print("[green]SUCCESS: Lineage read completed successfully[/green]")
201
+ console.print(json.dumps(result, indent=2))
202
+ else:
203
+ console.print("[yellow][!] Lineage read completed with no result[/yellow]")
204
+
205
+ except Exception as e:
206
+ console.print(f"[red]ERROR: Error executing lineage read: {str(e)}[/red]")
207
+
208
+
209
+ @lineage.command()
210
+ @click.option('--entity-guid', required=True, help='Entity GUID for impact analysis')
211
+ @click.option('--output-file', help='Export results to file')
212
+ @click.pass_context
213
+ def impact(ctx, entity_guid, output_file):
214
+ """Analyze lineage impact for an entity"""
215
+ try:
216
+ if ctx.obj.get("mock"):
217
+ console.print("[yellow][MOCK] lineage impact command[/yellow]")
218
+ console.print(f"[dim]Entity GUID: {entity_guid}[/dim]")
219
+ console.print(f"[dim]Output File: {output_file}[/dim]")
220
+ console.print("[green]MOCK lineage impact completed successfully[/green]")
221
+ return
222
+
223
+ args = {
224
+ "--entity-guid": entity_guid,
225
+ "--output-file": output_file,
226
+ }
227
+
228
+ from purviewcli.client._lineage import Lineage
229
+ lineage_client = Lineage()
230
+ result = lineage_client.lineageImpact(args)
231
+
232
+ if result:
233
+ console.print("[green]SUCCESS: Lineage impact analysis completed successfully[/green]")
234
+ console.print(json.dumps(result, indent=2))
235
+ else:
236
+ console.print("[yellow][!] Lineage impact analysis completed with no result[/yellow]")
237
+
238
+ except Exception as e:
239
+ console.print(f"[red]ERROR: Error executing lineage impact: {str(e)}[/red]")
240
+
241
+
242
+ @lineage.command()
243
+ @click.option('--entity-guid', required=True, help='Entity GUID for advanced lineage operations')
244
+ @click.option('--direction', default='BOTH', help='Analysis direction: INPUT, OUTPUT, or BOTH')
245
+ @click.option('--depth', type=int, default=3, help='Analysis depth')
246
+ @click.option('--output-file', help='Export results to file')
247
+ @click.pass_context
248
+ def analyze(ctx, entity_guid, direction, depth, output_file):
249
+ """Perform advanced lineage analysis"""
250
+ try:
251
+ if ctx.obj.get("mock"):
252
+ console.print("[yellow][MOCK] lineage analyze command[/yellow]")
253
+ console.print(f"[dim]Entity GUID: {entity_guid}[/dim]")
254
+ console.print(f"[dim]Direction: {direction}, Depth: {depth}[/dim]")
255
+ console.print(f"[dim]Output File: {output_file}[/dim]")
256
+ console.print("[green]MOCK lineage analyze completed successfully[/green]")
257
+ return
258
+
259
+ args = {
260
+ "--entity-guid": entity_guid,
261
+ "--direction": direction,
262
+ "--depth": depth,
263
+ "--output-file": output_file,
264
+ }
265
+
266
+ from purviewcli.client._lineage import Lineage
267
+ lineage_client = Lineage()
268
+ result = lineage_client.lineageAnalyze(args)
269
+
270
+ if result:
271
+ console.print("[green]SUCCESS: Lineage analysis completed successfully[/green]")
272
+ console.print(json.dumps(result, indent=2))
273
+ else:
274
+ console.print("[yellow][!] Lineage analysis completed with no result[/yellow]")
275
+
276
+ except Exception as e:
277
+ console.print(f"[red]ERROR: Error executing lineage analyze: {str(e)}[/red]")
278
+
279
+
280
+ @lineage.command(name="create-bulk")
281
+ @click.argument('json_file', type=click.Path(exists=True))
282
+ @click.pass_context
283
+ def create_bulk(ctx, json_file):
284
+ """Create lineage relationships in bulk from a JSON file (official API)."""
285
+ try:
286
+ if ctx.obj.get("mock"):
287
+ console.print("[yellow][MOCK] lineage create-bulk command[/yellow]")
288
+ console.print(f"[dim]File: {json_file}[/dim]")
289
+ console.print("[green]MOCK lineage create-bulk completed successfully[/green]")
290
+ return
291
+
292
+ from purviewcli.client._lineage import Lineage
293
+ lineage_client = Lineage()
294
+ args = {'--payloadFile': json_file}
295
+ result = lineage_client.lineageBulkCreate(args)
296
+ console.print("[green][OK] Bulk lineage creation completed successfully[/green]")
297
+ console.print(json.dumps(result, indent=2))
298
+ except Exception as e:
299
+ console.print(f"[red]ERROR: Error executing lineage create-bulk: {str(e)}[/red]")
300
+
301
+
302
+ @lineage.command(name="analyze-column")
303
+ @click.option('--guid', required=True, help='The globally unique identifier of the entity')
304
+ @click.option('--column-name', required=True, help='The name of the column to analyze')
305
+ @click.option('--direction', default='BOTH', help='The direction of the lineage: INPUT, OUTPUT or BOTH')
306
+ @click.option('--depth', type=int, default=3, help='The number of hops for lineage')
307
+ @click.option('--output', default='json', help='Output format: json, table')
308
+ @click.pass_context
309
+ def analyze_column(ctx, guid, column_name, direction, depth, output):
310
+ """Analyze column-level lineage for a specific entity and column"""
311
+ try:
312
+ if ctx.obj.get("mock"):
313
+ console.print("[yellow][MOCK] lineage analyze-column command[/yellow]")
314
+ console.print(f"[dim]GUID: {guid}, Column: {column_name}, Direction: {direction}, Depth: {depth}[/dim]")
315
+ console.print("[green]MOCK lineage analyze-column completed successfully[/green]")
316
+ return
317
+
318
+ args = {
319
+ "--guid": guid,
320
+ "--columnName": column_name,
321
+ "--direction": direction,
322
+ "--depth": depth,
323
+ "--output": output,
324
+ }
325
+
326
+ from purviewcli.client._lineage import Lineage
327
+ lineage_client = Lineage()
328
+ result = lineage_client.lineageAnalyzeColumn(args)
329
+
330
+ if result:
331
+ console.print("[green]SUCCESS: Column-level lineage analysis completed successfully[/green]")
332
+ console.print(json.dumps(result, indent=2))
333
+ else:
334
+ console.print("[yellow][!] Column-level lineage analysis completed with no result[/yellow]")
335
+
336
+ except Exception as e:
337
+ console.print(f"[red]ERROR: Error executing lineage analyze-column: {str(e)}[/red]")
338
+
339
+
340
+ @lineage.command(name="partial")
341
+ @click.option('--guid', required=True, help='The globally unique identifier of the entity')
342
+ @click.option('--columns', help='Comma-separated list of columns to restrict lineage to (optional)')
343
+ @click.option('--relationship-types', help='Comma-separated list of relationship types to include (optional)')
344
+ @click.option('--depth', type=int, default=3, help='The number of hops for lineage')
345
+ @click.option('--direction', default='BOTH', help='The direction of the lineage: INPUT, OUTPUT or BOTH')
346
+ @click.option('--output', default='json', help='Output format: json, table')
347
+ @click.pass_context
348
+ def partial_lineage(ctx, guid, columns, relationship_types, depth, direction, output):
349
+ """Query partial lineage for an entity (filter by columns/relationship types)"""
350
+ try:
351
+ if ctx.obj.get("mock"):
352
+ console.print("[yellow][MOCK] lineage partial command[/yellow]")
353
+ console.print(f"[dim]GUID: {guid}, Columns: {columns}, Types: {relationship_types}, Depth: {depth}, Direction: {direction}[/dim]")
354
+ console.print("[green]MOCK lineage partial completed successfully[/green]")
355
+ return
356
+
357
+ args = {
358
+ "--guid": guid,
359
+ "--columns": columns,
360
+ "--relationshipTypes": relationship_types,
361
+ "--depth": depth,
362
+ "--direction": direction,
363
+ "--output": output,
364
+ }
365
+
366
+ from purviewcli.client._lineage import Lineage
367
+ lineage_client = Lineage()
368
+ # Assume backend supports filtering; if not, filter result in CLI
369
+ result = lineage_client.lineageRead(args)
370
+ if columns or relationship_types:
371
+ # Filter result in CLI if backend does not support
372
+ def filter_fn(rel):
373
+ col_ok = True
374
+ type_ok = True
375
+ if columns:
376
+ col_list = [c.strip() for c in columns.split(",") if c.strip()]
377
+ col_ok = any(
378
+ (rel.get("source_column") in col_list or rel.get("target_column") in col_list)
379
+ for rel in result.get("relations", [])
380
+ )
381
+ if relationship_types:
382
+ type_list = [t.strip() for t in relationship_types.split(",") if t.strip()]
383
+ type_ok = rel.get("relationship_type") in type_list
384
+ return col_ok and type_ok
385
+ if "relations" in result:
386
+ result["relations"] = [rel for rel in result["relations"] if filter_fn(rel)]
387
+ if result:
388
+ console.print("[green]SUCCESS: Partial lineage query completed successfully[/green]")
389
+ console.print(json.dumps(result, indent=2))
390
+ else:
391
+ console.print("[yellow][!] Partial lineage query completed with no result[/yellow]")
392
+ except Exception as e:
393
+ console.print(f"[red]ERROR: Error executing lineage partial: {str(e)}[/red]")
394
+
395
+
396
+ @lineage.command(name="impact-report")
397
+ @click.option('--entity-guid', required=True, help='Entity GUID for impact analysis')
398
+ @click.option('--output-file', help='Export impact report to file (JSON)')
399
+ @click.pass_context
400
+ def impact_report(ctx, entity_guid, output_file):
401
+ """Generate and export a detailed lineage impact analysis report"""
402
+ try:
403
+ if ctx.obj.get("mock"):
404
+ console.print("[yellow][MOCK] lineage impact-report command[/yellow]")
405
+ console.print(f"[dim]Entity GUID: {entity_guid}, Output File: {output_file}[/dim]")
406
+ console.print("[green]MOCK lineage impact-report completed successfully[/green]")
407
+ return
408
+ from purviewcli.client.lineage_visualization import LineageReporting, AdvancedLineageAnalyzer
409
+ from purviewcli.client.api_client import PurviewClient
410
+ analyzer = AdvancedLineageAnalyzer(PurviewClient())
411
+ reporting = LineageReporting(analyzer)
412
+ import asyncio
413
+ report = asyncio.run(reporting.generate_impact_report(entity_guid, output_file or f"impact_report_{entity_guid}.json"))
414
+ console.print("[green][OK] Impact analysis report generated successfully[/green]")
415
+ if output_file:
416
+ console.print(f"[cyan]Report saved to {output_file}[/cyan]")
417
+ else:
418
+ console.print(json.dumps(report, indent=2))
419
+ except Exception as e:
420
+ console.print(f"[red]ERROR: Error executing lineage impact-report: {str(e)}[/red]")
421
+
422
+
423
+ @lineage.command(name="read-by-attribute")
424
+ @click.option('--type-name', required=True, help='The name of the entity type')
425
+ @click.option('--qualified-name', required=True, help='The qualified name of the entity')
426
+ @click.option('--depth', type=int, default=3, help='The number of hops for lineage')
427
+ @click.option('--width', type=int, default=6, help='The number of max expanding width in lineage')
428
+ @click.option('--direction', default='BOTH', help='The direction of the lineage: INPUT, OUTPUT or BOTH')
429
+ @click.option('--offset', type=int, default=0, help='Offset for paginated traversal (if supported)')
430
+ @click.option('--limit', type=int, default=100, help='Limit for paginated traversal (if supported)')
431
+ @click.option('--output', default='json', help='Output format: json, table')
432
+ @click.pass_context
433
+ def read_by_attribute(ctx, type_name, qualified_name, depth, width, direction, offset, limit, output):
434
+ """Read lineage for an entity by unique attribute (type and qualified name)"""
435
+ try:
436
+ if ctx.obj.get("mock"):
437
+ console.print("[yellow][MOCK] lineage read-by-attribute command[/yellow]")
438
+ console.print(f"[dim]Type: {type_name}, Qualified Name: {qualified_name}, Depth: {depth}, Direction: {direction}[/dim]")
439
+ console.print("[green]MOCK lineage read-by-attribute completed successfully[/green]")
440
+ return
441
+ args = {
442
+ "--typeName": type_name,
443
+ "--qualifiedName": qualified_name,
444
+ "--depth": depth,
445
+ "--width": width,
446
+ "--direction": direction,
447
+ "--offset": offset,
448
+ "--limit": limit,
449
+ "--output": output,
450
+ }
451
+ from purviewcli.client._lineage import Lineage
452
+ lineage_client = Lineage()
453
+ result = lineage_client.lineageReadUniqueAttribute(args)
454
+ if result:
455
+ console.print("[green][OK] Lineage by attribute read completed successfully[/green]")
456
+ console.print(json.dumps(result, indent=2))
457
+ else:
458
+ console.print("[yellow][!] Lineage by attribute read completed with no result[/yellow]")
459
+ except Exception as e:
460
+ console.print(f"[red]ERROR: Error executing lineage read-by-attribute: {str(e)}[/red]")
461
+
462
+
463
+ @lineage.command(name="read")
464
+ @click.option('--guid', required=True, help='The GUID of the entity to get lineage for')
465
+ @click.option('--direction', required=False, type=click.Choice(['INPUT', 'OUTPUT', 'BOTH'], case_sensitive=False), default='BOTH', help='Lineage direction')
466
+ @click.option('--depth', required=False, type=int, default=3, help='Depth of lineage traversal')
467
+ @click.pass_context
468
+ def read_lineage(ctx, guid, direction, depth):
469
+ """Read lineage information for an entity by GUID"""
470
+ try:
471
+ from purviewcli.client._lineage import Lineage
472
+ lineage_client = Lineage()
473
+ args = {"--guid": guid, "--direction": direction, "--depth": depth}
474
+ result = lineage_client.get_lineage_by_guid(args)
475
+ console.print("[green][OK] Lineage read completed successfully[/green]")
476
+ console.print(json.dumps(result, indent=2))
477
+ except Exception as e:
478
+ console.print(f"[red]ERROR: Error executing lineage read: {str(e)}[/red]")
479
+
480
+
481
+ @lineage.command(name="create-column")
482
+ @click.option('--source-table-guid', required=True, help='GUID of the source table')
483
+ @click.option('--target-table-guid', required=True, multiple=True, help='GUID of the target table(s) - can be specified multiple times')
484
+ @click.option('--source-column', required=True, help='Name of the source column')
485
+ @click.option('--target-column', required=True, multiple=True, help='Name of the target column(s) - can be specified multiple times')
486
+ @click.option('--process-name', required=False, help='Name of the transformation process')
487
+ @click.option('--description', required=False, help='Description of the column lineage')
488
+ @click.option('--owner', required=False, default='data-engineering', help='Owner of the lineage')
489
+ @click.option('--validate-types', is_flag=True, default=False, help='Validate column type compatibility')
490
+ @click.pass_context
491
+ def create_column_lineage(ctx, source_table_guid, target_table_guid, source_column, target_column, process_name, description, owner, validate_types):
492
+ """Create column-level lineage between tables (supports 1 source → N targets)
493
+
494
+ Examples:
495
+ # Single source to single target
496
+ pvw lineage create-column \\
497
+ --source-table-guid 4abfa830-7f67-4669-a9c9-0ef6f6f60000 \\
498
+ --target-table-guid 3c1d655c-ac7a-4011-8f9e-65f6f6f60000 \\
499
+ --source-column CityKey \\
500
+ --target-column CityKey
501
+
502
+ # Single source to multiple targets
503
+ pvw lineage create-column \\
504
+ --source-table-guid <source-guid> \\
505
+ --target-table-guid <target1-guid> \\
506
+ --target-table-guid <target2-guid> \\
507
+ --source-column CityKey \\
508
+ --target-column CityKey \\
509
+ --target-column City_ID \\
510
+ --validate-types
511
+ """
512
+ try:
513
+ from purviewcli.client._lineage import Lineage
514
+ lineage_client = Lineage()
515
+
516
+ # Convert tuples to lists
517
+ target_table_guids = list(target_table_guid)
518
+ target_columns = list(target_column)
519
+
520
+ # Validate that we have matching number of targets
521
+ if len(target_table_guids) != len(target_columns):
522
+ console.print("[red]ERROR: Number of target tables must match number of target columns[/red]")
523
+ console.print(f" Target tables: {len(target_table_guids)}")
524
+ console.print(f" Target columns: {len(target_columns)}")
525
+ return
526
+
527
+ args = {
528
+ "--source-table-guid": source_table_guid,
529
+ "--target-table-guids": target_table_guids,
530
+ "--source-column": source_column,
531
+ "--target-columns": target_columns,
532
+ "--process-name": process_name or f"{source_column}_Multi_Mapping",
533
+ "--description": description or f"Column-level lineage: {source_column} -> {', '.join(target_columns)}",
534
+ "--owner": owner,
535
+ "--validate-types": validate_types
536
+ }
537
+
538
+ result = lineage_client.lineageCreateColumnLevel(args)
539
+
540
+ if result.get("status") == "success":
541
+ console.print("[green]SUCCESS: Column-level lineage created successfully[/green]")
542
+ data = result.get("data", {})
543
+ created = data.get("mutatedEntities", {}).get("CREATE", [])
544
+ if created:
545
+ console.print(f"\n[cyan]Processes created: {len(created)}[/cyan]")
546
+ for i, process in enumerate(created, 1):
547
+ console.print(f"\n {i}. {process.get('displayText')}")
548
+ console.print(f" GUID: {process.get('guid')}")
549
+ console.print(f" Description: {process.get('attributes', {}).get('description')}")
550
+ else:
551
+ console.print(f"[red]ERROR: {result.get('message', 'Unknown error')}[/red]")
552
+
553
+ except Exception as e:
554
+ console.print(f"[red]ERROR: Error creating column lineage: {str(e)}[/red]")
555
+
556
+
557
+ @lineage.command(name="import-column-csv", help="Batch import column lineage from CSV file")
558
+ @click.argument("csv_file", type=click.Path(exists=True))
559
+ @click.option("--validate-types", is_flag=True, default=False, help="Validate column type compatibility")
560
+ @click.option("--dry-run", is_flag=True, default=False, help="Validate CSV without creating lineage")
561
+ def import_column_csv(csv_file, validate_types, dry_run):
562
+ """
563
+ Import column-level lineage from CSV file in batch.
564
+
565
+ CSV Format:
566
+ source_table_guid,source_column,target_table_guid,target_column,process_name,description,owner
567
+
568
+ Example CSV content:
569
+ 4abfa830-7f67-4669-a9c9-0ef6f6f60000,CityKey,3c1d655c-ac7a-4011-8f9e-65f6f6f60000,CityKey,City_ETL,Map city dimension to sales,data-engineering
570
+ 4abfa830-7f67-4669-a9c9-0ef6f6f60000,CityName,21ceaca7-a8eb-4085-afed-335e84241d51,city_name,City_Name_ETL,Map city names,etl-team
571
+ """
572
+ import csv
573
+ from rich.table import Table
574
+
575
+ try:
576
+ console.print(f"\n[cyan]Reading CSV file: {csv_file}[/cyan]")
577
+
578
+ # Read CSV file
579
+ rows = []
580
+ with open(csv_file, 'r', encoding='utf-8') as f:
581
+ reader = csv.DictReader(f)
582
+ required_columns = ['source_table_guid', 'source_column', 'target_table_guid', 'target_column']
583
+
584
+ # Validate CSV headers
585
+ fieldnames = reader.fieldnames or []
586
+ if not all(col in fieldnames for col in required_columns):
587
+ missing = [col for col in required_columns if col not in fieldnames]
588
+ console.print(f"[red]ERROR: Missing required columns: {', '.join(missing)}[/red]")
589
+ console.print(f"Required: {', '.join(required_columns)}")
590
+ console.print(f"Found: {', '.join(fieldnames)}")
591
+ return
592
+
593
+ for row in reader:
594
+ rows.append(row)
595
+
596
+ if not rows:
597
+ console.print("[yellow]WARNING: No data rows found in CSV file[/yellow]")
598
+ return
599
+
600
+ console.print(f"Found {len(rows)} lineage(s) to create")
601
+
602
+ # Validation phase
603
+ validation_errors = []
604
+ for idx, row in enumerate(rows, 1):
605
+ # Check required fields
606
+ for field in required_columns:
607
+ if not row.get(field) or row.get(field).strip() == '':
608
+ validation_errors.append(f"Row {idx}: Missing value for '{field}'")
609
+
610
+ # Validate GUID format (basic check)
611
+ for guid_field in ['source_table_guid', 'target_table_guid']:
612
+ guid_value = row.get(guid_field, '').strip()
613
+ if guid_value and len(guid_value) != 36:
614
+ validation_errors.append(f"Row {idx}: Invalid GUID format for '{guid_field}': {guid_value}")
615
+
616
+ if validation_errors:
617
+ console.print("\n[red]Validation Errors:[/red]")
618
+ for error in validation_errors:
619
+ console.print(f" - {error}")
620
+ return
621
+
622
+ console.print("[green]Validation: OK[/green]")
623
+
624
+ if dry_run:
625
+ console.print("\n[yellow]DRY-RUN mode: No lineages will be created[/yellow]")
626
+
627
+ # Show preview table
628
+ table = Table(title="Preview: Column Lineages to Create")
629
+ table.add_column("#", style="cyan")
630
+ table.add_column("Source Column", style="green")
631
+ table.add_column("Target Column", style="yellow")
632
+ table.add_column("Process Name", style="magenta")
633
+
634
+ for idx, row in enumerate(rows, 1):
635
+ table.add_row(
636
+ str(idx),
637
+ f"{row['source_column']}",
638
+ f"{row['target_column']}",
639
+ row.get('process_name', 'Auto-generated')
640
+ )
641
+
642
+ console.print(table)
643
+ return
644
+
645
+ # Creation phase
646
+ console.print("\n[cyan]Creating column lineages...[/cyan]")
647
+ client = Lineage()
648
+
649
+ success_count = 0
650
+ error_count = 0
651
+
652
+ for idx, row in enumerate(rows, 1):
653
+ try:
654
+ console.print(f"\n[{idx}/{len(rows)}] {row['source_column']} -> {row['target_column']}...")
655
+
656
+ args = {
657
+ "--source-table-guid": row['source_table_guid'].strip(),
658
+ "--target-table-guids": [row['target_table_guid'].strip()],
659
+ "--source-column": row['source_column'].strip(),
660
+ "--target-columns": [row['target_column'].strip()],
661
+ "--process-name": row.get('process_name', '').strip() or None,
662
+ "--description": row.get('description', '').strip() or None,
663
+ "--owner": row.get('owner', 'data-engineering').strip(),
664
+ "--validate-types": validate_types
665
+ }
666
+
667
+ result = client.lineageCreateColumnLevel(args)
668
+
669
+ if result.get('status') == 'success':
670
+ console.print(" [green]SUCCESS[/green]")
671
+ success_count += 1
672
+ else:
673
+ console.print(f" [red]FAILED: {result.get('message', 'Unknown error')}[/red]")
674
+ error_count += 1
675
+
676
+ # Rate limiting: small delay between requests
677
+ import time
678
+ time.sleep(0.2)
679
+
680
+ except Exception as e:
681
+ console.print(f" [red]ERROR: {str(e)}[/red]")
682
+ error_count += 1
683
+
684
+ # Summary
685
+ console.print(f"\n[bold]Summary:[/bold]")
686
+ console.print(f" [green]SUCCESS: {success_count}[/green]")
687
+ console.print(f" [red]FAILED: {error_count}[/red]")
688
+ console.print(f" Total: {len(rows)}")
689
+
690
+ except Exception as e:
691
+ console.print(f"[red]ERROR: {str(e)}[/red]")
692
+
693
+
694
+ @lineage.command(name="list-column", help="List existing column-level lineage")
695
+ @click.option("--source-table-guid", help="Filter by source table GUID")
696
+ @click.option("--target-table-guid", help="Filter by target table GUID")
697
+ @click.option("--format", "output_format", type=click.Choice(['table', 'json']), default='table', help="Output format")
698
+ def list_column_lineage(source_table_guid, target_table_guid, output_format):
699
+ """
700
+ List existing column-level lineage relationships.
701
+
702
+ This command queries for Process entities that have column-type inputs and outputs,
703
+ representing column-level lineage mappings.
704
+
705
+ Examples:
706
+ pvw lineage list-column
707
+ pvw lineage list-column --source-table-guid <guid>
708
+ pvw lineage list-column --format json
709
+ """
710
+ from rich.table import Table
711
+
712
+ try:
713
+ client = Lineage()
714
+
715
+ # Search for Process entities with column inputs/outputs
716
+ # We'll use the search endpoint to find all Process entities
717
+ from purviewcli.client.endpoint import get_data
718
+ from purviewcli.client.endpoints import get_api_version_params
719
+
720
+ console.print("\n[cyan]Searching for column-level lineage...[/cyan]")
721
+
722
+ # Build search query for Process entities
723
+ search_payload = {
724
+ "keywords": "*",
725
+ "filter": {
726
+ "typeName": "Process"
727
+ },
728
+ "limit": 1000
729
+ }
730
+
731
+ search_result = get_data({
732
+ "app": "catalog",
733
+ "method": "POST",
734
+ "endpoint": "/datamap/api/atlas/v2/search/basic",
735
+ "params": get_api_version_params("datamap"),
736
+ "payload": search_payload
737
+ })
738
+
739
+ if not search_result or search_result.get('value', []) == []:
740
+ console.print("[yellow]No column lineages found[/yellow]")
741
+ return
742
+
743
+ entities = search_result.get('value', [])
744
+ console.print(f"Found {len(entities)} Process entities")
745
+
746
+ # Filter for column-level lineage and apply filters
747
+ column_lineages = []
748
+
749
+ for entity in entities:
750
+ entity_guid = entity.get('id')
751
+
752
+ # Get full entity details to check inputs/outputs
753
+ full_entity = get_data({
754
+ "app": "catalog",
755
+ "method": "GET",
756
+ "endpoint": f"/datamap/api/atlas/v2/entity/guid/{entity_guid}",
757
+ "params": get_api_version_params("datamap")
758
+ })
759
+
760
+ if not full_entity:
761
+ continue
762
+
763
+ entity_data = full_entity.get('entity', {})
764
+ inputs = entity_data.get('relationshipAttributes', {}).get('inputs', [])
765
+ outputs = entity_data.get('relationshipAttributes', {}).get('outputs', [])
766
+
767
+ # Check if inputs/outputs are columns (not tables)
768
+ has_column_inputs = any(inp.get('typeName') == 'column' for inp in inputs)
769
+ has_column_outputs = any(out.get('typeName') == 'column' for out in outputs)
770
+
771
+ if not (has_column_inputs and has_column_outputs):
772
+ continue
773
+
774
+ # Extract column information
775
+ for inp in inputs:
776
+ if inp.get('typeName') != 'column':
777
+ continue
778
+
779
+ for out in outputs:
780
+ if out.get('typeName') != 'column':
781
+ continue
782
+
783
+ # Get parent table GUIDs from column qualified names
784
+ source_qualified_name = inp.get('attributes', {}).get('qualifiedName', '')
785
+ target_qualified_name = out.get('attributes', {}).get('qualifiedName', '')
786
+
787
+ # Apply filters if provided
788
+ if source_table_guid or target_table_guid:
789
+ # Get column details to find parent table
790
+ source_col = get_data({
791
+ "app": "catalog",
792
+ "method": "GET",
793
+ "endpoint": f"/datamap/api/atlas/v2/entity/guid/{inp.get('guid')}",
794
+ "params": get_api_version_params("datamap")
795
+ })
796
+
797
+ target_col = get_data({
798
+ "app": "catalog",
799
+ "method": "GET",
800
+ "endpoint": f"/datamap/api/atlas/v2/entity/guid/{out.get('guid')}",
801
+ "params": get_api_version_params("datamap")
802
+ })
803
+
804
+ source_table = source_col.get('entity', {}).get('relationshipAttributes', {}).get('table', {}).get('guid', '')
805
+ target_table = target_col.get('entity', {}).get('relationshipAttributes', {}).get('table', {}).get('guid', '')
806
+
807
+ if source_table_guid and source_table != source_table_guid:
808
+ continue
809
+ if target_table_guid and target_table != target_table_guid:
810
+ continue
811
+
812
+ column_lineages.append({
813
+ 'process_guid': entity_guid,
814
+ 'process_name': entity_data.get('attributes', {}).get('name', 'N/A'),
815
+ 'description': entity_data.get('attributes', {}).get('description', ''),
816
+ 'source_column': inp.get('displayText', 'N/A'),
817
+ 'source_guid': inp.get('guid', ''),
818
+ 'target_column': out.get('displayText', 'N/A'),
819
+ 'target_guid': out.get('guid', ''),
820
+ 'owner': entity_data.get('attributes', {}).get('owner', 'N/A')
821
+ })
822
+
823
+ if not column_lineages:
824
+ console.print("[yellow]No column-level lineages found matching the criteria[/yellow]")
825
+ return
826
+
827
+ # Output results
828
+ if output_format == 'json':
829
+ import json
830
+ console.print(json.dumps(column_lineages, indent=2))
831
+ else:
832
+ table = Table(title=f"Column Lineages ({len(column_lineages)} found)")
833
+ table.add_column("Process Name", style="cyan", no_wrap=False)
834
+ table.add_column("Source Column", style="green")
835
+ table.add_column("Target Column", style="yellow")
836
+ table.add_column("Owner", style="magenta")
837
+ table.add_column("Process GUID", style="dim")
838
+
839
+ for lineage in column_lineages:
840
+ table.add_row(
841
+ lineage['process_name'][:40],
842
+ lineage['source_column'],
843
+ lineage['target_column'],
844
+ lineage['owner'],
845
+ lineage['process_guid'][:8] + "..."
846
+ )
847
+
848
+ console.print(table)
849
+
850
+ except Exception as e:
851
+ console.print(f"[red]ERROR: {str(e)}[/red]")
852
+
853
+
854
+ @lineage.command(name="delete-column", help="Delete a column-level lineage by Process GUID")
855
+ @click.option("--process-guid", required=True, help="GUID of the Process entity to delete")
856
+ @click.option("--force", is_flag=True, default=False, help="Skip confirmation prompt")
857
+ def delete_column_lineage(process_guid, force):
858
+ """
859
+ Delete a column-level lineage Process entity.
860
+
861
+ This removes the Process entity that represents the column-level lineage mapping,
862
+ which will also remove the associated relationships.
863
+
864
+ Examples:
865
+ pvw lineage delete-column --process-guid <guid>
866
+ pvw lineage delete-column --process-guid <guid> --force
867
+ """
868
+ from purviewcli.client.endpoint import get_data
869
+ from purviewcli.client.endpoints import get_api_version_params
870
+
871
+ try:
872
+ # First, get the Process entity details to show what will be deleted
873
+ console.print(f"\n[cyan]Fetching Process entity details...[/cyan]")
874
+
875
+ entity_details = get_data({
876
+ "app": "catalog",
877
+ "method": "GET",
878
+ "endpoint": f"/datamap/api/atlas/v2/entity/guid/{process_guid}",
879
+ "params": get_api_version_params("datamap")
880
+ })
881
+
882
+ if not entity_details:
883
+ console.print(f"[red]ERROR: Process entity not found with GUID: {process_guid}[/red]")
884
+ return
885
+
886
+ entity_data = entity_details.get('entity', {})
887
+ process_name = entity_data.get('attributes', {}).get('name', 'N/A')
888
+ description = entity_data.get('attributes', {}).get('description', 'N/A')
889
+ inputs = entity_data.get('relationshipAttributes', {}).get('inputs', [])
890
+ outputs = entity_data.get('relationshipAttributes', {}).get('outputs', [])
891
+
892
+ # Display what will be deleted
893
+ console.print(f"\n[bold]Process to delete:[/bold]")
894
+ console.print(f" Name: {process_name}")
895
+ console.print(f" Description: {description}")
896
+ console.print(f" GUID: {process_guid}")
897
+
898
+ if inputs:
899
+ console.print(f"\n [green]Inputs ({len(inputs)}):[/green]")
900
+ for inp in inputs:
901
+ console.print(f" - {inp.get('displayText', 'N/A')} ({inp.get('typeName', 'N/A')})")
902
+
903
+ if outputs:
904
+ console.print(f"\n [yellow]Outputs ({len(outputs)}):[/yellow]")
905
+ for out in outputs:
906
+ console.print(f" - {out.get('displayText', 'N/A')} ({out.get('typeName', 'N/A')})")
907
+
908
+ # Confirmation unless --force
909
+ if not force:
910
+ console.print(f"\n[bold red]WARNING: This action cannot be undone![/bold red]")
911
+ confirm = input("Type 'yes' to confirm deletion: ")
912
+ if confirm.lower() != 'yes':
913
+ console.print("[yellow]Deletion cancelled[/yellow]")
914
+ return
915
+
916
+ # Delete the Process entity
917
+ console.print(f"\n[cyan]Deleting Process entity...[/cyan]")
918
+
919
+ delete_result = get_data({
920
+ "app": "catalog",
921
+ "method": "DELETE",
922
+ "endpoint": f"/datamap/api/atlas/v2/entity/guid/{process_guid}",
923
+ "params": get_api_version_params("datamap")
924
+ })
925
+
926
+ if delete_result:
927
+ console.print(f"[green]SUCCESS: Column lineage deleted[/green]")
928
+ console.print(f"Process GUID: {process_guid}")
929
+
930
+ # Show deleted entities
931
+ if isinstance(delete_result, dict):
932
+ deleted = delete_result.get('mutatedEntities', {}).get('DELETE', [])
933
+ if deleted:
934
+ console.print(f"\nDeleted {len(deleted)} entity(ies):")
935
+ for entity in deleted:
936
+ console.print(f" - {entity.get('displayText', 'N/A')} ({entity.get('guid', 'N/A')[:8]}...)")
937
+ else:
938
+ console.print(f"[red]ERROR: Failed to delete Process entity[/red]")
939
+
940
+ except Exception as e:
941
+ console.print(f"[red]ERROR: {str(e)}[/red]")
942
+
943
+
944
+ @lineage.command()
945
+ @click.option('--source-guid', required=True, help='Source entity GUID')
946
+ @click.option('--target-guid', required=True, help='Target entity GUID')
947
+ @click.option('--source-type', default='azure_sql_table', help='Source entity type (default: azure_sql_table)')
948
+ @click.option('--target-type', default='azure_sql_table', help='Target entity type (default: azure_sql_table)')
949
+ @click.option('--column-mapping', default='', help='Optional column mapping JSON')
950
+ @click.pass_context
951
+ def create_direct(ctx, source_guid, target_guid, source_type, target_type, column_mapping):
952
+ """
953
+ Create direct lineage between two datasets (UI-style, Process hidden).
954
+
955
+ This creates the same type of lineage as the Purview UI manual lineage,
956
+ where the Process entity exists but is not displayed as a visible box.
957
+
958
+ Examples:
959
+ pvw lineage create-direct --source-guid <guid1> --target-guid <guid2>
960
+ pvw lineage create-direct --source-guid <guid1> --target-guid <guid2> --source-type azure_sql_view
961
+ """
962
+ try:
963
+ if ctx.obj and ctx.obj.get("mock"):
964
+ console.print("[yellow][MOCK] lineage create-direct command[/yellow]")
965
+ return
966
+
967
+ from purviewcli.client._lineage import Lineage
968
+
969
+ console.print(f"[cyan]Creating direct lineage...[/cyan]")
970
+ console.print(f" Source: {source_guid} ({source_type})")
971
+ console.print(f" Target: {target_guid} ({target_type})")
972
+
973
+ lineage_client = Lineage()
974
+ args = {
975
+ "--source-guid": source_guid,
976
+ "--target-guid": target_guid,
977
+ "--source-type": source_type,
978
+ "--target-type": target_type,
979
+ "--column-mapping": column_mapping
980
+ }
981
+
982
+ result = lineage_client.lineageCreateDirect(args)
983
+
984
+ if result:
985
+ console.print("[green]SUCCESS: Direct lineage created[/green]")
986
+ console.print(json.dumps(result, indent=2))
987
+
988
+ # Extract relationship GUID if present
989
+ if isinstance(result, dict) and 'guid' in result:
990
+ console.print(f"\n[bold]Relationship GUID:[/bold] {result['guid']}")
991
+ else:
992
+ console.print("[yellow]Direct lineage creation completed (no result returned)[/yellow]")
993
+
994
+ except Exception as e:
995
+ console.print(f"[red]ERROR: {str(e)}[/red]")
996
+ import traceback
997
+ if ctx.obj and ctx.obj.get("debug"):
998
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
999
+
1000
+
1001
+ @lineage.command()
1002
+ @click.option('--process-guid', required=True, help='GUID of the Process entity')
1003
+ @click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json']),
1004
+ help='Output format')
1005
+ @click.pass_context
1006
+ def show_relationships(ctx, process_guid, output_format):
1007
+ """Show all relationships for a Process entity."""
1008
+ try:
1009
+ if ctx.obj and ctx.obj.get("mock"):
1010
+ console.print("[yellow][MOCK] lineage show-relationships command[/yellow]")
1011
+ return
1012
+
1013
+ from purviewcli.client.endpoint import get_data
1014
+ from purviewcli.client.endpoints import get_api_version_params
1015
+ from rich.table import Table
1016
+
1017
+ console.print(f"[cyan]Fetching Process entity: {process_guid}...[/cyan]")
1018
+
1019
+ # Read the Process entity
1020
+ result = get_data({
1021
+ "app": "catalog",
1022
+ "method": "GET",
1023
+ "endpoint": f"/datamap/api/atlas/v2/entity/guid/{process_guid}",
1024
+ "params": get_api_version_params("datamap")
1025
+ })
1026
+
1027
+ if not result or 'entity' not in result:
1028
+ console.print(f"[red]ERROR: Process entity not found[/red]")
1029
+ return
1030
+
1031
+ entity = result['entity']
1032
+
1033
+ # Extract relationship information
1034
+ relationships = entity.get('relationshipAttributes', {})
1035
+
1036
+ if output_format == 'json':
1037
+ console.print(json.dumps(relationships, indent=2))
1038
+ return
1039
+
1040
+ # Table format
1041
+ console.print(f"\n[bold]Process: {entity.get('attributes', {}).get('name', 'N/A')}[/bold]")
1042
+ console.print(f"GUID: {process_guid}")
1043
+ console.print(f"Type: {entity.get('typeName', 'N/A')}")
1044
+
1045
+ # Inputs
1046
+ inputs = relationships.get('inputs', [])
1047
+ if inputs:
1048
+ console.print(f"\n[green]Inputs ({len(inputs)}):[/green]")
1049
+ table = Table(show_header=True, header_style="bold magenta")
1050
+ table.add_column("Name", style="cyan")
1051
+ table.add_column("Type", style="yellow")
1052
+ table.add_column("GUID", style="dim")
1053
+ table.add_column("Qualified Name", style="dim")
1054
+
1055
+ for inp in inputs:
1056
+ table.add_row(
1057
+ inp.get('displayText', 'N/A'),
1058
+ inp.get('typeName', 'N/A'),
1059
+ inp.get('guid', 'N/A')[:8] + '...' if inp.get('guid') else 'N/A',
1060
+ inp.get('attributes', {}).get('qualifiedName', 'N/A')[:50] + '...' if len(inp.get('attributes', {}).get('qualifiedName', '')) > 50 else inp.get('attributes', {}).get('qualifiedName', 'N/A')
1061
+ )
1062
+
1063
+ console.print(table)
1064
+
1065
+ # Outputs
1066
+ outputs = relationships.get('outputs', [])
1067
+ if outputs:
1068
+ console.print(f"\n[blue]Outputs ({len(outputs)}):[/blue]")
1069
+ table = Table(show_header=True, header_style="bold magenta")
1070
+ table.add_column("Name", style="cyan")
1071
+ table.add_column("Type", style="yellow")
1072
+ table.add_column("GUID", style="dim")
1073
+ table.add_column("Qualified Name", style="dim")
1074
+
1075
+ for out in outputs:
1076
+ table.add_row(
1077
+ out.get('displayText', 'N/A'),
1078
+ out.get('typeName', 'N/A'),
1079
+ out.get('guid', 'N/A')[:8] + '...' if out.get('guid') else 'N/A',
1080
+ out.get('attributes', {}).get('qualifiedName', 'N/A')[:50] + '...' if len(out.get('attributes', {}).get('qualifiedName', '')) > 50 else out.get('attributes', {}).get('qualifiedName', 'N/A')
1081
+ )
1082
+
1083
+ console.print(table)
1084
+
1085
+ # Relationship metadata
1086
+ console.print(f"\n[dim]Relationship Types:[/dim]")
1087
+ for rel_type, rel_data in relationships.items():
1088
+ if rel_type not in ['inputs', 'outputs']:
1089
+ console.print(f" {rel_type}: {type(rel_data).__name__}")
1090
+
1091
+ except Exception as e:
1092
+ console.print(f"[red]ERROR: {str(e)}[/red]")
1093
+ import traceback
1094
+ if ctx.obj and ctx.obj.get("debug"):
1095
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
1096
+
1097
+
1098
+ # Remove the duplicate registration and ensure only one 'import' command is registered
1099
+ # lineage.add_command(import_cmd, name='import')
1100
+
1101
+
1102
+ # Make the lineage group available for import
1103
+ __all__ = ['lineage']