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.
- purviewcli/__init__.py +27 -0
- purviewcli/__main__.py +15 -0
- purviewcli/cli/__init__.py +5 -0
- purviewcli/cli/account.py +199 -0
- purviewcli/cli/cli.py +170 -0
- purviewcli/cli/collections.py +502 -0
- purviewcli/cli/domain.py +361 -0
- purviewcli/cli/entity.py +2436 -0
- purviewcli/cli/glossary.py +533 -0
- purviewcli/cli/health.py +250 -0
- purviewcli/cli/insight.py +113 -0
- purviewcli/cli/lineage.py +1103 -0
- purviewcli/cli/management.py +141 -0
- purviewcli/cli/policystore.py +103 -0
- purviewcli/cli/relationship.py +75 -0
- purviewcli/cli/scan.py +357 -0
- purviewcli/cli/search.py +527 -0
- purviewcli/cli/share.py +478 -0
- purviewcli/cli/types.py +831 -0
- purviewcli/cli/unified_catalog.py +3540 -0
- purviewcli/cli/workflow.py +402 -0
- purviewcli/client/__init__.py +21 -0
- purviewcli/client/_account.py +1877 -0
- purviewcli/client/_collections.py +1761 -0
- purviewcli/client/_domain.py +414 -0
- purviewcli/client/_entity.py +3545 -0
- purviewcli/client/_glossary.py +3233 -0
- purviewcli/client/_health.py +501 -0
- purviewcli/client/_insight.py +2873 -0
- purviewcli/client/_lineage.py +2138 -0
- purviewcli/client/_management.py +2202 -0
- purviewcli/client/_policystore.py +2915 -0
- purviewcli/client/_relationship.py +1351 -0
- purviewcli/client/_scan.py +2607 -0
- purviewcli/client/_search.py +1472 -0
- purviewcli/client/_share.py +272 -0
- purviewcli/client/_types.py +2708 -0
- purviewcli/client/_unified_catalog.py +5112 -0
- purviewcli/client/_workflow.py +2734 -0
- purviewcli/client/api_client.py +1295 -0
- purviewcli/client/business_rules.py +675 -0
- purviewcli/client/config.py +231 -0
- purviewcli/client/data_quality.py +433 -0
- purviewcli/client/endpoint.py +123 -0
- purviewcli/client/endpoints.py +554 -0
- purviewcli/client/exceptions.py +38 -0
- purviewcli/client/lineage_visualization.py +797 -0
- purviewcli/client/monitoring_dashboard.py +712 -0
- purviewcli/client/rate_limiter.py +30 -0
- purviewcli/client/retry_handler.py +125 -0
- purviewcli/client/scanning_operations.py +523 -0
- purviewcli/client/settings.py +1 -0
- purviewcli/client/sync_client.py +250 -0
- purviewcli/plugins/__init__.py +1 -0
- purviewcli/plugins/plugin_system.py +709 -0
- pvw_cli-1.2.8.dist-info/METADATA +1618 -0
- pvw_cli-1.2.8.dist-info/RECORD +60 -0
- pvw_cli-1.2.8.dist-info/WHEEL +5 -0
- pvw_cli-1.2.8.dist-info/entry_points.txt +3 -0
- 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']
|