pvw-cli 1.0.4__py3-none-any.whl → 1.0.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.

@@ -1,278 +0,0 @@
1
- import click
2
- import csv
3
- import json
4
- import tempfile
5
- import os
6
- from rich.console import Console
7
- from purviewcli.client._unified_catalog import UnifiedCatalogDataProduct
8
-
9
- console = Console()
10
-
11
- @click.group()
12
- def data_product():
13
- """Manage data products in Microsoft Purview using Unified Catalog API."""
14
- pass
15
-
16
- @data_product.command()
17
- @click.option('--name', required=True, help="Name of the data product")
18
- @click.option('--description', required=False, help="Description of the data product")
19
- @click.option('--domain-guid', required=False, help="GUID of the domain to associate with")
20
- def create(name, description, domain_guid):
21
- """Create a new data product using Unified Catalog API."""
22
- try:
23
- data_product_client = UnifiedCatalogDataProduct()
24
- result = data_product_client.create_data_product(
25
- name=name,
26
- description=description,
27
- domain_guid=domain_guid
28
- )
29
-
30
- if result.get("status") == "error":
31
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
32
- return
33
-
34
- console.print(f"[green]SUCCESS:[/green] Created data product '{name}'")
35
- console.print(json.dumps(result, indent=2))
36
-
37
- except Exception as e:
38
- console.print(f"[red]ERROR:[/red] {str(e)}")
39
-
40
- @data_product.command()
41
- @click.option('--data-product-id', required=True, help="ID of the data product")
42
- def show(data_product_id):
43
- """Show details of a data product using Unified Catalog API."""
44
- try:
45
- data_product_client = UnifiedCatalogDataProduct()
46
- result = data_product_client.get_data_product(data_product_id)
47
-
48
- if result.get("status") == "error":
49
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
50
- return
51
-
52
- console.print(json.dumps(result, indent=2))
53
- except Exception as e:
54
- console.print(f"[red]ERROR:[/red] {str(e)}")
55
-
56
- @data_product.command()
57
- @click.option('--data-product-id', required=True, help="ID of the data product")
58
- @click.option('--name', required=False, help="New name for the data product")
59
- @click.option('--description', required=False, help="New description for the data product")
60
- def update(data_product_id, name, description):
61
- """Update a data product using Unified Catalog API."""
62
- try:
63
- data_product_client = UnifiedCatalogDataProduct()
64
- updates = {}
65
- if name:
66
- updates['name'] = name
67
- if description:
68
- updates['description'] = description
69
-
70
- if not updates:
71
- console.print("[yellow]WARNING:[/yellow] No updates specified")
72
- return
73
-
74
- result = data_product_client.update_data_product(data_product_id, updates)
75
-
76
- if result.get("status") == "error":
77
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
78
- return
79
-
80
- console.print(f"[green]SUCCESS:[/green] Updated data product '{data_product_id}'")
81
- console.print(json.dumps(result, indent=2))
82
-
83
- except Exception as e:
84
- console.print(f"[red]ERROR:[/red] {str(e)}")
85
-
86
- @data_product.command()
87
- @click.option('--data-product-id', required=True, help="ID of the data product")
88
- def delete(data_product_id):
89
- """Delete a data product using Unified Catalog API."""
90
- try:
91
- data_product_client = UnifiedCatalogDataProduct()
92
- result = data_product_client.delete_data_product(data_product_id)
93
-
94
- if result.get("status") == "error":
95
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
96
- return
97
-
98
- console.print(f"[green]SUCCESS:[/green] Deleted data product '{data_product_id}'")
99
- console.print(json.dumps(result, indent=2))
100
- except Exception as e:
101
- console.print(f"[red]ERROR:[/red] {str(e)}")
102
-
103
- @data_product.command()
104
- @click.option('--limit', default=50, help="Maximum number of data products to list")
105
- @click.option('--skip', default=0, help="Number of data products to skip")
106
- @click.option('--domain-id', required=False, help="Filter by governance domain ID")
107
- def list(limit, skip, domain_id):
108
- """List data products using Unified Catalog API."""
109
- try:
110
- data_product_client = UnifiedCatalogDataProduct()
111
- result = data_product_client.list_data_products(
112
- limit=limit,
113
- offset=skip,
114
- domain_id=domain_id
115
- )
116
-
117
- if result.get("status") == "error":
118
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
119
- return
120
-
121
- console.print(json.dumps(result, indent=2))
122
- except Exception as e:
123
- console.print(f"[red]ERROR:[/red] {str(e)}")
124
-
125
- @data_product.command()
126
- @click.option('--data-product-id', required=True, help="ID of the data product")
127
- def publish(data_product_id):
128
- """Publish a data product using Unified Catalog API."""
129
- try:
130
- data_product_client = UnifiedCatalogDataProduct()
131
- result = data_product_client.update_data_product(data_product_id, {"status": "Published"})
132
-
133
- if result.get("status") == "error":
134
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
135
- return
136
-
137
- console.print(f"[green]SUCCESS:[/green] Published data product '{data_product_id}'")
138
- console.print(json.dumps(result, indent=2))
139
- except Exception as e:
140
- console.print(f"[red]ERROR:[/red] {str(e)}")
141
-
142
- @data_product.command()
143
- @click.option('--data-product-id', required=True, help="ID of the data product")
144
- def unpublish(data_product_id):
145
- """Unpublish a data product using Unified Catalog API."""
146
- try:
147
- data_product_client = UnifiedCatalogDataProduct()
148
- result = data_product_client.update_data_product(data_product_id, {"status": "Draft"})
149
-
150
- if result.get("status") == "error":
151
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
152
- return
153
-
154
- console.print(f"[green]SUCCESS:[/green] Unpublished data product '{data_product_id}'")
155
- console.print(json.dumps(result, indent=2))
156
- except Exception as e:
157
- console.print(f"[red]ERROR:[/red] {str(e)}")
158
-
159
- # === BUSINESS DOMAIN COMMANDS ===
160
- # Business domains are required for data product creation
161
-
162
- @data_product.command()
163
- def list_domains():
164
- """List all business domains."""
165
- try:
166
- data_product_client = UnifiedCatalogDataProduct()
167
- result = data_product_client.businessDomainList({})
168
-
169
- if result.get("status") == "error":
170
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
171
- return
172
-
173
- console.print(json.dumps(result, indent=2))
174
- except Exception as e:
175
- console.print(f"[red]ERROR:[/red] {str(e)}")
176
-
177
- @data_product.command()
178
- @click.option('--name', required=True, help="Name of the business domain")
179
- @click.option('--description', required=False, help="Description of the business domain")
180
- @click.option('--type', default="DataDomain", help="Type of the business domain (DataDomain, LineOfBusiness, etc.)")
181
- def create_domain(name, description, type):
182
- """Create a new business domain."""
183
- try:
184
- data_product_client = UnifiedCatalogDataProduct()
185
- args = {
186
- "--name": name,
187
- "--description": description or "",
188
- "--type": type,
189
- "--status": "Draft"
190
- }
191
- result = data_product_client.businessDomainCreate(args)
192
-
193
- if result.get("status") == "error":
194
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
195
- return
196
-
197
- console.print(f"[green]SUCCESS:[/green] Created business domain '{name}'")
198
- console.print(json.dumps(result, indent=2))
199
- except Exception as e:
200
- console.print(f"[red]ERROR:[/red] {str(e)}")
201
-
202
- @data_product.command()
203
- @click.option('--domain-id', required=True, help="ID of the business domain")
204
- def show_domain(domain_id):
205
- """Show details of a business domain."""
206
- try:
207
- data_product_client = UnifiedCatalogDataProduct()
208
- result = data_product_client.businessDomainRead({"--domainId": domain_id})
209
-
210
- if result.get("status") == "error":
211
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
212
- return
213
-
214
- console.print(json.dumps(result, indent=2))
215
- except Exception as e:
216
- console.print(f"[red]ERROR:[/red] {str(e)}")
217
-
218
- # === GLOSSARY TERM COMMANDS ===
219
-
220
- @data_product.command()
221
- @click.option('--domain-id', required=False, help="Filter by governance domain ID")
222
- def list_terms(domain_id):
223
- """List glossary terms."""
224
- try:
225
- data_product_client = UnifiedCatalogDataProduct()
226
- args = {}
227
- if domain_id:
228
- args["--governanceDomain"] = domain_id
229
- result = data_product_client.glossaryTermsList(args)
230
-
231
- if result.get("status") == "error":
232
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
233
- return
234
-
235
- console.print(json.dumps(result, indent=2))
236
- except Exception as e:
237
- console.print(f"[red]ERROR:[/red] {str(e)}")
238
-
239
- @data_product.command()
240
- @click.option('--name', required=True, help="Name of the glossary term")
241
- @click.option('--description', required=False, help="Description of the glossary term")
242
- @click.option('--domain-id', required=True, help="ID of the governance domain")
243
- def create_term(name, description, domain_id):
244
- """Create a new glossary term."""
245
- try:
246
- data_product_client = UnifiedCatalogDataProduct()
247
- result = data_product_client.create_glossary_term(
248
- name=name,
249
- description=description or "",
250
- domain_id=domain_id
251
- )
252
-
253
- if result.get("status") == "error":
254
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
255
- return
256
-
257
- console.print(f"[green]SUCCESS:[/green] Created glossary term '{name}'")
258
- console.print(json.dumps(result, indent=2))
259
- except Exception as e:
260
- console.print(f"[red]ERROR:[/red] {str(e)}")
261
-
262
- @data_product.command()
263
- @click.option('--data-product-id', required=True, help="ID of the data product")
264
- @click.option('--term-id', required=True, help="ID of the glossary term")
265
- def link_term(data_product_id, term_id):
266
- """Link a glossary term to a data product."""
267
- try:
268
- data_product_client = UnifiedCatalogDataProduct()
269
- result = data_product_client.link_term_to_data_product(data_product_id, term_id)
270
-
271
- if result.get("status") == "error":
272
- console.print(f"[red]ERROR:[/red] {result.get('message', 'Unknown error')}")
273
- return
274
-
275
- console.print(f"[green]SUCCESS:[/green] Linked term '{term_id}' to data product '{data_product_id}'")
276
- console.print(json.dumps(result, indent=2))
277
- except Exception as e:
278
- console.print(f"[red]ERROR:[/red] {str(e)}")
@@ -1,168 +0,0 @@
1
- import json
2
- import tempfile
3
- import os
4
- import csv
5
- from purviewcli.client._entity import Entity
6
-
7
- class DataProduct:
8
- """Client for managing data products in Microsoft Purview."""
9
-
10
- def __init__(self):
11
- self.entity_client = Entity()
12
-
13
- def import_from_csv(self, products):
14
- with open("import_from_csv_debug.log", "a") as logf:
15
- logf.write(f"products type: {type(products)}\n")
16
- if products:
17
- logf.write(f"first product type: {type(products[0])}\n")
18
- logf.write(f"first product: {products[0]}\n")
19
- if not isinstance(products, list):
20
- raise TypeError(f"Expected a list, got: {type(products)} with value: {products}")
21
- if not products or not isinstance(products[0], dict):
22
- raise TypeError(
23
- f"Expected a list of dicts, got: {type(products[0])} with value: {products[0]}"
24
- )
25
- required_fields = ["qualifiedName"]
26
- entities = []
27
- for product in products:
28
- # Validate required fields
29
- for field in required_fields:
30
- if not product.get(field):
31
- raise ValueError(f"Missing required field '{field}' in row: {product}")
32
- # Always use typeName DataSet (Purview default)
33
- attributes = {k: v for k, v in product.items() if k != "typeName"}
34
- entity = {
35
- "typeName": "DataSet",
36
- "attributes": attributes
37
- }
38
- entities.append(entity)
39
- # Write the bulk payload to a temp file (always as {"entities": [...]})
40
- bulk_payload = {"entities": entities}
41
- with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmpf:
42
- json.dump(bulk_payload, tmpf, indent=2)
43
- tmpf.flush()
44
- payload_file = tmpf.name
45
- try:
46
- result = self.entity_client.entityCreateBulk({"--payloadFile": payload_file})
47
- return [(e["attributes"]["qualifiedName"], result) for e in entities]
48
- finally:
49
- os.remove(payload_file)
50
-
51
- def import_from_csv_file(self, csv_file_path, dry_run=False):
52
- """Load data products from a CSV file and import them. If dry_run is True, return the parsed products only."""
53
- with open(csv_file_path, 'r', encoding='utf-8') as f:
54
- reader = csv.DictReader(f)
55
- products = list(reader)
56
- if dry_run:
57
- return products
58
- return self.import_from_csv(products)
59
-
60
- def create(self, qualified_name, name=None, description=None, type_name="DataProduct"):
61
- """Create a single data product entity."""
62
- payload = {
63
- "typeName": type_name,
64
- "attributes": {
65
- "qualifiedName": qualified_name,
66
- "name": name or qualified_name,
67
- "description": description or ""
68
- },
69
- }
70
- # Write payload to temp file since entity client expects a file path
71
- with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmpf:
72
- json.dump(payload, tmpf, indent=2)
73
- tmpf.flush()
74
- payload_file = tmpf.name
75
-
76
- try:
77
- return self.entity_client.entityCreate({"--payloadFile": payload_file})
78
- finally:
79
- os.remove(payload_file)
80
-
81
- def list(self, type_name="DataProduct"):
82
- """List all data products (by typeName)."""
83
- # Use the entity client's search method to find all entities of type DataProduct
84
- search_args = {
85
- "keywords": "*", # match all
86
- "filter": {
87
- "entityType": [type_name]
88
- },
89
- "limit": 100
90
- }
91
- # If the entity client has a search method, use it; otherwise, return empty list
92
- if hasattr(self.entity_client, "search_entities"):
93
- return self.entity_client.search_entities(search_args)
94
- # No suitable fallback for listing all entities by type; return empty list
95
- return []
96
-
97
- def show(self, qualified_name, type_name="DataProduct"):
98
- """Show a data product by qualifiedName."""
99
- args = {"--typeName": type_name, "--qualifiedName": qualified_name}
100
- return self.entity_client.entityReadUniqueAttribute(args)
101
-
102
- def delete(self, qualified_name, type_name="DataProduct"):
103
- """Delete a data product by qualifiedName."""
104
- args = {"--typeName": type_name, "--qualifiedName": qualified_name}
105
- return self.entity_client.entityDeleteUniqueAttribute(args)
106
-
107
- def add_classification(self, qualified_name, classification, type_name="DataProduct"):
108
- """Add a classification to a data product."""
109
- args = {
110
- "--typeName": type_name,
111
- "--qualifiedName": qualified_name,
112
- "--payloadFile": {"classifications": [classification]}
113
- }
114
- return self.entity_client.entityAddClassificationsByUniqueAttribute(args)
115
-
116
- def remove_classification(self, qualified_name, classification, type_name="DataProduct"):
117
- """Remove a classification from a data product."""
118
- args = {
119
- "--typeName": type_name,
120
- "--qualifiedName": qualified_name,
121
- "--classificationName": classification
122
- }
123
- return self.entity_client.entityDeleteClassificationByUniqueAttribute(args)
124
-
125
- def add_label(self, qualified_name, label, type_name="DataProduct"):
126
- """Add a label to a data product."""
127
- args = {
128
- "--typeName": type_name,
129
- "--qualifiedName": qualified_name,
130
- "--payloadFile": {"labels": [label]}
131
- }
132
- return self.entity_client.entityAddLabelsByUniqueAttribute(args)
133
-
134
- def remove_label(self, qualified_name, label, type_name="DataProduct"):
135
- """Remove a label from a data product."""
136
- args = {
137
- "--typeName": type_name,
138
- "--qualifiedName": qualified_name,
139
- "--payloadFile": {"labels": [label]}
140
- }
141
- return self.entity_client.entityRemoveLabelsByUniqueAttribute(args)
142
-
143
- def link_glossary(self, qualified_name, term, type_name="DataProduct"):
144
- """Link a glossary term to a data product."""
145
- # This assumes business metadata or a custom attribute for glossary terms
146
- # You may need to adjust this to your Purview model
147
- args = {
148
- "--typeName": type_name,
149
- "--qualifiedName": qualified_name,
150
- "--payloadFile": {"meanings": [term]}
151
- }
152
- return self.entity_client.entityPartialUpdateByUniqueAttribute(args)
153
-
154
- def show_lineage(self, qualified_name, type_name="DataProduct"):
155
- """Show lineage for a data product."""
156
- args = {"--typeName": type_name, "--qualifiedName": qualified_name}
157
- # This assumes you have a lineage client or can call entityReadUniqueAttribute and extract lineage
158
- # For now, just return the entity details
159
- return self.entity_client.entityReadUniqueAttribute(args)
160
-
161
- def set_status(self, qualified_name, status, type_name="DataProduct"):
162
- """Set the status of a data product (e.g., active, deprecated)."""
163
- args = {
164
- "--typeName": type_name,
165
- "--qualifiedName": qualified_name,
166
- "--payloadFile": {"status": status}
167
- }
168
- return self.entity_client.entityPartialUpdateByUniqueAttribute(args)