pydtsx-parser 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydtsx_parser/__init__.py +3 -0
- pydtsx_parser/__main__.py +8 -0
- pydtsx_parser/cli.py +120 -0
- pydtsx_parser/constants.py +41 -0
- pydtsx_parser/dispatcher.py +373 -0
- pydtsx_parser/envelope.py +164 -0
- pydtsx_parser/errors.py +37 -0
- pydtsx_parser/extractors/__init__.py +1 -0
- pydtsx_parser/extractors/columns.py +157 -0
- pydtsx_parser/extractors/components.py +284 -0
- pydtsx_parser/extractors/connections.py +349 -0
- pydtsx_parser/extractors/executables.py +118 -0
- pydtsx_parser/extractors/paths.py +133 -0
- pydtsx_parser/extractors/pipeline.py +115 -0
- pydtsx_parser/extractors/precedence.py +134 -0
- pydtsx_parser/extractors/sort.py +156 -0
- pydtsx_parser/extractors/sources.py +130 -0
- pydtsx_parser/extractors/sql_tasks.py +195 -0
- pydtsx_parser/extractors/transformations.py +371 -0
- pydtsx_parser/extractors/variables.py +76 -0
- pydtsx_parser/mcp_entry.py +31 -0
- pydtsx_parser/mcp_server.py +499 -0
- pydtsx_parser/parsers/__init__.py +1 -0
- pydtsx_parser/parsers/conmgr.py +67 -0
- pydtsx_parser/parsers/dtproj.py +470 -0
- pydtsx_parser/parsers/dtsx.py +156 -0
- pydtsx_parser/parsers/params.py +132 -0
- pydtsx_parser/py.typed +0 -0
- pydtsx_parser/redaction.py +282 -0
- pydtsx_parser/xml_utils.py +185 -0
- pydtsx_parser-0.1.0.dist-info/METADATA +280 -0
- pydtsx_parser-0.1.0.dist-info/RECORD +36 -0
- pydtsx_parser-0.1.0.dist-info/WHEEL +5 -0
- pydtsx_parser-0.1.0.dist-info/entry_points.txt +3 -0
- pydtsx_parser-0.1.0.dist-info/licenses/LICENSE +21 -0
- pydtsx_parser-0.1.0.dist-info/top_level.txt +1 -0
pydtsx_parser/cli.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""CLI module for the SSIS Parser.
|
|
2
|
+
|
|
3
|
+
Provides argument parsing, output routing (stdout vs file), stderr
|
|
4
|
+
warning/error logging, and exit code handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from pydtsx_parser.dispatcher import SUPPORTED_EXTENSIONS, dispatch
|
|
14
|
+
from pydtsx_parser.errors import SSISParseError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("pydtsx_parser")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_args(args: list[str]) -> argparse.Namespace:
|
|
20
|
+
"""Parse CLI arguments.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
args: List of command-line argument strings.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Parsed argparse.Namespace with path, output, and pretty attributes.
|
|
27
|
+
"""
|
|
28
|
+
parser = argparse.ArgumentParser(
|
|
29
|
+
prog="pydtsx-parser",
|
|
30
|
+
description="Parse SSIS files (.dtsx, .dtproj, .conmgr, .params) into JSON.",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"path",
|
|
34
|
+
help="File or directory path to parse",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--output",
|
|
38
|
+
"-o",
|
|
39
|
+
default=None,
|
|
40
|
+
help="Output file path (default: stdout)",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--pretty",
|
|
44
|
+
"-p",
|
|
45
|
+
action="store_true",
|
|
46
|
+
default=False,
|
|
47
|
+
help="Pretty-print JSON with 2-space indent",
|
|
48
|
+
)
|
|
49
|
+
return parser.parse_args(args)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main(args: list[str] | None = None) -> int:
|
|
53
|
+
"""Entry point for the SSIS Parser CLI.
|
|
54
|
+
|
|
55
|
+
Parses arguments, dispatches to the appropriate parser, and routes
|
|
56
|
+
output to stdout or a file. Logs warnings/errors to stderr.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
args: Optional list of CLI arguments. If None, uses sys.argv[1:].
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Exit code: 0 for success, non-zero for errors.
|
|
63
|
+
"""
|
|
64
|
+
# Configure stderr logging for warnings and errors
|
|
65
|
+
logging.basicConfig(
|
|
66
|
+
stream=sys.stderr,
|
|
67
|
+
format="%(levelname)s: %(message)s",
|
|
68
|
+
level=logging.WARNING,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if args is None:
|
|
72
|
+
args = sys.argv[1:]
|
|
73
|
+
|
|
74
|
+
parsed = parse_args(args)
|
|
75
|
+
path = parsed.path
|
|
76
|
+
output_path = parsed.output
|
|
77
|
+
pretty = parsed.pretty
|
|
78
|
+
|
|
79
|
+
# Resolve the path
|
|
80
|
+
resolved = Path(path).resolve()
|
|
81
|
+
|
|
82
|
+
# Check if path exists
|
|
83
|
+
if not resolved.exists():
|
|
84
|
+
print(f"Error: Path does not exist: {resolved}", file=sys.stderr)
|
|
85
|
+
return 1
|
|
86
|
+
|
|
87
|
+
# For single files, check if the extension is supported
|
|
88
|
+
if resolved.is_file():
|
|
89
|
+
ext = resolved.suffix.lower()
|
|
90
|
+
if ext not in SUPPORTED_EXTENSIONS:
|
|
91
|
+
logger.warning("Unsupported file type '%s': %s (skipping)", ext, resolved)
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
# Dispatch parsing
|
|
95
|
+
try:
|
|
96
|
+
result = dispatch(str(resolved), pretty=pretty)
|
|
97
|
+
except SSISParseError as e:
|
|
98
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
99
|
+
return 1
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Error: Unexpected error: {e}", file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
|
|
104
|
+
# Serialize to JSON
|
|
105
|
+
indent = 2 if pretty else None
|
|
106
|
+
json_output = json.dumps(result, indent=indent, ensure_ascii=False)
|
|
107
|
+
|
|
108
|
+
# Route output
|
|
109
|
+
if output_path:
|
|
110
|
+
try:
|
|
111
|
+
output_file = Path(output_path)
|
|
112
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
output_file.write_text(json_output, encoding="utf-8")
|
|
114
|
+
except OSError as e:
|
|
115
|
+
print(f"Error: Cannot write to output file: {e}", file=sys.stderr)
|
|
116
|
+
return 1
|
|
117
|
+
else:
|
|
118
|
+
print(json_output)
|
|
119
|
+
|
|
120
|
+
return 0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Constants used throughout the SSIS Parser."""
|
|
2
|
+
|
|
3
|
+
# Component classification map: componentClassID -> category
|
|
4
|
+
# Used to classify data flow components as source, destination, or transformation.
|
|
5
|
+
# Components not in this map are classified as "unknown".
|
|
6
|
+
COMPONENT_CLASSIFICATION = {
|
|
7
|
+
"Microsoft.OLEDBSource": "source",
|
|
8
|
+
"Microsoft.FlatFileSource": "source",
|
|
9
|
+
"Microsoft.SSISOracleSrc": "source",
|
|
10
|
+
"Microsoft.OLEDBDestination": "destination",
|
|
11
|
+
"Microsoft.FlatFileDestination": "destination",
|
|
12
|
+
"Microsoft.DerivedColumn": "transformation",
|
|
13
|
+
"Microsoft.Sort": "transformation",
|
|
14
|
+
"Microsoft.MergeJoin": "transformation",
|
|
15
|
+
"Microsoft.ConditionalSplit": "transformation",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# SSIS numeric data type codes to human-readable type names
|
|
19
|
+
DATA_TYPE_MAP = {
|
|
20
|
+
"2": "i2",
|
|
21
|
+
"3": "i4",
|
|
22
|
+
"4": "r4",
|
|
23
|
+
"5": "r8",
|
|
24
|
+
"6": "cy",
|
|
25
|
+
"7": "dbDate",
|
|
26
|
+
"11": "bool",
|
|
27
|
+
"20": "i8",
|
|
28
|
+
"72": "guid",
|
|
29
|
+
"129": "str",
|
|
30
|
+
"130": "wstr",
|
|
31
|
+
"131": "numeric",
|
|
32
|
+
"135": "dbTimeStamp",
|
|
33
|
+
"139": "numericWithPrecision",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# XML namespace URIs used in SSIS files
|
|
37
|
+
NAMESPACES = {
|
|
38
|
+
"DTS": "www.microsoft.com/SqlServer/Dts",
|
|
39
|
+
"SSIS": "www.microsoft.com/SqlServer/SSIS",
|
|
40
|
+
"SQLTask": "www.microsoft.com/sqlserver/dts/tasks/sqltask",
|
|
41
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""File dispatcher for the SSIS Parser.
|
|
2
|
+
|
|
3
|
+
Routes files to the correct parser by extension, handles directory scanning
|
|
4
|
+
for recursive discovery of supported SSIS files, and produces combined
|
|
5
|
+
project-level output when given a directory.
|
|
6
|
+
|
|
7
|
+
Supported file types:
|
|
8
|
+
- .dtsx → DTSX package parser
|
|
9
|
+
- .dtproj → DTPROJ project parser
|
|
10
|
+
- .conmgr → Connection manager parser
|
|
11
|
+
- .params → Parameters parser
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from pydtsx_parser.envelope import build_envelope
|
|
19
|
+
from pydtsx_parser.errors import SSISParseError
|
|
20
|
+
from pydtsx_parser.parsers.conmgr import parse_conmgr
|
|
21
|
+
from pydtsx_parser.parsers.dtproj import parse_dtproj
|
|
22
|
+
from pydtsx_parser.parsers.dtsx import parse_dtsx
|
|
23
|
+
from pydtsx_parser.parsers.params import parse_params
|
|
24
|
+
from pydtsx_parser.redaction import redact
|
|
25
|
+
|
|
26
|
+
# Supported file extensions for SSIS parsing
|
|
27
|
+
SUPPORTED_EXTENSIONS = {".dtsx", ".dtproj", ".conmgr", ".params"}
|
|
28
|
+
|
|
29
|
+
# Mapping of file extension to file_type enum value
|
|
30
|
+
_EXTENSION_TO_FILE_TYPE = {
|
|
31
|
+
".dtsx": "dtsx_package",
|
|
32
|
+
".dtproj": "dtproj_project",
|
|
33
|
+
".conmgr": "conmgr_connection",
|
|
34
|
+
".params": "params_parameters",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Mapping of file extension to parser function
|
|
38
|
+
_EXTENSION_TO_PARSER = {
|
|
39
|
+
".dtsx": parse_dtsx,
|
|
40
|
+
".dtproj": parse_dtproj,
|
|
41
|
+
".conmgr": parse_conmgr,
|
|
42
|
+
".params": parse_params,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def dispatch(path: str, pretty: bool = False) -> dict:
|
|
47
|
+
"""Parse a single file or directory and return the result dict.
|
|
48
|
+
|
|
49
|
+
For a single file:
|
|
50
|
+
- Detects file type from extension
|
|
51
|
+
- Calls the appropriate parser
|
|
52
|
+
- Applies redaction
|
|
53
|
+
- Wraps in output envelope with metadata
|
|
54
|
+
|
|
55
|
+
For a directory:
|
|
56
|
+
- Recursively scans for supported SSIS files
|
|
57
|
+
- Parses each file
|
|
58
|
+
- Produces combined project-level output grouping packages,
|
|
59
|
+
connection managers, and parameters with cross-references
|
|
60
|
+
by ObjectName
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
path: File or directory path to parse.
|
|
64
|
+
pretty: Whether to pretty-print JSON (passed through for
|
|
65
|
+
downstream consumers; does not affect dict output).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The parsed result dictionary (envelope for single files,
|
|
69
|
+
combined project structure for directories).
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
FileNotFoundError: If the path does not exist.
|
|
73
|
+
ValueError: If a single file has an unsupported extension.
|
|
74
|
+
"""
|
|
75
|
+
if not path:
|
|
76
|
+
raise FileNotFoundError(path or "", "Path is empty or None")
|
|
77
|
+
|
|
78
|
+
resolved = Path(path).resolve()
|
|
79
|
+
|
|
80
|
+
if not resolved.exists():
|
|
81
|
+
raise FileNotFoundError(str(resolved), f"Path does not exist: {resolved}")
|
|
82
|
+
|
|
83
|
+
if resolved.is_dir():
|
|
84
|
+
return _dispatch_directory(str(resolved))
|
|
85
|
+
else:
|
|
86
|
+
return _dispatch_file(str(resolved))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def scan_directory(dir_path: str) -> list[str]:
|
|
90
|
+
"""Recursively find all supported SSIS files in a directory tree.
|
|
91
|
+
|
|
92
|
+
Walks the directory tree and collects files with extensions matching
|
|
93
|
+
SUPPORTED_EXTENSIONS. Results are sorted for deterministic output.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
dir_path: Path to the directory to scan.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Sorted list of absolute file paths for all supported SSIS files.
|
|
100
|
+
"""
|
|
101
|
+
found_files = []
|
|
102
|
+
resolved = Path(dir_path).resolve()
|
|
103
|
+
|
|
104
|
+
for root, _dirs, files in os.walk(str(resolved)):
|
|
105
|
+
for filename in files:
|
|
106
|
+
ext = Path(filename).suffix.lower()
|
|
107
|
+
if ext in SUPPORTED_EXTENSIONS:
|
|
108
|
+
found_files.append(str(Path(root) / filename))
|
|
109
|
+
|
|
110
|
+
found_files.sort()
|
|
111
|
+
return found_files
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def detect_file_type(file_path: str) -> str:
|
|
115
|
+
"""Map file extension to file_type enum value.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
file_path: Path to the file.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
One of: "dtsx_package", "dtproj_project", "conmgr_connection",
|
|
122
|
+
"params_parameters".
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
ValueError: If the file extension is not supported.
|
|
126
|
+
"""
|
|
127
|
+
ext = Path(file_path).suffix.lower()
|
|
128
|
+
if ext not in _EXTENSION_TO_FILE_TYPE:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"Unsupported file extension '{ext}' for file: {file_path}. "
|
|
131
|
+
f"Supported extensions: {', '.join(sorted(SUPPORTED_EXTENSIONS))}"
|
|
132
|
+
)
|
|
133
|
+
return _EXTENSION_TO_FILE_TYPE[ext]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _dispatch_file(file_path: str) -> dict:
|
|
137
|
+
"""Parse a single file: detect type, parse, redact, wrap in envelope.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
file_path: Absolute path to the file.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Output envelope dict with parsed content.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ValueError: If file extension is not supported.
|
|
147
|
+
"""
|
|
148
|
+
file_type = detect_file_type(file_path)
|
|
149
|
+
ext = Path(file_path).suffix.lower()
|
|
150
|
+
parser = _EXTENSION_TO_PARSER[ext]
|
|
151
|
+
|
|
152
|
+
# Call the appropriate parser
|
|
153
|
+
content = parser(file_path)
|
|
154
|
+
|
|
155
|
+
# Apply redaction
|
|
156
|
+
redacted_content, redaction_count = redact(content)
|
|
157
|
+
|
|
158
|
+
# Wrap in output envelope
|
|
159
|
+
envelope = build_envelope(
|
|
160
|
+
content=redacted_content,
|
|
161
|
+
source_path=file_path,
|
|
162
|
+
file_type=file_type,
|
|
163
|
+
redaction_count=redaction_count,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return envelope
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _dispatch_directory(dir_path: str) -> dict:
|
|
170
|
+
"""Parse all supported files in a directory and produce combined output.
|
|
171
|
+
|
|
172
|
+
Produces a project-level structure grouping:
|
|
173
|
+
- packages (.dtsx files)
|
|
174
|
+
- connection_managers (.conmgr files)
|
|
175
|
+
- parameters (.params files)
|
|
176
|
+
- projects (.dtproj files)
|
|
177
|
+
|
|
178
|
+
Cross-references are established by ObjectName where available.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
dir_path: Absolute path to the directory.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Combined project-level output dict.
|
|
185
|
+
"""
|
|
186
|
+
files = scan_directory(dir_path)
|
|
187
|
+
|
|
188
|
+
packages = []
|
|
189
|
+
connection_managers = []
|
|
190
|
+
parameters = []
|
|
191
|
+
projects = []
|
|
192
|
+
errors = []
|
|
193
|
+
|
|
194
|
+
for file_path in files:
|
|
195
|
+
ext = Path(file_path).suffix.lower()
|
|
196
|
+
|
|
197
|
+
if ext not in SUPPORTED_EXTENSIONS:
|
|
198
|
+
# Skip unsupported files with a warning
|
|
199
|
+
print(
|
|
200
|
+
f"Warning: Skipping unsupported file type: {file_path}",
|
|
201
|
+
file=sys.stderr,
|
|
202
|
+
)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
file_type = detect_file_type(file_path)
|
|
207
|
+
parser = _EXTENSION_TO_PARSER[ext]
|
|
208
|
+
content = parser(file_path)
|
|
209
|
+
|
|
210
|
+
# Check if parser returned an error dict
|
|
211
|
+
if isinstance(content, dict) and content.get("error"):
|
|
212
|
+
errors.append(
|
|
213
|
+
{
|
|
214
|
+
"file_path": file_path,
|
|
215
|
+
"file_type": file_type,
|
|
216
|
+
"error": content,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
# Apply redaction to content
|
|
222
|
+
redacted_content, redaction_count = redact(content)
|
|
223
|
+
|
|
224
|
+
entry = {
|
|
225
|
+
"file_path": file_path,
|
|
226
|
+
"file_type": file_type,
|
|
227
|
+
"file_name": Path(file_path).name,
|
|
228
|
+
"redaction_count": redaction_count,
|
|
229
|
+
"content": redacted_content,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Add ObjectName cross-reference where available
|
|
233
|
+
object_name = _extract_object_name(redacted_content, ext)
|
|
234
|
+
if object_name:
|
|
235
|
+
entry["object_name"] = object_name
|
|
236
|
+
|
|
237
|
+
# Group by file type
|
|
238
|
+
if ext == ".dtsx":
|
|
239
|
+
packages.append(entry)
|
|
240
|
+
elif ext == ".conmgr":
|
|
241
|
+
connection_managers.append(entry)
|
|
242
|
+
elif ext == ".params":
|
|
243
|
+
parameters.append(entry)
|
|
244
|
+
elif ext == ".dtproj":
|
|
245
|
+
projects.append(entry)
|
|
246
|
+
|
|
247
|
+
except SSISParseError as e:
|
|
248
|
+
errors.append(
|
|
249
|
+
{
|
|
250
|
+
"file_path": file_path,
|
|
251
|
+
"file_type": _EXTENSION_TO_FILE_TYPE.get(ext, "unknown"),
|
|
252
|
+
"error": {
|
|
253
|
+
"error": True,
|
|
254
|
+
"file_path": file_path,
|
|
255
|
+
"error_type": type(e).__name__.lower(),
|
|
256
|
+
"message": str(e),
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
except Exception as e:
|
|
261
|
+
errors.append(
|
|
262
|
+
{
|
|
263
|
+
"file_path": file_path,
|
|
264
|
+
"file_type": _EXTENSION_TO_FILE_TYPE.get(ext, "unknown"),
|
|
265
|
+
"error": {
|
|
266
|
+
"error": True,
|
|
267
|
+
"file_path": file_path,
|
|
268
|
+
"error_type": "unexpected_error",
|
|
269
|
+
"message": str(e),
|
|
270
|
+
},
|
|
271
|
+
}
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Build cross-reference index by ObjectName
|
|
275
|
+
cross_references = _build_cross_references(
|
|
276
|
+
packages, connection_managers, parameters
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
"directory_path": dir_path,
|
|
281
|
+
"file_type": "project_directory",
|
|
282
|
+
"summary": {
|
|
283
|
+
"total_files": len(files),
|
|
284
|
+
"packages": len(packages),
|
|
285
|
+
"connection_managers": len(connection_managers),
|
|
286
|
+
"parameters": len(parameters),
|
|
287
|
+
"projects": len(projects),
|
|
288
|
+
"errors": len(errors),
|
|
289
|
+
},
|
|
290
|
+
"packages": packages,
|
|
291
|
+
"connection_managers": connection_managers,
|
|
292
|
+
"parameters": parameters,
|
|
293
|
+
"projects": projects,
|
|
294
|
+
"cross_references": cross_references,
|
|
295
|
+
"errors": errors,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _extract_object_name(content: dict, ext: str) -> str | None:
|
|
300
|
+
"""Extract the ObjectName from parsed content for cross-referencing.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
content: The parsed (and redacted) content dict.
|
|
304
|
+
ext: The file extension.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
The ObjectName string if found, or None.
|
|
308
|
+
"""
|
|
309
|
+
if ext == ".dtsx":
|
|
310
|
+
# DTSX packages have object_name in package_attributes
|
|
311
|
+
pkg_attrs = content.get("package_attributes", {})
|
|
312
|
+
return pkg_attrs.get("object_name")
|
|
313
|
+
elif ext == ".conmgr":
|
|
314
|
+
# Connection managers have object_name in the connection_manager dict
|
|
315
|
+
conn_mgr = content.get("connection_manager", {})
|
|
316
|
+
return conn_mgr.get("object_name")
|
|
317
|
+
elif ext == ".dtproj":
|
|
318
|
+
# Project files: use the project name from manifest
|
|
319
|
+
manifest = content.get("manifest", {})
|
|
320
|
+
project_props = manifest.get("project_properties", {})
|
|
321
|
+
return project_props.get("name")
|
|
322
|
+
elif ext == ".params":
|
|
323
|
+
# Params files don't typically have an object name
|
|
324
|
+
return None
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _build_cross_references(
|
|
329
|
+
packages: list[dict],
|
|
330
|
+
connection_managers: list[dict],
|
|
331
|
+
parameters: list[dict],
|
|
332
|
+
) -> dict:
|
|
333
|
+
"""Build cross-reference index mapping ObjectNames across file types.
|
|
334
|
+
|
|
335
|
+
Creates a lookup that shows which connection managers and parameters
|
|
336
|
+
are referenced by which packages.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
packages: List of parsed package entries.
|
|
340
|
+
connection_managers: List of parsed connection manager entries.
|
|
341
|
+
parameters: List of parsed parameter entries.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Dict with connection_manager_index and parameter_index.
|
|
345
|
+
"""
|
|
346
|
+
# Index connection managers by their ObjectName
|
|
347
|
+
conn_mgr_index = {}
|
|
348
|
+
for cm in connection_managers:
|
|
349
|
+
obj_name = cm.get("object_name")
|
|
350
|
+
if obj_name:
|
|
351
|
+
conn_mgr_index[obj_name] = {
|
|
352
|
+
"file_path": cm.get("file_path"),
|
|
353
|
+
"file_name": cm.get("file_name"),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Index which packages reference which connection managers
|
|
357
|
+
package_connections = {}
|
|
358
|
+
for pkg in packages:
|
|
359
|
+
pkg_name = pkg.get("object_name", pkg.get("file_name", ""))
|
|
360
|
+
content = pkg.get("content", {})
|
|
361
|
+
pkg_conn_mgrs = content.get("connection_managers", [])
|
|
362
|
+
referenced_names = []
|
|
363
|
+
for conn in pkg_conn_mgrs:
|
|
364
|
+
conn_name = conn.get("object_name")
|
|
365
|
+
if conn_name:
|
|
366
|
+
referenced_names.append(conn_name)
|
|
367
|
+
if referenced_names:
|
|
368
|
+
package_connections[pkg_name] = referenced_names
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
"connection_manager_index": conn_mgr_index,
|
|
372
|
+
"package_connection_references": package_connections,
|
|
373
|
+
}
|