datacontract-cli 0.10.32__py3-none-any.whl → 0.10.33__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 datacontract-cli might be problematic. Click here for more details.
- datacontract/cli.py +20 -5
- datacontract/data_contract.py +8 -2
- datacontract/export/excel_exporter.py +922 -0
- datacontract/export/exporter.py +1 -0
- datacontract/export/exporter_factory.py +4 -0
- datacontract/imports/excel_importer.py +13 -5
- {datacontract_cli-0.10.32.dist-info → datacontract_cli-0.10.33.dist-info}/METADATA +28 -4
- {datacontract_cli-0.10.32.dist-info → datacontract_cli-0.10.33.dist-info}/RECORD +12 -11
- {datacontract_cli-0.10.32.dist-info → datacontract_cli-0.10.33.dist-info}/WHEEL +0 -0
- {datacontract_cli-0.10.32.dist-info → datacontract_cli-0.10.33.dist-info}/entry_points.txt +0 -0
- {datacontract_cli-0.10.32.dist-info → datacontract_cli-0.10.33.dist-info}/licenses/LICENSE +0 -0
- {datacontract_cli-0.10.32.dist-info → datacontract_cli-0.10.33.dist-info}/top_level.txt +0 -0
datacontract/cli.py
CHANGED
|
@@ -210,12 +210,21 @@ def export(
|
|
|
210
210
|
# TODO: this should be a subcommand
|
|
211
211
|
template: Annotated[
|
|
212
212
|
Optional[Path],
|
|
213
|
-
typer.Option(
|
|
213
|
+
typer.Option(
|
|
214
|
+
help="The file path or URL of a template. For Excel format: path/URL to custom Excel template. For custom format: path to Jinja template."
|
|
215
|
+
),
|
|
214
216
|
] = None,
|
|
215
217
|
):
|
|
216
218
|
"""
|
|
217
219
|
Convert data contract to a specific format. Saves to file specified by `output` option if present, otherwise prints to stdout.
|
|
218
220
|
"""
|
|
221
|
+
# Validate that Excel format requires an output file path
|
|
222
|
+
if format == ExportFormat.excel and output is None:
|
|
223
|
+
console.print("❌ Error: Excel export requires an output file path.")
|
|
224
|
+
console.print("💡 Hint: Use --output to specify where to save the Excel file, e.g.:")
|
|
225
|
+
console.print(" datacontract export --format excel --output datacontract.xlsx")
|
|
226
|
+
raise typer.Exit(code=1)
|
|
227
|
+
|
|
219
228
|
# TODO exception handling
|
|
220
229
|
result = DataContract(data_contract_file=location, schema_location=schema, server=server).export(
|
|
221
230
|
export_format=format,
|
|
@@ -230,8 +239,13 @@ def export(
|
|
|
230
239
|
if output is None:
|
|
231
240
|
console.print(result, markup=False, soft_wrap=True)
|
|
232
241
|
else:
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
if isinstance(result, bytes):
|
|
243
|
+
# If the result is bytes, we assume it's a binary file (e.g., Excel, PDF)
|
|
244
|
+
with output.open(mode="wb") as f:
|
|
245
|
+
f.write(result)
|
|
246
|
+
else:
|
|
247
|
+
with output.open(mode="w", encoding="utf-8") as f:
|
|
248
|
+
f.write(result)
|
|
235
249
|
console.print(f"Written result to {output}")
|
|
236
250
|
|
|
237
251
|
|
|
@@ -482,13 +496,14 @@ def _get_uvicorn_arguments(port: int, host: str, context: typer.Context) -> dict
|
|
|
482
496
|
}
|
|
483
497
|
|
|
484
498
|
# Create a list of the extra arguments, remove the leading -- from the cli arguments
|
|
485
|
-
trimmed_keys = list(map(lambda x
|
|
499
|
+
trimmed_keys = list(map(lambda x: str(x).replace("--", ""), context.args[::2]))
|
|
486
500
|
# Merge the two dicts and return them as one dict
|
|
487
501
|
return default_args | dict(zip(trimmed_keys, context.args[1::2]))
|
|
488
502
|
|
|
503
|
+
|
|
489
504
|
@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
|
490
505
|
def api(
|
|
491
|
-
ctx: Annotated[typer.Context, typer.Option(help="Extra arguments to pass to uvicorn.run().")],
|
|
506
|
+
ctx: Annotated[typer.Context, typer.Option(help="Extra arguments to pass to uvicorn.run().")],
|
|
492
507
|
port: Annotated[int, typer.Option(help="Bind socket to this port.")] = 4242,
|
|
493
508
|
host: Annotated[
|
|
494
509
|
str, typer.Option(help="Bind socket to this host. Hint: For running in docker, set it to 0.0.0.0")
|
datacontract/data_contract.py
CHANGED
|
@@ -250,8 +250,14 @@ class DataContract:
|
|
|
250
250
|
inline_quality=self._inline_quality,
|
|
251
251
|
)
|
|
252
252
|
|
|
253
|
-
def export(
|
|
254
|
-
|
|
253
|
+
def export(
|
|
254
|
+
self, export_format: ExportFormat, model: str = "all", sql_server_type: str = "auto", **kwargs
|
|
255
|
+
) -> str | bytes:
|
|
256
|
+
if (
|
|
257
|
+
export_format == ExportFormat.html
|
|
258
|
+
or export_format == ExportFormat.mermaid
|
|
259
|
+
or export_format == ExportFormat.excel
|
|
260
|
+
):
|
|
255
261
|
data_contract = resolve.resolve_data_contract_v2(
|
|
256
262
|
self._data_contract_file,
|
|
257
263
|
self._data_contract_str,
|
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import logging
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
import openpyxl
|
|
7
|
+
import requests
|
|
8
|
+
from open_data_contract_standard.model import (
|
|
9
|
+
DataQuality,
|
|
10
|
+
OpenDataContractStandard,
|
|
11
|
+
SchemaObject,
|
|
12
|
+
SchemaProperty,
|
|
13
|
+
)
|
|
14
|
+
from openpyxl.cell.cell import Cell
|
|
15
|
+
from openpyxl.workbook.defined_name import DefinedName
|
|
16
|
+
from openpyxl.workbook.workbook import Workbook
|
|
17
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
18
|
+
|
|
19
|
+
from datacontract.export.exporter import Exporter
|
|
20
|
+
from datacontract.model.data_contract_specification import DataContractSpecification
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
ODCS_EXCEL_TEMPLATE_URL = (
|
|
25
|
+
"https://github.com/datacontract/open-data-contract-standard-excel-template/raw/refs/heads/main/odcs-template.xlsx"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExcelExporter(Exporter):
|
|
30
|
+
"""Excel exporter that uses the official ODCS template"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, export_format):
|
|
33
|
+
super().__init__(export_format)
|
|
34
|
+
|
|
35
|
+
def export(self, data_contract, model, server, sql_server_type, export_args) -> bytes:
|
|
36
|
+
"""
|
|
37
|
+
Export data contract to Excel using the official ODCS template
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data_contract: DataContractSpecification or OpenDataContractStandard to export
|
|
41
|
+
model: Model name (not used for Excel export)
|
|
42
|
+
server: Server name (not used for Excel export)
|
|
43
|
+
sql_server_type: SQL server type (not used for Excel export)
|
|
44
|
+
export_args: Additional export arguments (template can be specified here)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Excel file as bytes
|
|
48
|
+
"""
|
|
49
|
+
# Convert to ODCS if needed
|
|
50
|
+
if isinstance(data_contract, DataContractSpecification):
|
|
51
|
+
# First convert DCS to ODCS format via YAML
|
|
52
|
+
yaml_content = data_contract.to_yaml()
|
|
53
|
+
odcs = OpenDataContractStandard.from_string(yaml_content)
|
|
54
|
+
else:
|
|
55
|
+
odcs = data_contract
|
|
56
|
+
|
|
57
|
+
# Get template from export_args if provided, otherwise use default
|
|
58
|
+
template = export_args.get("template") if export_args else None
|
|
59
|
+
return export_to_excel_bytes(odcs, template)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def export_to_excel_bytes(odcs: OpenDataContractStandard, template_path: Optional[str] = None) -> bytes:
|
|
63
|
+
"""
|
|
64
|
+
Export ODCS to Excel format using the official template and return as bytes
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
odcs: OpenDataContractStandard object to export
|
|
68
|
+
template_path: Optional path/URL to custom Excel template. If None, uses default template.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Excel file as bytes
|
|
72
|
+
"""
|
|
73
|
+
if template_path:
|
|
74
|
+
workbook = create_workbook_from_template(template_path)
|
|
75
|
+
else:
|
|
76
|
+
workbook = create_workbook_from_template(ODCS_EXCEL_TEMPLATE_URL)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
fill_fundamentals(workbook, odcs)
|
|
80
|
+
fill_schema(workbook, odcs)
|
|
81
|
+
fill_quality(workbook, odcs)
|
|
82
|
+
fill_custom_properties(workbook, odcs)
|
|
83
|
+
fill_support(workbook, odcs)
|
|
84
|
+
fill_team(workbook, odcs)
|
|
85
|
+
fill_roles(workbook, odcs)
|
|
86
|
+
fill_sla_properties(workbook, odcs)
|
|
87
|
+
fill_servers(workbook, odcs)
|
|
88
|
+
fill_pricing(workbook, odcs)
|
|
89
|
+
|
|
90
|
+
# Set focus on the Fundamentals sheet
|
|
91
|
+
workbook.active = workbook["Fundamentals"]
|
|
92
|
+
|
|
93
|
+
# Force formula recalculation
|
|
94
|
+
try:
|
|
95
|
+
workbook.calculation.calcMode = "auto"
|
|
96
|
+
except (AttributeError, ValueError):
|
|
97
|
+
# Fallback for older openpyxl versions or if calcMode doesn't exist
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# Write to output stream
|
|
101
|
+
output = io.BytesIO()
|
|
102
|
+
workbook.save(output)
|
|
103
|
+
output.seek(0)
|
|
104
|
+
return output.getvalue()
|
|
105
|
+
finally:
|
|
106
|
+
workbook.close()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def create_workbook_from_template(template_path: str) -> Workbook:
|
|
110
|
+
"""Load Excel template from file path or URL"""
|
|
111
|
+
try:
|
|
112
|
+
# Convert Path object to string if needed
|
|
113
|
+
template_path_str = str(template_path)
|
|
114
|
+
logger.info(f"Processing template path: {template_path_str}")
|
|
115
|
+
|
|
116
|
+
# Check if it's a URL
|
|
117
|
+
if template_path_str.startswith(("http://", "https://")):
|
|
118
|
+
logger.info(f"Identified as URL, downloading from: {template_path_str}")
|
|
119
|
+
# Download from URL
|
|
120
|
+
response = requests.get(template_path_str, timeout=30)
|
|
121
|
+
response.raise_for_status()
|
|
122
|
+
template_bytes = response.content
|
|
123
|
+
workbook = openpyxl.load_workbook(io.BytesIO(template_bytes))
|
|
124
|
+
else:
|
|
125
|
+
logger.info(f"Identified as local file: {template_path_str}")
|
|
126
|
+
# Load from local file
|
|
127
|
+
workbook = openpyxl.load_workbook(template_path_str)
|
|
128
|
+
|
|
129
|
+
return workbook
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Failed to load Excel template from {template_path}: {e}")
|
|
132
|
+
raise RuntimeError(f"Failed to load Excel template: {e}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def fill_fundamentals(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
136
|
+
"""Fill the Fundamentals sheet with basic contract information"""
|
|
137
|
+
set_cell_value_by_name(workbook, "apiVersion", odcs.apiVersion)
|
|
138
|
+
set_cell_value_by_name(workbook, "kind", odcs.kind)
|
|
139
|
+
set_cell_value_by_name(workbook, "id", odcs.id)
|
|
140
|
+
set_cell_value_by_name(workbook, "name", odcs.name)
|
|
141
|
+
set_cell_value_by_name(workbook, "version", odcs.version)
|
|
142
|
+
set_cell_value_by_name(workbook, "status", odcs.status)
|
|
143
|
+
set_cell_value_by_name(workbook, "domain", odcs.domain)
|
|
144
|
+
set_cell_value_by_name(workbook, "dataProduct", odcs.dataProduct)
|
|
145
|
+
set_cell_value_by_name(workbook, "tenant", odcs.tenant)
|
|
146
|
+
|
|
147
|
+
# Set owner from custom properties
|
|
148
|
+
owner_value = None
|
|
149
|
+
if odcs.customProperties:
|
|
150
|
+
for prop in odcs.customProperties:
|
|
151
|
+
if prop.property == "owner":
|
|
152
|
+
owner_value = prop.value
|
|
153
|
+
break
|
|
154
|
+
set_cell_value_by_name(workbook, "owner", owner_value)
|
|
155
|
+
|
|
156
|
+
set_cell_value_by_name(workbook, "slaDefaultElement", odcs.slaDefaultElement)
|
|
157
|
+
|
|
158
|
+
# Set description fields
|
|
159
|
+
if odcs.description:
|
|
160
|
+
set_cell_value_by_name(workbook, "description.purpose", odcs.description.purpose)
|
|
161
|
+
set_cell_value_by_name(workbook, "description.limitations", odcs.description.limitations)
|
|
162
|
+
set_cell_value_by_name(workbook, "description.usage", odcs.description.usage)
|
|
163
|
+
|
|
164
|
+
# Set tags as comma-separated string
|
|
165
|
+
if odcs.tags:
|
|
166
|
+
set_cell_value_by_name(workbook, "tags", ",".join(odcs.tags))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fill_pricing(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
170
|
+
"""Fill pricing information"""
|
|
171
|
+
if odcs.price:
|
|
172
|
+
set_cell_value_by_name(workbook, "price.priceAmount", odcs.price.priceAmount)
|
|
173
|
+
set_cell_value_by_name(workbook, "price.priceCurrency", odcs.price.priceCurrency)
|
|
174
|
+
set_cell_value_by_name(workbook, "price.priceUnit", odcs.price.priceUnit)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def fill_schema(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
178
|
+
"""Fill schema information by cloning template sheets"""
|
|
179
|
+
# Get template sheet "Schema <table_name>"
|
|
180
|
+
schema_template_sheet = workbook["Schema <table_name>"]
|
|
181
|
+
|
|
182
|
+
if odcs.schema_:
|
|
183
|
+
# Create copies for all schemas first
|
|
184
|
+
new_sheets = []
|
|
185
|
+
for schema in odcs.schema_:
|
|
186
|
+
# Clone the template sheet
|
|
187
|
+
new_sheet = workbook.copy_worksheet(schema_template_sheet)
|
|
188
|
+
new_sheet.title = f"Schema {schema.name}"
|
|
189
|
+
|
|
190
|
+
# Copy defined names with schema sheet scope to the new sheet
|
|
191
|
+
copy_sheet_names(workbook, schema_template_sheet, new_sheet)
|
|
192
|
+
|
|
193
|
+
# Move the new sheet before the template sheet
|
|
194
|
+
schema_template_sheet_index = workbook.index(schema_template_sheet)
|
|
195
|
+
new_sheet_index = workbook.index(new_sheet)
|
|
196
|
+
|
|
197
|
+
workbook.move_sheet(new_sheet, offset=schema_template_sheet_index - new_sheet_index)
|
|
198
|
+
|
|
199
|
+
new_sheets.append((new_sheet, schema))
|
|
200
|
+
|
|
201
|
+
# Remove the template sheet before filling
|
|
202
|
+
workbook.remove(schema_template_sheet)
|
|
203
|
+
|
|
204
|
+
# Now fill in schema information for each copied sheet
|
|
205
|
+
for new_sheet, schema in new_sheets:
|
|
206
|
+
# Copy named ranges from template to new sheet (if needed)
|
|
207
|
+
# Note: copy_worksheet should have copied the named ranges already
|
|
208
|
+
|
|
209
|
+
# Fill in schema information
|
|
210
|
+
fill_single_schema(new_sheet, schema)
|
|
211
|
+
else:
|
|
212
|
+
# Remove the template sheet even if no schemas
|
|
213
|
+
workbook.remove(schema_template_sheet)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def copy_sheet_names(workbook: Workbook, template_sheet: Worksheet, new_sheet: Worksheet):
|
|
217
|
+
"""Copy worksheet-scoped named ranges from template sheet to new sheet"""
|
|
218
|
+
try:
|
|
219
|
+
# Copy worksheet-scoped defined names from template sheet to new sheet
|
|
220
|
+
for name_str in template_sheet.defined_names:
|
|
221
|
+
try:
|
|
222
|
+
# Get the DefinedName object
|
|
223
|
+
defined_name = template_sheet.defined_names[name_str]
|
|
224
|
+
|
|
225
|
+
# Get the original range reference
|
|
226
|
+
original_ref = defined_name.attr_text
|
|
227
|
+
|
|
228
|
+
# Create new defined name with same name and reference but scoped to new sheet
|
|
229
|
+
new_name = DefinedName(name_str, attr_text=original_ref.replace(template_sheet.title, new_sheet.title))
|
|
230
|
+
|
|
231
|
+
# Add to the new sheet's defined names (worksheet-scoped)
|
|
232
|
+
new_sheet.defined_names.add(new_name)
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.warning(f"Failed to copy worksheet-scoped named range {name_str}: {e}")
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.warning(f"Error copying sheet names: {e}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def fill_single_schema(sheet: Worksheet, schema: SchemaObject):
|
|
242
|
+
"""Fill a single schema sheet with schema information using named ranges"""
|
|
243
|
+
# Use worksheet-scoped named ranges that were copied from the template
|
|
244
|
+
set_cell_value_by_name_in_sheet(sheet, "schema.name", schema.name)
|
|
245
|
+
set_cell_value_by_name_in_sheet(
|
|
246
|
+
sheet, "schema.physicalType", schema.physicalType if schema.physicalType else "table"
|
|
247
|
+
)
|
|
248
|
+
set_cell_value_by_name_in_sheet(sheet, "schema.description", schema.description)
|
|
249
|
+
set_cell_value_by_name_in_sheet(sheet, "schema.businessName", schema.businessName)
|
|
250
|
+
set_cell_value_by_name_in_sheet(sheet, "schema.physicalName", schema.physicalName)
|
|
251
|
+
set_cell_value_by_name_in_sheet(sheet, "schema.dataGranularityDescription", schema.dataGranularityDescription)
|
|
252
|
+
|
|
253
|
+
# Set tags as comma-separated string
|
|
254
|
+
if schema.tags:
|
|
255
|
+
set_cell_value_by_name_in_sheet(sheet, "schema.tags", ",".join(schema.tags))
|
|
256
|
+
|
|
257
|
+
# Fill properties using the template's properties table structure
|
|
258
|
+
if schema.properties:
|
|
259
|
+
fill_properties_in_schema_sheet(sheet, schema.properties)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def fill_properties_in_schema_sheet(sheet: Worksheet, properties: List[SchemaProperty], prefix: str = ""):
|
|
263
|
+
"""Fill properties in the schema sheet using the template's existing properties table"""
|
|
264
|
+
try:
|
|
265
|
+
# The template already has a properties table starting at row 13 with headers
|
|
266
|
+
# Find the header row and map column names to indices
|
|
267
|
+
header_row_index = 13
|
|
268
|
+
headers = get_headers_from_header_row(sheet, header_row_index)
|
|
269
|
+
|
|
270
|
+
# Reverse the headers dict to map header_name -> column_index
|
|
271
|
+
header_map = {header_name.lower(): col_idx for col_idx, header_name in headers.items()}
|
|
272
|
+
|
|
273
|
+
# Fill properties starting after header row
|
|
274
|
+
row_index = header_row_index + 1
|
|
275
|
+
for property in properties:
|
|
276
|
+
row_index = fill_single_property_template(sheet, row_index, prefix, property, header_map)
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"Error filling properties: {e}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def fill_single_property_template(
|
|
283
|
+
sheet: Worksheet, row_index: int, prefix: str, property: SchemaProperty, header_map: dict
|
|
284
|
+
) -> int:
|
|
285
|
+
"""Fill a single property row using the template's column structure"""
|
|
286
|
+
property_name = f"{prefix}.{property.name}" if prefix else property.name
|
|
287
|
+
|
|
288
|
+
# Helper function to set cell value by header name
|
|
289
|
+
def set_by_header(header_name: str, value: Any):
|
|
290
|
+
col_idx = header_map.get(header_name.lower())
|
|
291
|
+
if col_idx is not None:
|
|
292
|
+
sheet.cell(row=row_index, column=col_idx + 1).value = value
|
|
293
|
+
|
|
294
|
+
# Fill property fields based on template headers
|
|
295
|
+
set_by_header("Property", property_name)
|
|
296
|
+
set_by_header("Business Name", property.businessName)
|
|
297
|
+
set_by_header("Logical Type", property.logicalType)
|
|
298
|
+
set_by_header("Physical Type", property.physicalType)
|
|
299
|
+
set_by_header("Physical Name", property.physicalName)
|
|
300
|
+
set_by_header("Description", property.description)
|
|
301
|
+
set_by_header("Required", property.required)
|
|
302
|
+
set_by_header("Unique", property.unique)
|
|
303
|
+
set_by_header("Primary Key", property.primaryKey)
|
|
304
|
+
set_by_header("Primary Key Position", property.primaryKeyPosition)
|
|
305
|
+
set_by_header("Partitioned", property.partitioned)
|
|
306
|
+
set_by_header("Partition Key Position", property.partitionKeyPosition)
|
|
307
|
+
set_by_header("Classification", property.classification)
|
|
308
|
+
set_by_header("Tags", ",".join(property.tags) if property.tags else "")
|
|
309
|
+
set_by_header(
|
|
310
|
+
"Example(s)", ",".join(property.examples) if property.examples else ""
|
|
311
|
+
) # Note: using "Example(s)" as in template
|
|
312
|
+
set_by_header("Encrypted Name", property.encryptedName)
|
|
313
|
+
set_by_header(
|
|
314
|
+
"Transform Sources", ",".join(property.transformSourceObjects) if property.transformSourceObjects else ""
|
|
315
|
+
)
|
|
316
|
+
set_by_header("Transform Logic", property.transformLogic)
|
|
317
|
+
|
|
318
|
+
# Authoritative definitions
|
|
319
|
+
if property.authoritativeDefinitions and len(property.authoritativeDefinitions) > 0:
|
|
320
|
+
set_by_header("Authoritative Definition URL", property.authoritativeDefinitions[0].url)
|
|
321
|
+
set_by_header("Authoritative Definition Type", property.authoritativeDefinitions[0].type)
|
|
322
|
+
|
|
323
|
+
next_row_index = row_index + 1
|
|
324
|
+
|
|
325
|
+
# Handle nested properties
|
|
326
|
+
if property.properties:
|
|
327
|
+
for nested_property in property.properties:
|
|
328
|
+
next_row_index = fill_single_property_template(
|
|
329
|
+
sheet, next_row_index, property_name, nested_property, header_map
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Handle array items
|
|
333
|
+
if property.items:
|
|
334
|
+
next_row_index = fill_single_property_template(
|
|
335
|
+
sheet, next_row_index, f"{property_name}.items", property.items, header_map
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return next_row_index
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def fill_single_property_simple(
|
|
342
|
+
sheet: Worksheet, row_index: int, prefix: str, property: SchemaProperty, header_map: dict = None
|
|
343
|
+
) -> int:
|
|
344
|
+
"""Fill a single property row using header names (deprecated - use fill_single_property_template instead)"""
|
|
345
|
+
# This function is kept for backward compatibility but should use header_map if provided
|
|
346
|
+
if header_map is None:
|
|
347
|
+
# Fallback to the template-based approach
|
|
348
|
+
header_row_index = 13
|
|
349
|
+
headers = get_headers_from_header_row(sheet, header_row_index)
|
|
350
|
+
header_map = {header_name.lower(): col_idx for col_idx, header_name in headers.items()}
|
|
351
|
+
|
|
352
|
+
# Delegate to the template-based function
|
|
353
|
+
return fill_single_property_template(sheet, row_index, prefix, property, header_map)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def fill_quality(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
357
|
+
"""Fill the Quality sheet with quality data"""
|
|
358
|
+
quality_sheet = workbook["Quality"]
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
ref = name_to_ref(workbook, "quality")
|
|
362
|
+
if not ref:
|
|
363
|
+
logger.warning("No quality range found")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Parse range to find header row
|
|
367
|
+
header_row_index = parse_range_safely(ref)
|
|
368
|
+
|
|
369
|
+
headers = get_headers_from_header_row(quality_sheet, header_row_index)
|
|
370
|
+
current_row_index = header_row_index + 1
|
|
371
|
+
|
|
372
|
+
# Iterate through all schemas
|
|
373
|
+
if odcs.schema_:
|
|
374
|
+
for schema in odcs.schema_:
|
|
375
|
+
# Add schema-level quality attributes
|
|
376
|
+
if schema.quality:
|
|
377
|
+
for quality in schema.quality:
|
|
378
|
+
row = get_or_create_row(quality_sheet, current_row_index)
|
|
379
|
+
fill_quality_row(row, headers, schema.name, None, quality)
|
|
380
|
+
current_row_index += 1
|
|
381
|
+
|
|
382
|
+
# Add property-level quality attributes
|
|
383
|
+
if schema.properties:
|
|
384
|
+
current_row_index = fill_properties_quality(
|
|
385
|
+
quality_sheet, headers, schema.name, schema.properties, current_row_index
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
except Exception as e:
|
|
389
|
+
logger.warning(f"Error filling quality: {e}")
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def fill_properties_quality(
|
|
393
|
+
sheet: Worksheet,
|
|
394
|
+
headers: dict,
|
|
395
|
+
schema_name: str,
|
|
396
|
+
properties: List[SchemaProperty],
|
|
397
|
+
start_row_index: int,
|
|
398
|
+
prefix: str = "",
|
|
399
|
+
) -> int:
|
|
400
|
+
"""Recursively fill quality data for properties"""
|
|
401
|
+
current_row_index = start_row_index
|
|
402
|
+
|
|
403
|
+
for property in properties:
|
|
404
|
+
if not property.name:
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
full_property_name = f"{prefix}.{property.name}" if prefix else property.name
|
|
408
|
+
|
|
409
|
+
# Add quality attributes for this property
|
|
410
|
+
if property.quality:
|
|
411
|
+
for quality in property.quality:
|
|
412
|
+
row = get_or_create_row(sheet, current_row_index)
|
|
413
|
+
fill_quality_row(row, headers, schema_name, full_property_name, quality)
|
|
414
|
+
current_row_index += 1
|
|
415
|
+
|
|
416
|
+
# Recursively handle nested properties
|
|
417
|
+
if property.properties:
|
|
418
|
+
current_row_index = fill_properties_quality(
|
|
419
|
+
sheet, headers, schema_name, property.properties, current_row_index, full_property_name
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# Handle array items
|
|
423
|
+
if property.items:
|
|
424
|
+
items_property_name = f"{full_property_name}.items"
|
|
425
|
+
if property.items.quality:
|
|
426
|
+
for quality in property.items.quality:
|
|
427
|
+
row = get_or_create_row(sheet, current_row_index)
|
|
428
|
+
fill_quality_row(row, headers, schema_name, items_property_name, quality)
|
|
429
|
+
current_row_index += 1
|
|
430
|
+
|
|
431
|
+
# Handle nested properties in array items
|
|
432
|
+
if property.items.properties:
|
|
433
|
+
current_row_index = fill_properties_quality(
|
|
434
|
+
sheet, headers, schema_name, property.items.properties, current_row_index, items_property_name
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return current_row_index
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def fill_quality_row(row, headers: dict, schema_name: str, property_name: Optional[str], quality: DataQuality):
|
|
441
|
+
"""Fill a single quality row"""
|
|
442
|
+
for cell_index, header_name in headers.items():
|
|
443
|
+
header_lower = header_name.lower().strip()
|
|
444
|
+
|
|
445
|
+
if header_lower == "schema":
|
|
446
|
+
set_cell_value(row, cell_index, schema_name)
|
|
447
|
+
elif header_lower == "property":
|
|
448
|
+
set_cell_value(row, cell_index, property_name)
|
|
449
|
+
elif header_lower == "quality type":
|
|
450
|
+
set_cell_value(row, cell_index, quality.type)
|
|
451
|
+
elif header_lower == "description":
|
|
452
|
+
set_cell_value(row, cell_index, quality.description)
|
|
453
|
+
elif header_lower == "rule (library)":
|
|
454
|
+
set_cell_value(row, cell_index, quality.rule)
|
|
455
|
+
elif header_lower == "query (sql)":
|
|
456
|
+
set_cell_value(row, cell_index, quality.query)
|
|
457
|
+
elif header_lower == "threshold operator":
|
|
458
|
+
operator = get_threshold_operator(quality)
|
|
459
|
+
set_cell_value(row, cell_index, operator)
|
|
460
|
+
elif header_lower == "threshold value":
|
|
461
|
+
value = get_threshold_value(quality)
|
|
462
|
+
set_cell_value(row, cell_index, value)
|
|
463
|
+
elif header_lower == "quality engine (custom)":
|
|
464
|
+
set_cell_value(row, cell_index, quality.engine)
|
|
465
|
+
elif header_lower == "implementation (custom)":
|
|
466
|
+
set_cell_value(row, cell_index, quality.implementation)
|
|
467
|
+
elif header_lower == "severity":
|
|
468
|
+
set_cell_value(row, cell_index, quality.severity)
|
|
469
|
+
elif header_lower == "scheduler":
|
|
470
|
+
set_cell_value(row, cell_index, quality.scheduler)
|
|
471
|
+
elif header_lower == "schedule":
|
|
472
|
+
set_cell_value(row, cell_index, quality.schedule)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def get_threshold_operator(quality: DataQuality) -> Optional[str]:
|
|
476
|
+
"""Get the threshold operator from quality object"""
|
|
477
|
+
if hasattr(quality, "mustBe") and quality.mustBe is not None:
|
|
478
|
+
return "mustBe"
|
|
479
|
+
elif hasattr(quality, "mustNotBe") and quality.mustNotBe is not None:
|
|
480
|
+
return "mustNotBe"
|
|
481
|
+
elif hasattr(quality, "mustBeGreaterThan") and quality.mustBeGreaterThan is not None:
|
|
482
|
+
return "mustBeGreaterThan"
|
|
483
|
+
elif hasattr(quality, "mustBeGreaterThanOrEqualTo") and quality.mustBeGreaterThanOrEqualTo is not None:
|
|
484
|
+
return "mustBeGreaterThanOrEqualTo"
|
|
485
|
+
elif hasattr(quality, "mustBeLessThan") and quality.mustBeLessThan is not None:
|
|
486
|
+
return "mustBeLessThan"
|
|
487
|
+
elif hasattr(quality, "mustBeLessThanOrEqualTo") and quality.mustBeLessThanOrEqualTo is not None:
|
|
488
|
+
return "mustBeLessThanOrEqualTo"
|
|
489
|
+
elif hasattr(quality, "mustBeBetween") and quality.mustBeBetween is not None:
|
|
490
|
+
return "mustBeBetween"
|
|
491
|
+
elif hasattr(quality, "mustNotBeBetween") and quality.mustNotBeBetween is not None:
|
|
492
|
+
return "mustNotBeBetween"
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def get_threshold_value(quality: DataQuality) -> Optional[str]:
|
|
497
|
+
"""Get the threshold value from quality object"""
|
|
498
|
+
if hasattr(quality, "mustBe") and quality.mustBe is not None:
|
|
499
|
+
return str(quality.mustBe)
|
|
500
|
+
elif hasattr(quality, "mustNotBe") and quality.mustNotBe is not None:
|
|
501
|
+
return str(quality.mustNotBe)
|
|
502
|
+
elif hasattr(quality, "mustBeGreaterThan") and quality.mustBeGreaterThan is not None:
|
|
503
|
+
return str(quality.mustBeGreaterThan)
|
|
504
|
+
elif hasattr(quality, "mustBeGreaterThanOrEqualTo") and quality.mustBeGreaterThanOrEqualTo is not None:
|
|
505
|
+
return str(quality.mustBeGreaterThanOrEqualTo)
|
|
506
|
+
elif hasattr(quality, "mustBeLessThan") and quality.mustBeLessThan is not None:
|
|
507
|
+
return str(quality.mustBeLessThan)
|
|
508
|
+
elif hasattr(quality, "mustBeLessThanOrEqualTo") and quality.mustBeLessThanOrEqualTo is not None:
|
|
509
|
+
return str(quality.mustBeLessThanOrEqualTo)
|
|
510
|
+
elif hasattr(quality, "mustBeBetween") and quality.mustBeBetween is not None and len(quality.mustBeBetween) >= 2:
|
|
511
|
+
return f"[{quality.mustBeBetween[0]}, {quality.mustBeBetween[1]}]"
|
|
512
|
+
elif (
|
|
513
|
+
hasattr(quality, "mustNotBeBetween")
|
|
514
|
+
and quality.mustNotBeBetween is not None
|
|
515
|
+
and len(quality.mustNotBeBetween) >= 2
|
|
516
|
+
):
|
|
517
|
+
return f"[{quality.mustNotBeBetween[0]}, {quality.mustNotBeBetween[1]}]"
|
|
518
|
+
return None
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def fill_custom_properties(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
522
|
+
"""Fill the Custom Properties sheet"""
|
|
523
|
+
try:
|
|
524
|
+
ref = name_to_ref(workbook, "CustomProperties")
|
|
525
|
+
if not ref:
|
|
526
|
+
logger.warning("No CustomProperties range found")
|
|
527
|
+
return
|
|
528
|
+
|
|
529
|
+
custom_properties_sheet = workbook["Custom Properties"]
|
|
530
|
+
|
|
531
|
+
# Parse range to find header row
|
|
532
|
+
header_row_index = parse_range_safely(ref)
|
|
533
|
+
|
|
534
|
+
# Fill custom properties excluding owner
|
|
535
|
+
if odcs.customProperties:
|
|
536
|
+
row_index = header_row_index + 1
|
|
537
|
+
for prop in odcs.customProperties:
|
|
538
|
+
if prop.property != "owner" and prop.property:
|
|
539
|
+
row = get_or_create_row(custom_properties_sheet, row_index)
|
|
540
|
+
set_cell_value(row, 0, prop.property) # Property column
|
|
541
|
+
set_cell_value(row, 1, prop.value) # Value column
|
|
542
|
+
row_index += 1
|
|
543
|
+
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.warning(f"Error filling custom properties: {e}")
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def fill_support(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
549
|
+
"""Fill the Support sheet"""
|
|
550
|
+
try:
|
|
551
|
+
ref = name_to_ref(workbook, "support")
|
|
552
|
+
if not ref:
|
|
553
|
+
logger.warning("No support range found")
|
|
554
|
+
return
|
|
555
|
+
|
|
556
|
+
support_sheet = workbook["Support"]
|
|
557
|
+
|
|
558
|
+
# Parse range to find header row
|
|
559
|
+
header_row_index = parse_range_safely(ref)
|
|
560
|
+
|
|
561
|
+
headers = get_headers_from_header_row(support_sheet, header_row_index)
|
|
562
|
+
|
|
563
|
+
if odcs.support:
|
|
564
|
+
for support_index, support_channel in enumerate(odcs.support):
|
|
565
|
+
row = get_or_create_row(support_sheet, header_row_index + 1 + support_index)
|
|
566
|
+
|
|
567
|
+
for cell_index, header_name in headers.items():
|
|
568
|
+
header_lower = header_name.lower()
|
|
569
|
+
if header_lower == "channel":
|
|
570
|
+
set_cell_value(row, cell_index, support_channel.channel)
|
|
571
|
+
elif header_lower == "channel url":
|
|
572
|
+
set_cell_value(row, cell_index, support_channel.url)
|
|
573
|
+
elif header_lower == "description":
|
|
574
|
+
set_cell_value(row, cell_index, support_channel.description)
|
|
575
|
+
elif header_lower == "tool":
|
|
576
|
+
set_cell_value(row, cell_index, support_channel.tool)
|
|
577
|
+
elif header_lower == "scope":
|
|
578
|
+
set_cell_value(row, cell_index, support_channel.scope)
|
|
579
|
+
elif header_lower == "invitation url":
|
|
580
|
+
set_cell_value(row, cell_index, support_channel.invitationUrl)
|
|
581
|
+
|
|
582
|
+
except Exception as e:
|
|
583
|
+
logger.warning(f"Error filling support: {e}")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def fill_team(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
587
|
+
"""Fill the Team sheet"""
|
|
588
|
+
try:
|
|
589
|
+
ref = name_to_ref(workbook, "team")
|
|
590
|
+
if not ref:
|
|
591
|
+
logger.warning("No team range found")
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
team_sheet = workbook["Team"]
|
|
595
|
+
|
|
596
|
+
# Parse range to find header row
|
|
597
|
+
header_row_index = parse_range_safely(ref)
|
|
598
|
+
|
|
599
|
+
headers = get_headers_from_header_row(team_sheet, header_row_index)
|
|
600
|
+
|
|
601
|
+
if odcs.team:
|
|
602
|
+
for team_index, team_member in enumerate(odcs.team):
|
|
603
|
+
row = get_or_create_row(team_sheet, header_row_index + 1 + team_index)
|
|
604
|
+
|
|
605
|
+
for cell_index, header_name in headers.items():
|
|
606
|
+
header_lower = header_name.lower()
|
|
607
|
+
if header_lower == "username":
|
|
608
|
+
set_cell_value(row, cell_index, team_member.username)
|
|
609
|
+
elif header_lower == "name":
|
|
610
|
+
set_cell_value(row, cell_index, team_member.name)
|
|
611
|
+
elif header_lower == "description":
|
|
612
|
+
set_cell_value(row, cell_index, team_member.description)
|
|
613
|
+
elif header_lower == "role":
|
|
614
|
+
set_cell_value(row, cell_index, team_member.role)
|
|
615
|
+
elif header_lower == "date in":
|
|
616
|
+
set_cell_value(row, cell_index, team_member.dateIn)
|
|
617
|
+
elif header_lower == "date out":
|
|
618
|
+
set_cell_value(row, cell_index, team_member.dateOut)
|
|
619
|
+
elif header_lower == "replaced by username":
|
|
620
|
+
set_cell_value(row, cell_index, team_member.replacedByUsername)
|
|
621
|
+
|
|
622
|
+
except Exception as e:
|
|
623
|
+
logger.warning(f"Error filling team: {e}")
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def fill_roles(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
627
|
+
"""Fill the Roles sheet using fixed table structure"""
|
|
628
|
+
try:
|
|
629
|
+
roles_sheet = workbook["Roles"]
|
|
630
|
+
|
|
631
|
+
# From template analysis: Row 4 has headers
|
|
632
|
+
header_row_index = 4
|
|
633
|
+
headers = get_headers_from_header_row(roles_sheet, header_row_index)
|
|
634
|
+
|
|
635
|
+
if odcs.roles:
|
|
636
|
+
for role_index, role in enumerate(odcs.roles):
|
|
637
|
+
row = get_or_create_row(roles_sheet, header_row_index + 1 + role_index)
|
|
638
|
+
|
|
639
|
+
for cell_index, header_name in headers.items():
|
|
640
|
+
header_lower = header_name.lower()
|
|
641
|
+
if header_lower == "role":
|
|
642
|
+
set_cell_value(row, cell_index, role.role)
|
|
643
|
+
elif header_lower == "description":
|
|
644
|
+
set_cell_value(row, cell_index, role.description)
|
|
645
|
+
elif header_lower == "access":
|
|
646
|
+
set_cell_value(row, cell_index, role.access)
|
|
647
|
+
elif header_lower == "1st level approvers":
|
|
648
|
+
set_cell_value(row, cell_index, role.firstLevelApprovers)
|
|
649
|
+
elif header_lower == "2nd level approvers":
|
|
650
|
+
set_cell_value(row, cell_index, role.secondLevelApprovers)
|
|
651
|
+
|
|
652
|
+
except Exception as e:
|
|
653
|
+
logger.warning(f"Error filling roles: {e}")
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def fill_sla_properties(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
657
|
+
"""Fill the SLA sheet using fixed table structure"""
|
|
658
|
+
try:
|
|
659
|
+
sla_sheet = workbook["SLA"]
|
|
660
|
+
|
|
661
|
+
# From template analysis: Row 6 has the SLA properties table headers
|
|
662
|
+
header_row_index = 6
|
|
663
|
+
|
|
664
|
+
headers = get_headers_from_header_row(sla_sheet, header_row_index)
|
|
665
|
+
|
|
666
|
+
if odcs.slaProperties:
|
|
667
|
+
for sla_index, sla_prop in enumerate(odcs.slaProperties):
|
|
668
|
+
row = get_or_create_row(sla_sheet, header_row_index + 1 + sla_index)
|
|
669
|
+
|
|
670
|
+
for cell_index, header_name in headers.items():
|
|
671
|
+
header_lower = header_name.lower()
|
|
672
|
+
if header_lower == "property":
|
|
673
|
+
set_cell_value(row, cell_index, sla_prop.property)
|
|
674
|
+
elif header_lower == "value":
|
|
675
|
+
set_cell_value(row, cell_index, sla_prop.value)
|
|
676
|
+
elif header_lower == "extended value":
|
|
677
|
+
set_cell_value(row, cell_index, sla_prop.valueExt)
|
|
678
|
+
elif header_lower == "unit":
|
|
679
|
+
set_cell_value(row, cell_index, sla_prop.unit)
|
|
680
|
+
elif header_lower == "element":
|
|
681
|
+
set_cell_value(row, cell_index, sla_prop.element)
|
|
682
|
+
elif header_lower == "driver":
|
|
683
|
+
set_cell_value(row, cell_index, sla_prop.driver)
|
|
684
|
+
|
|
685
|
+
except Exception as e:
|
|
686
|
+
logger.warning(f"Error filling SLA properties: {e}")
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def fill_servers(workbook: Workbook, odcs: OpenDataContractStandard):
|
|
690
|
+
"""Fill the Servers sheet"""
|
|
691
|
+
try:
|
|
692
|
+
servers_sheet = workbook["Servers"]
|
|
693
|
+
|
|
694
|
+
if odcs.servers:
|
|
695
|
+
for index, server in enumerate(odcs.servers):
|
|
696
|
+
set_cell_value_by_column_index(servers_sheet, "servers.server", index, server.server)
|
|
697
|
+
set_cell_value_by_column_index(servers_sheet, "servers.description", index, server.description)
|
|
698
|
+
set_cell_value_by_column_index(servers_sheet, "servers.environment", index, server.environment)
|
|
699
|
+
set_cell_value_by_column_index(servers_sheet, "servers.type", index, server.type)
|
|
700
|
+
|
|
701
|
+
# Type-specific fields
|
|
702
|
+
server_type = server.type
|
|
703
|
+
if server_type == "azure":
|
|
704
|
+
set_cell_value_by_column_index(servers_sheet, "servers.azure.location", index, server.location)
|
|
705
|
+
set_cell_value_by_column_index(servers_sheet, "servers.azure.format", index, server.format)
|
|
706
|
+
set_cell_value_by_column_index(servers_sheet, "servers.azure.delimiter", index, server.delimiter)
|
|
707
|
+
elif server_type == "bigquery":
|
|
708
|
+
set_cell_value_by_column_index(servers_sheet, "servers.bigquery.project", index, server.project)
|
|
709
|
+
set_cell_value_by_column_index(servers_sheet, "servers.bigquery.dataset", index, server.dataset)
|
|
710
|
+
elif server_type == "databricks":
|
|
711
|
+
set_cell_value_by_column_index(servers_sheet, "servers.databricks.catalog", index, server.catalog)
|
|
712
|
+
set_cell_value_by_column_index(servers_sheet, "servers.databricks.host", index, server.host)
|
|
713
|
+
set_cell_value_by_column_index(servers_sheet, "servers.databricks.schema", index, server.schema_)
|
|
714
|
+
# Add other server types as needed...
|
|
715
|
+
|
|
716
|
+
except Exception as e:
|
|
717
|
+
logger.warning(f"Error filling servers: {e}")
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
# Helper functions
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def find_cell_by_name(workbook: Workbook, name: str) -> Optional[Cell]:
|
|
724
|
+
"""Find a cell by its named range"""
|
|
725
|
+
try:
|
|
726
|
+
ref = name_to_ref(workbook, name)
|
|
727
|
+
if not ref:
|
|
728
|
+
return None
|
|
729
|
+
return find_cell_by_ref(workbook, ref)
|
|
730
|
+
except Exception:
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def find_cell_by_name_in_sheet(sheet: Worksheet, name: str) -> Optional[Cell]:
|
|
735
|
+
"""Find a cell by its named range within a specific sheet"""
|
|
736
|
+
try:
|
|
737
|
+
# Access worksheet-scoped defined names directly
|
|
738
|
+
for named_range in sheet.defined_names:
|
|
739
|
+
if named_range == name:
|
|
740
|
+
destinations = sheet.defined_names[named_range].destinations
|
|
741
|
+
for sheet_title, coordinate in destinations:
|
|
742
|
+
if sheet_title == sheet.title:
|
|
743
|
+
return sheet[coordinate]
|
|
744
|
+
except Exception:
|
|
745
|
+
return None
|
|
746
|
+
return None
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def find_cell_by_ref(workbook: Workbook, cell_ref: str) -> Optional[Cell]:
|
|
750
|
+
"""Find a cell by its reference"""
|
|
751
|
+
try:
|
|
752
|
+
from openpyxl.utils.cell import column_index_from_string, coordinate_from_string
|
|
753
|
+
|
|
754
|
+
# Parse the reference
|
|
755
|
+
if "!" in cell_ref:
|
|
756
|
+
sheet_name, coord = cell_ref.split("!")
|
|
757
|
+
sheet_name = sheet_name.strip("'")
|
|
758
|
+
sheet = workbook[sheet_name]
|
|
759
|
+
else:
|
|
760
|
+
coord = cell_ref
|
|
761
|
+
sheet = workbook.active
|
|
762
|
+
|
|
763
|
+
# Remove $ signs
|
|
764
|
+
coord = coord.replace("$", "")
|
|
765
|
+
col_letter, row_num = coordinate_from_string(coord)
|
|
766
|
+
col_num = column_index_from_string(col_letter)
|
|
767
|
+
|
|
768
|
+
return sheet.cell(row=int(row_num), column=col_num)
|
|
769
|
+
except Exception:
|
|
770
|
+
return None
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def find_cell_by_ref_in_sheet(sheet: Worksheet, cell_ref: str) -> Optional[Cell]:
|
|
774
|
+
"""Find a cell by its reference within a specific sheet"""
|
|
775
|
+
try:
|
|
776
|
+
from openpyxl.utils.cell import column_index_from_string, coordinate_from_string
|
|
777
|
+
|
|
778
|
+
# Remove sheet name if present
|
|
779
|
+
if "!" in cell_ref:
|
|
780
|
+
_, coord = cell_ref.split("!")
|
|
781
|
+
else:
|
|
782
|
+
coord = cell_ref
|
|
783
|
+
|
|
784
|
+
# Remove $ signs
|
|
785
|
+
coord = coord.replace("$", "")
|
|
786
|
+
col_letter, row_num = coordinate_from_string(coord)
|
|
787
|
+
col_num = column_index_from_string(col_letter)
|
|
788
|
+
|
|
789
|
+
return sheet.cell(row=int(row_num), column=col_num)
|
|
790
|
+
except Exception:
|
|
791
|
+
return None
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def name_to_ref(workbook: Workbook, name: str) -> Optional[str]:
|
|
795
|
+
"""Get the reference for a named range in the workbook"""
|
|
796
|
+
try:
|
|
797
|
+
defined_name = workbook.defined_names.get(name)
|
|
798
|
+
if defined_name:
|
|
799
|
+
return defined_name.attr_text
|
|
800
|
+
except Exception:
|
|
801
|
+
pass
|
|
802
|
+
return None
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def name_to_ref_in_sheet(sheet: Worksheet, name: str) -> Optional[str]:
|
|
806
|
+
"""Get the reference for a named range in a specific sheet"""
|
|
807
|
+
try:
|
|
808
|
+
workbook = sheet.parent
|
|
809
|
+
defined_names = [dn for dn in workbook.defined_names if dn.name == name]
|
|
810
|
+
for dn in defined_names:
|
|
811
|
+
if sheet.title in dn.attr_text:
|
|
812
|
+
return dn.attr_text
|
|
813
|
+
except Exception:
|
|
814
|
+
pass
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def set_cell_value_by_name(workbook: Workbook, cell_name: str, value: Any):
|
|
819
|
+
"""Set cell value by named range"""
|
|
820
|
+
cell = find_cell_by_name(workbook, cell_name)
|
|
821
|
+
if cell:
|
|
822
|
+
set_cell_value_direct(cell, value)
|
|
823
|
+
else:
|
|
824
|
+
logger.warning(f"Cell with name {cell_name} not found in workbook")
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def set_cell_value_by_name_in_sheet(sheet: Worksheet, cell_name: str, value: Any):
|
|
828
|
+
"""Set cell value by named range within a specific sheet"""
|
|
829
|
+
cell = find_cell_by_name_in_sheet(sheet, cell_name)
|
|
830
|
+
if cell:
|
|
831
|
+
set_cell_value_direct(cell, value)
|
|
832
|
+
else:
|
|
833
|
+
logger.warning(f"Cell with name {cell_name} not found in sheet {sheet.title}")
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def set_cell_value_by_column_index(sheet: Worksheet, name: str, column_index: int, value: Any):
|
|
837
|
+
"""Set cell value by column offset from named range"""
|
|
838
|
+
try:
|
|
839
|
+
workbook = sheet.parent
|
|
840
|
+
first_cell = find_cell_by_name(workbook, name)
|
|
841
|
+
if first_cell:
|
|
842
|
+
target_cell = sheet.cell(row=first_cell.row, column=first_cell.column + column_index)
|
|
843
|
+
set_cell_value_direct(target_cell, value)
|
|
844
|
+
except Exception as e:
|
|
845
|
+
logger.warning(f"Error setting cell value by column index: {e}")
|
|
846
|
+
|
|
847
|
+
|
|
848
|
+
def set_cell_value_direct(cell: Cell, value: Any):
|
|
849
|
+
"""Set cell value directly"""
|
|
850
|
+
if value is not None:
|
|
851
|
+
if isinstance(value, bool):
|
|
852
|
+
cell.value = value
|
|
853
|
+
elif isinstance(value, (int, float, Decimal)):
|
|
854
|
+
cell.value = float(value)
|
|
855
|
+
else:
|
|
856
|
+
cell.value = str(value)
|
|
857
|
+
else:
|
|
858
|
+
cell.value = None
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def set_cell_value(row, cell_index: int, value: Any):
|
|
862
|
+
"""Set cell value in a row at specific index"""
|
|
863
|
+
cell = get_or_create_cell(row, cell_index)
|
|
864
|
+
set_cell_value_direct(cell, value)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def get_or_create_row(sheet: Worksheet, row_index: int):
|
|
868
|
+
"""Get or create a row at the specified index"""
|
|
869
|
+
try:
|
|
870
|
+
return sheet[row_index]
|
|
871
|
+
except (IndexError, KeyError):
|
|
872
|
+
# If row doesn't exist, create it
|
|
873
|
+
while len(list(sheet.rows)) < row_index:
|
|
874
|
+
sheet.append([])
|
|
875
|
+
return sheet[row_index]
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def get_or_create_cell(row, cell_index: int) -> Cell:
|
|
879
|
+
"""Get or create a cell at the specified index in a row"""
|
|
880
|
+
try:
|
|
881
|
+
return row[cell_index]
|
|
882
|
+
except IndexError:
|
|
883
|
+
# Extend the row if needed
|
|
884
|
+
while len(row) <= cell_index:
|
|
885
|
+
row.append(None)
|
|
886
|
+
return row[cell_index]
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def parse_range_safely(ref: str) -> int:
|
|
890
|
+
"""Parse a range reference and return the starting row number"""
|
|
891
|
+
try:
|
|
892
|
+
from openpyxl.utils import range_boundaries
|
|
893
|
+
|
|
894
|
+
min_col, min_row, max_col, max_row = range_boundaries(ref)
|
|
895
|
+
return min_row
|
|
896
|
+
except Exception:
|
|
897
|
+
# Handle malformed ranges - extract row number from range like "Quality!$A$4:$AZ$300"
|
|
898
|
+
if ":" in ref:
|
|
899
|
+
start_ref = ref.split(":")[0]
|
|
900
|
+
if "!" in start_ref:
|
|
901
|
+
start_ref = start_ref.split("!")[-1]
|
|
902
|
+
start_ref = start_ref.replace("$", "")
|
|
903
|
+
# Extract row number
|
|
904
|
+
import re
|
|
905
|
+
|
|
906
|
+
row_match = re.search(r"(\d+)", start_ref)
|
|
907
|
+
if row_match:
|
|
908
|
+
return int(row_match.group(1))
|
|
909
|
+
return 1
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def get_headers_from_header_row(sheet: Worksheet, header_row_index: int) -> dict:
|
|
913
|
+
"""Get headers from a row and return as dict mapping cell_index -> header_name"""
|
|
914
|
+
headers = {}
|
|
915
|
+
try:
|
|
916
|
+
header_row = sheet[header_row_index]
|
|
917
|
+
for cell_index, cell in enumerate(header_row):
|
|
918
|
+
if cell.value:
|
|
919
|
+
headers[cell_index] = str(cell.value).strip()
|
|
920
|
+
except Exception as e:
|
|
921
|
+
logger.warning(f"Error getting headers from row {header_row_index}: {e}")
|
|
922
|
+
return headers
|
datacontract/export/exporter.py
CHANGED
|
@@ -204,3 +204,7 @@ exporter_factory.register_lazy_exporter(
|
|
|
204
204
|
exporter_factory.register_lazy_exporter(
|
|
205
205
|
name=ExportFormat.custom, module_path="datacontract.export.custom_converter", class_name="CustomExporter"
|
|
206
206
|
)
|
|
207
|
+
|
|
208
|
+
exporter_factory.register_lazy_exporter(
|
|
209
|
+
name=ExportFormat.excel, module_path="datacontract.export.excel_exporter", class_name="ExcelExporter"
|
|
210
|
+
)
|
|
@@ -415,7 +415,8 @@ def get_cell_value_by_name(workbook: Workbook, name: str) -> str | None:
|
|
|
415
415
|
try:
|
|
416
416
|
cell = get_cell_by_name_in_workbook(workbook, name)
|
|
417
417
|
if cell.value is not None:
|
|
418
|
-
|
|
418
|
+
value = str(cell.value).strip()
|
|
419
|
+
return value if value else None
|
|
419
420
|
except Exception as e:
|
|
420
421
|
logger.warning(f"Error getting cell value by name {name}: {str(e)}")
|
|
421
422
|
return None
|
|
@@ -431,7 +432,8 @@ def get_cell_value_by_name_in_sheet(sheet: Worksheet, name: str) -> str | None:
|
|
|
431
432
|
if sheet_title == sheet.title:
|
|
432
433
|
cell = sheet[coordinate]
|
|
433
434
|
if cell.value is not None:
|
|
434
|
-
|
|
435
|
+
value = str(cell.value).strip()
|
|
436
|
+
return value if value else None
|
|
435
437
|
except Exception as e:
|
|
436
438
|
logger.warning(f"Error getting cell value by name {name} in sheet {sheet.title}: {str(e)}")
|
|
437
439
|
return None
|
|
@@ -443,7 +445,10 @@ def get_cell_value(row, col_idx):
|
|
|
443
445
|
return None
|
|
444
446
|
try:
|
|
445
447
|
cell = row[col_idx]
|
|
446
|
-
|
|
448
|
+
if cell.value is not None:
|
|
449
|
+
value = str(cell.value).strip()
|
|
450
|
+
return value if value else None
|
|
451
|
+
return None
|
|
447
452
|
except (IndexError, AttributeError):
|
|
448
453
|
return None
|
|
449
454
|
|
|
@@ -452,7 +457,10 @@ def get_cell_value_by_position(sheet, row_idx, col_idx):
|
|
|
452
457
|
"""Get cell value by row and column indices (0-based)"""
|
|
453
458
|
try:
|
|
454
459
|
cell = sheet.cell(row=row_idx + 1, column=col_idx + 1) # Convert to 1-based indices
|
|
455
|
-
|
|
460
|
+
if cell.value is not None:
|
|
461
|
+
value = str(cell.value).strip()
|
|
462
|
+
return value if value else None
|
|
463
|
+
return None
|
|
456
464
|
except Exception as e:
|
|
457
465
|
logger.warning(f"Error getting cell value by position ({row_idx}, {col_idx}): {str(e)}")
|
|
458
466
|
return None
|
|
@@ -823,7 +831,7 @@ def import_custom_properties(workbook: Workbook) -> List[CustomProperty]:
|
|
|
823
831
|
except Exception as e:
|
|
824
832
|
logger.warning(f"Error importing custom properties: {str(e)}")
|
|
825
833
|
|
|
826
|
-
return custom_properties
|
|
834
|
+
return custom_properties if custom_properties else None
|
|
827
835
|
|
|
828
836
|
|
|
829
837
|
def parse_property_value(value: str) -> Any:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datacontract-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.33
|
|
4
4
|
Summary: The datacontract CLI is an open source command-line tool for working with Data Contracts. It uses data contract YAML files to lint the data contract, connect to data sources and execute schema and quality tests, detect breaking changes, and export to different formats. The tool is written in Python. It can be used as a standalone CLI tool, in a CI/CD pipeline, or directly as a Python library.
|
|
5
5
|
Author-email: Jochen Christ <jochen.christ@innoq.com>, Stefan Negele <stefan.negele@innoq.com>, Simon Harrer <simon.harrer@innoq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -42,7 +42,7 @@ Provides-Extra: databricks
|
|
|
42
42
|
Requires-Dist: soda-core-spark-df<3.6.0,>=3.3.20; extra == "databricks"
|
|
43
43
|
Requires-Dist: soda-core-spark[databricks]<3.6.0,>=3.3.20; extra == "databricks"
|
|
44
44
|
Requires-Dist: databricks-sql-connector<4.1.0,>=3.7.0; extra == "databricks"
|
|
45
|
-
Requires-Dist: databricks-sdk<0.
|
|
45
|
+
Requires-Dist: databricks-sdk<0.61.0; extra == "databricks"
|
|
46
46
|
Requires-Dist: pyspark<4.0.0,>=3.5.5; extra == "databricks"
|
|
47
47
|
Provides-Extra: iceberg
|
|
48
48
|
Requires-Dist: pyiceberg==0.9.1; extra == "iceberg"
|
|
@@ -214,9 +214,15 @@ $ datacontract export --format odcs datacontract.yaml --output odcs.yaml
|
|
|
214
214
|
# import ODCS to data contract
|
|
215
215
|
$ datacontract import --format odcs odcs.yaml --output datacontract.yaml
|
|
216
216
|
|
|
217
|
-
# import sql (other formats: avro, glue, bigquery, jsonschema ...)
|
|
217
|
+
# import sql (other formats: avro, glue, bigquery, jsonschema, excel ...)
|
|
218
218
|
$ datacontract import --format sql --source my-ddl.sql --dialect postgres --output datacontract.yaml
|
|
219
219
|
|
|
220
|
+
# import from Excel template
|
|
221
|
+
$ datacontract import --format excel --source odcs.xlsx --output datacontract.yaml
|
|
222
|
+
|
|
223
|
+
# export to Excel template
|
|
224
|
+
$ datacontract export --format excel --output odcs.xlsx datacontract.yaml
|
|
225
|
+
|
|
220
226
|
# find differences between two data contracts
|
|
221
227
|
$ datacontract diff datacontract-v1.yaml datacontract-v2.yaml
|
|
222
228
|
|
|
@@ -933,7 +939,7 @@ models:
|
|
|
933
939
|
│ terraform|avro-idl|sql|sql-query|mer │
|
|
934
940
|
│ maid|html|go|bigquery|dbml|spark|sql │
|
|
935
941
|
│ alchemy|data-caterer|dcs|markdown|ic │
|
|
936
|
-
│ eberg|custom]
|
|
942
|
+
│ eberg|custom|excel] │
|
|
937
943
|
│ --output PATH Specify the file path where the │
|
|
938
944
|
│ exported data will be saved. If no │
|
|
939
945
|
│ path is provided, the output will be │
|
|
@@ -1003,6 +1009,7 @@ Available export options:
|
|
|
1003
1009
|
| `dcs` | Export to Data Contract Specification in YAML format | ✅ |
|
|
1004
1010
|
| `markdown` | Export to Markdown | ✅ |
|
|
1005
1011
|
| `iceberg` | Export to an Iceberg JSON Schema Definition | partial |
|
|
1012
|
+
| `excel` | Export to ODCS Excel Template | ✅ |
|
|
1006
1013
|
| `custom` | Export to Custom format with Jinja | ✅ |
|
|
1007
1014
|
| Missing something? | Please create an issue on GitHub | TBD |
|
|
1008
1015
|
|
|
@@ -1274,6 +1281,22 @@ FROM
|
|
|
1274
1281
|
{{ ref('orders') }}
|
|
1275
1282
|
```
|
|
1276
1283
|
|
|
1284
|
+
#### ODCS Excel Templace
|
|
1285
|
+
|
|
1286
|
+
The `export` function converts a data contract into an ODCS (Open Data Contract Standard) Excel template. This creates a user-friendly Excel spreadsheet that can be used for authoring, sharing, and managing data contracts using the familiar Excel interface.
|
|
1287
|
+
|
|
1288
|
+
```shell
|
|
1289
|
+
datacontract export --format excel --output datacontract.xlsx datacontract.yaml
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
The Excel format enables:
|
|
1293
|
+
- **User-friendly authoring**: Create and edit data contracts in Excel's familiar interface
|
|
1294
|
+
- **Easy sharing**: Distribute data contracts as standard Excel files
|
|
1295
|
+
- **Collaboration**: Enable non-technical stakeholders to contribute to data contract definitions
|
|
1296
|
+
- **Round-trip conversion**: Import Excel templates back to YAML data contracts
|
|
1297
|
+
|
|
1298
|
+
For more information about the Excel template structure, visit the [ODCS Excel Template repository](https://github.com/datacontract/open-data-contract-standard-excel-template).
|
|
1299
|
+
|
|
1277
1300
|
### import
|
|
1278
1301
|
```
|
|
1279
1302
|
|
|
@@ -1392,6 +1415,7 @@ Available import options:
|
|
|
1392
1415
|
| `spark` | Import from Spark StructTypes, Variant | ✅ |
|
|
1393
1416
|
| `sql` | Import from SQL DDL | ✅ |
|
|
1394
1417
|
| `unity` | Import from Databricks Unity Catalog | partial |
|
|
1418
|
+
| `excel` | Import from ODCS Excel Template | ✅ |
|
|
1395
1419
|
| Missing something? | Please create an issue on GitHub | TBD |
|
|
1396
1420
|
|
|
1397
1421
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
datacontract/__init__.py,sha256=ThDdxDJsd7qNErLoh628nK5M7RzhJNYCmN-C6BAJFoo,405
|
|
2
2
|
datacontract/api.py,sha256=Ze6pVD3Ub0oyMJI3iYSNXH78K2nPKbXKKHA-0DerJ48,8175
|
|
3
|
-
datacontract/cli.py,sha256=
|
|
4
|
-
datacontract/data_contract.py,sha256=
|
|
3
|
+
datacontract/cli.py,sha256=MxtTI15tnkPieSbHdqtU-wCiwj1oCiEnlMHFGzB4OUg,19364
|
|
4
|
+
datacontract/data_contract.py,sha256=Jlgkbzj6UN8RtFDK5VFcqm7v8oitVs-q10msU8W3Uo8,15183
|
|
5
5
|
datacontract/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
datacontract/breaking/breaking.py,sha256=DnqgxUjD-EAZcg5RBizOP9a2WxsFTaQBik0AB_m3K00,20431
|
|
7
7
|
datacontract/breaking/breaking_change.py,sha256=BIDEUo1U2CQLVT2-I5PyFttxAj6zQPI1UUkEoOOQXMY,2249
|
|
@@ -34,8 +34,9 @@ datacontract/export/dbml_converter.py,sha256=f_OZEFwRUyL-Kg2yn_G58I8iz1VfFrZh8Nb
|
|
|
34
34
|
datacontract/export/dbt_converter.py,sha256=U2x7rtEnq1s3pHhM0L2B6D6OQtKdCdm4PBSqNCHczHk,10577
|
|
35
35
|
datacontract/export/dcs_exporter.py,sha256=RALQ7bLAjak7EsoFFL2GFX2Oju7pnCDPCdRN_wo9wHM,210
|
|
36
36
|
datacontract/export/duckdb_type_converter.py,sha256=hUAAbImhJUMJOXEG-UoOKQqYGrJM6UILpn2YjUuAUOw,2216
|
|
37
|
-
datacontract/export/
|
|
38
|
-
datacontract/export/
|
|
37
|
+
datacontract/export/excel_exporter.py,sha256=ySZL93oaENIjaLyctwoXOiT3yWf311YG3vYtLttjImI,38274
|
|
38
|
+
datacontract/export/exporter.py,sha256=Xo4RyPq9W42hH3xfAX2v8FeQdMFoW0eVzgahY6JjlWI,3069
|
|
39
|
+
datacontract/export/exporter_factory.py,sha256=JRrfcQ9CXiZCw56nFNu9uPSLjlDJLfUC7xPdVTyk6K8,6164
|
|
39
40
|
datacontract/export/go_converter.py,sha256=Ttvbfu3YU-3GBwRD6nwCsFyZuc_hiIvJD-Jg2sT5WLw,3331
|
|
40
41
|
datacontract/export/great_expectations_converter.py,sha256=Wx0mESRy4xAf8y7HjESsGsQaaei8k9xOVu3RbC6BlQM,12257
|
|
41
42
|
datacontract/export/html_exporter.py,sha256=EyTMj25_Df3irZiYw1hxVZeLYWp6YSG6z3IuFUviP14,3066
|
|
@@ -59,7 +60,7 @@ datacontract/imports/bigquery_importer.py,sha256=7TcP9FDsIas5LwJZ-HrOPXZ-NuR056s
|
|
|
59
60
|
datacontract/imports/csv_importer.py,sha256=mBsmyTvfB8q64Z3NYqv4zTDUOvoXG896hZvp3oLt5YM,5330
|
|
60
61
|
datacontract/imports/dbml_importer.py,sha256=o0IOgvXN34lU1FICDHm_QUTv0DKsgwbHPHUDxQhIapE,3872
|
|
61
62
|
datacontract/imports/dbt_importer.py,sha256=hQwqD9vbvwLLc6Yj3tQbar5ldI0pV-ynSiz7CZZ0JCc,8290
|
|
62
|
-
datacontract/imports/excel_importer.py,sha256=
|
|
63
|
+
datacontract/imports/excel_importer.py,sha256=eBLc9VS9OYVFYFcHFHq9HYOStAPBDfVHwmgnBHjxOmc,46415
|
|
63
64
|
datacontract/imports/glue_importer.py,sha256=fiJPkvfwOCsaKKCGW19-JM5CCGXZ2mkNrVtUzp2iw6g,8370
|
|
64
65
|
datacontract/imports/iceberg_importer.py,sha256=vadGJVqQKgG-j8swUytZALFB8QjbGRqZPCcPcCy0vco,5923
|
|
65
66
|
datacontract/imports/importer.py,sha256=NRhR_9AWPWDNq2ac_DVUHGoJuvkVpwwaao8nDfJG_l0,1257
|
|
@@ -112,9 +113,9 @@ datacontract/templates/partials/model_field.html,sha256=2YBF95ypNCPFYuYKoeilRnDG
|
|
|
112
113
|
datacontract/templates/partials/quality.html,sha256=ynEDWRn8I90Uje-xhGYgFcfwOgKI1R-CDki-EvTsauQ,1785
|
|
113
114
|
datacontract/templates/partials/server.html,sha256=dHFJtonMjhiUHtT69RUgTpkoRwmNdTRzkCdH0LtGg_4,6279
|
|
114
115
|
datacontract/templates/style/output.css,sha256=ioIo1f96VW7LHhDifj6QI8QbRChJl-LlQ59EwM8MEmA,28692
|
|
115
|
-
datacontract_cli-0.10.
|
|
116
|
-
datacontract_cli-0.10.
|
|
117
|
-
datacontract_cli-0.10.
|
|
118
|
-
datacontract_cli-0.10.
|
|
119
|
-
datacontract_cli-0.10.
|
|
120
|
-
datacontract_cli-0.10.
|
|
116
|
+
datacontract_cli-0.10.33.dist-info/licenses/LICENSE,sha256=23h64qnSeIZ0DKeziWAKC-zBCt328iSbRbWBrXoYRb4,2210
|
|
117
|
+
datacontract_cli-0.10.33.dist-info/METADATA,sha256=gqEgdS3X0NnbsbAuATDP2YmwNVcqQMgXEJmVayqQbVA,111469
|
|
118
|
+
datacontract_cli-0.10.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
119
|
+
datacontract_cli-0.10.33.dist-info/entry_points.txt,sha256=D3Eqy4q_Z6bHauGd4ppIyQglwbrm1AJnLau4Ppbw9Is,54
|
|
120
|
+
datacontract_cli-0.10.33.dist-info/top_level.txt,sha256=VIRjd8EIUrBYWjEXJJjtdUgc0UAJdPZjmLiOR8BRBYM,13
|
|
121
|
+
datacontract_cli-0.10.33.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|