#!/usr/bin/env python3 # skd-info v1.8 — Analyze 1C DCS structure # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os import re import sys from collections import OrderedDict from lxml import etree S_NS = "http://v8.1c.ru/8.1/data-composition-system/schema" DCSCOM_NS = "http://v8.1c.ru/8.1/data-composition-system/common" DCSCOR_NS = "http://v8.1c.ru/8.1/data-composition-system/core" DCSSET_NS = "http://v8.1c.ru/8.1/data-composition-system/settings" V8_NS = "http://v8.1c.ru/8.1/data/core" V8UI_NS = "http://v8.1c.ru/8.1/data/ui" XS_NS = "http://www.w3.org/2001/XMLSchema" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" DCSAT_NS = "http://v8.1c.ru/8.1/data-composition-system/area-template" NSMAP = { "s": S_NS, "dcscom": DCSCOM_NS, "dcscor": DCSCOR_NS, "dcsset": DCSSET_NS, "v8": V8_NS, "v8ui": V8UI_NS, "xs": XS_NS, "xsi": XSI_NS, "dcsat": DCSAT_NS, } def localname(el): return etree.QName(el.tag).localname def get_ml_text(node): if node is None: return "" content = node.find("v8:item/v8:content", NSMAP) if content is not None and content.text: return content.text text = (node.text or "").strip() if text: return text # Also check inner text (all text content) all_text = "".join(node.itertext()).strip() if all_text: return all_text return "" def unescape_xml(text): if not text: return text text = text.replace("&", "&") text = text.replace(">", ">") text = text.replace("<", "<") text = text.replace(""", '"') text = text.replace("'", "'") return text def get_compact_type(value_type_node): if value_type_node is None: return "" types = [] for t in value_type_node.findall("v8:Type", NSMAP): raw = (t.text or "").strip() if raw == "xs:string": types.append("String") elif raw == "xs:decimal": types.append("Number") elif raw == "xs:boolean": types.append("Boolean") elif raw == "xs:dateTime": types.append("DateTime") elif raw == "v8:StandardPeriod": types.append("StandardPeriod") elif raw == "v8:StandardBeginningDate": types.append("StandardBeginningDate") elif raw == "v8:AccountType": types.append("AccountType") elif raw == "v8:Null": types.append("Null") else: # Strip namespace prefixes like d4p1: cfg: clean = re.sub(r'^[a-zA-Z0-9]+:', '', raw) types.append(clean) if not types: return "" return " | ".join(types) def get_dataset_type(ds_node): xsi_type = ds_node.get(f"{{{XSI_NS}}}type", "") if "DataSetQuery" in xsi_type: return "Query" if "DataSetObject" in xsi_type: return "Object" if "DataSetUnion" in xsi_type: return "Union" return "Unknown" def get_field_count(ds_node): return len(ds_node.findall("s:field", NSMAP)) def get_query_line_count(ds_node): query_node = ds_node.find("s:query", NSMAP) if query_node is None: return 0 text = "".join(query_node.itertext()) return len(text.split("\n")) def get_structure_item_type(item_node): xsi_type = item_node.get(f"{{{XSI_NS}}}type", "") if "StructureItemGroup" in xsi_type: return "Group" if "StructureItemTable" in xsi_type: return "Table" if "StructureItemChart" in xsi_type: return "Chart" return "Unknown" def get_group_fields(item_node): fields = [] for gi in item_node.findall("dcsset:groupItems/dcsset:item", NSMAP): field_node = gi.find("dcsset:field", NSMAP) group_type = gi.find("dcsset:groupType", NSMAP) if field_node is not None: f = (field_node.text or "").strip() gt = (group_type.text or "").strip() if group_type is not None else "" if gt and gt != "Items": f += f"({gt})" fields.append(f) return fields def get_selection_fields(item_node): fields = [] for si in item_node.findall("dcsset:selection/dcsset:item", NSMAP): xsi_type = si.get(f"{{{XSI_NS}}}type", "") if "SelectedItemAuto" in xsi_type: fields.append("Auto") elif "SelectedItemField" in xsi_type: f = si.find("dcsset:field", NSMAP) if f is not None: fields.append((f.text or "").strip()) elif "SelectedItemFolder" in xsi_type: fields.append("Folder") return fields def get_filter_summary(settings_node): filters = [] for fi in settings_node.findall("dcsset:filter/dcsset:item", NSMAP): xsi_type = fi.get(f"{{{XSI_NS}}}type", "") if "FilterItemGroup" in xsi_type: group_type = fi.find("dcsset:groupType", NSMAP) gt = (group_type.text or "").strip() if group_type is not None else "And" sub_count = len(fi.findall("dcsset:item", NSMAP)) filters.append(f"[Group:{gt} {sub_count} items]") continue use = fi.find("dcsset:use", NSMAP) is_active = "[ ]" if (use is not None and (use.text or "").strip() == "false") else "[x]" left = fi.find("dcsset:left", NSMAP) comp = fi.find("dcsset:comparisonType", NSMAP) right = fi.find("dcsset:right", NSMAP) pres = fi.find("dcsset:presentation", NSMAP) user_setting = fi.find("dcsset:userSettingID", NSMAP) left_str = (left.text or "").strip() if left is not None else "?" comp_str = (comp.text or "").strip() if comp is not None else "?" right_str = "" if right is not None: right_text = "".join(right.itertext()).strip() right_str = f" {right_text}" pres_str = "" if pres is not None: pt = get_ml_text(pres) if pt: pres_str = f' "{pt}"' user_str = "" if user_setting is not None: user_str = " [user]" filters.append(f"{is_active} {left_str} {comp_str}{right_str}{pres_str}{user_str}") return filters def build_structure_tree(item_node, prefix, is_last, out_lines): item_type = get_structure_item_type(item_node) name_node = item_node.find("dcsset:name", NSMAP) item_name = (name_node.text or "").strip() if name_node is not None else "" group_fields = get_group_fields(item_node) group_str = "[" + ", ".join(group_fields) + "]" if group_fields else "(detail)" sel_fields = get_selection_fields(item_node) sel_str = "Selection: " + ", ".join(sel_fields) if sel_fields else "" line = "" if item_type == "Group": line = f"{item_type} {group_str}" if item_name: line = f'{item_type} "{item_name}" {group_str}' elif item_type == "Table": line = "Table" if item_name: line = f'Table "{item_name}"' elif item_type == "Chart": line = "Chart" if item_name: line = f'Chart "{item_name}"' out_lines.append(f"{prefix}{line}") if sel_str and item_type == "Group": out_lines.append(f"{prefix} {sel_str}") # For Table, show columns and rows if item_type == "Table": columns = item_node.findall("dcsset:column", NSMAP) rows = item_node.findall("dcsset:row", NSMAP) for col in columns: col_group = get_group_fields(col) col_group_str = "[" + ", ".join(col_group) + "]" if col_group else "(detail)" col_sel = get_selection_fields(col) col_sel_str = "Selection: " + ", ".join(col_sel) if col_sel else "" conn_c = "\u251C\u2500\u2500" if rows else "\u2514\u2500\u2500" cont_c = "\u2502 " if rows else " " out_lines.append(f"{prefix}{conn_c} Columns: {col_group_str}") if col_sel_str: out_lines.append(f"{prefix}{cont_c} {col_sel_str}") for row in rows: row_group = get_group_fields(row) row_group_str = "[" + ", ".join(row_group) + "]" if row_group else "(detail)" row_sel = get_selection_fields(row) row_sel_str = "Selection: " + ", ".join(row_sel) if row_sel else "" out_lines.append(f"{prefix}\u2514\u2500\u2500 Rows: {row_group_str}") if row_sel_str: out_lines.append(f"{prefix} {row_sel_str}") # Recurse into nested structure items (for Group) if item_type == "Group": children = item_node.findall("dcsset:item", NSMAP) for i, child in enumerate(children): child_type = child.get(f"{{{XSI_NS}}}type", "") if "StructureItem" in child_type: last = (i == len(children) - 1) connector = "\u2514\u2500 " if last else "\u251C\u2500 " continuation = " " if last else "\u2502 " build_structure_tree(child, f"{prefix}{continuation}", last, out_lines) def get_support_status_for_path(target_path): try: def root_uuid(xml_path): if not os.path.isfile(xml_path): return None try: mx = etree.parse(xml_path).getroot() for child in mx: if isinstance(child.tag, str) and child.get("uuid"): return child.get("uuid") except Exception: pass return None def is_external_root(xml_path): if not os.path.isfile(xml_path): return False try: mx = etree.parse(xml_path).getroot() for child in mx: if isinstance(child.tag, str): return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport") except Exception: return False return False rp = os.path.abspath(target_path) # The target file itself may be the element meta-xml (e.g. Subsystems/X.xml). elem_uuid = root_uuid(rp) if is_external_root(rp): return None bin_path = None d = os.path.dirname(rp) for _ in range(12): if not d: break if is_external_root(d + ".xml"): return None if not elem_uuid: elem_uuid = root_uuid(d + ".xml") if not bin_path: cand = os.path.join(d, "Ext", "ParentConfigurations.bin") if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")): bin_path = cand if elem_uuid and bin_path: break parent = os.path.dirname(d) if parent == d: break d = parent if not bin_path or not os.path.exists(bin_path): return "не на поддержке" data = open(bin_path, "rb").read() if len(data) <= 32: return "снято с поддержки (правки свободны)" if data[:3] == b"\xef\xbb\xbf": data = data[3:] text = data.decode("utf-8", "replace") h = re.match(r"\{6,(\d+),(\d+),", text) if not h: return "не на поддержке" g = int(h.group(1)) k = int(h.group(2)) if k == 0: return "снято с поддержки (правки свободны)" if g == 1: return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" if not elem_uuid: return "не на поддержке" best = None for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text): f1 = int(m.group(1)) if best is None or f1 < best: best = f1 if best is None: return "не на поддержке" return { 0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта", 1: "редактируется с сохранением поддержки", 2: "снято с поддержки (правки свободны)", }.get(best, "не на поддержке") except Exception: return "не на поддержке" def main(): sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser(description="Analyze 1C DCS structure", allow_abbrev=False) parser.add_argument("-TemplatePath", "-Path", required=True) parser.add_argument("-Mode", default="overview", choices=["overview", "query", "fields", "links", "calculated", "resources", "params", "variant", "trace", "templates", "full"]) parser.add_argument("-Name", default=None) parser.add_argument("-Batch", type=int, default=0) parser.add_argument("-Limit", type=int, default=150) parser.add_argument("-Offset", type=int, default=0) parser.add_argument("-OutFile", default=None) parser.add_argument("-Raw", action="store_true") args = parser.parse_args() # --- Resolve path --- original_path = args.TemplatePath template_path = original_path if not template_path.endswith(".xml"): candidate = os.path.join(template_path, "Ext", "Template.xml") if os.path.isfile(candidate): template_path = candidate # If still not found, try resolving from object directory (Reports/X, DataProcessors/X) if not os.path.isfile(template_path) and not template_path.endswith(".xml"): templates_dir = os.path.join(original_path, "Templates") if os.path.isdir(templates_dir): dcs_templates = [] for fname in os.listdir(templates_dir): if not fname.endswith(".xml"): continue meta_path = os.path.join(templates_dir, fname) if not os.path.isfile(meta_path): continue try: meta_tree = etree.parse(meta_path, etree.XMLParser(remove_blank_text=True)) tt_nodes = meta_tree.xpath("//*[local-name()='TemplateType']") if tt_nodes and (tt_nodes[0].text or "").strip() == "DataCompositionSchema": tpl_name = os.path.splitext(fname)[0] tpl_path = os.path.join(templates_dir, tpl_name, "Ext", "Template.xml") if os.path.isfile(tpl_path): dcs_templates.append(tpl_path) except Exception: continue if len(dcs_templates) == 1: template_path = dcs_templates[0] resolved_display = os.path.relpath(os.path.abspath(template_path)) print(f"[i] Resolved: {resolved_display}") elif len(dcs_templates) > 1: print(f"Multiple DCS templates found in: {original_path}") for i, p in enumerate(dcs_templates): print(f" {i+1}. {os.path.relpath(os.path.abspath(p))}") print("Specify the template path.") sys.exit(1) else: print(f"No DCS templates found in: {original_path}", file=sys.stderr) sys.exit(1) if not os.path.isabs(template_path): template_path = os.path.join(os.getcwd(), template_path) if not os.path.isfile(template_path): print(f"File not found: {template_path}", file=sys.stderr) sys.exit(1) resolved_path = os.path.abspath(template_path) # --- Load XML --- xml_parser = etree.XMLParser(remove_blank_text=True) tree = etree.parse(resolved_path, xml_parser) root = tree.getroot() # --- Output collector --- lines = [] # Determine template name from path path_parts = re.split(r'[/\\]', resolved_path) template_name = resolved_path for i in range(len(path_parts) - 1, -1, -1): if path_parts[i] == "Ext" and i >= 1: template_name = path_parts[i - 1] break with open(resolved_path, "r", encoding="utf-8-sig") as fh: total_xml_lines = len(fh.readlines()) # === Helper functions that use root/lines/NSMAP === def show_overview(): lines.append(f"=== DCS: {template_name} ({total_xml_lines} lines) ===") _support = get_support_status_for_path(template_path) if _support is not None: lines.append(f"Поддержка: {_support}") lines.append("") # Sources sources = [] for ds in root.findall("s:dataSource", NSMAP): ds_name_node = ds.find("s:name", NSMAP) ds_type_node = ds.find("s:dataSourceType", NSMAP) ds_name = (ds_name_node.text or "") if ds_name_node is not None else "" ds_type = (ds_type_node.text or "") if ds_type_node is not None else "" sources.append(f"{ds_name} ({ds_type})") lines.append("Sources: " + ", ".join(sources)) lines.append("") # Datasets (recursive for Union) lines.append("Datasets:") for ds in root.findall("s:dataSet", NSMAP): ds_type = get_dataset_type(ds) ds_name_node = ds.find("s:name", NSMAP) ds_name = (ds_name_node.text or "") if ds_name_node is not None else "" field_count = get_field_count(ds) if ds_type == "Query": query_lines = get_query_line_count(ds) lines.append(f" [Query] {ds_name} {field_count} fields, query {query_lines} lines") elif ds_type == "Object": obj_name = ds.find("s:objectName", NSMAP) obj_str = f" objectName={obj_name.text}" if obj_name is not None else "" lines.append(f" [Object] {ds_name}{obj_str} {field_count} fields") elif ds_type == "Union": lines.append(f" [Union] {ds_name} {field_count} fields") for sub_ds in ds.findall("s:item", NSMAP): sub_type = get_dataset_type(sub_ds) sub_name_node = sub_ds.find("s:name", NSMAP) sub_name_str = (sub_name_node.text or "") if sub_name_node is not None else "?" sub_fields = get_field_count(sub_ds) if sub_type == "Query": sub_query_lines = get_query_line_count(sub_ds) lines.append(f" \u251C\u2500 [Query] {sub_name_str} {sub_fields} fields, query {sub_query_lines} lines") elif sub_type == "Object": sub_obj_name = sub_ds.find("s:objectName", NSMAP) sub_obj_str = f" objectName={sub_obj_name.text}" if sub_obj_name is not None else "" lines.append(f" \u251C\u2500 [Object] {sub_name_str}{sub_obj_str} {sub_fields} fields") else: lines.append(f" \u251C\u2500 [{sub_type}] {sub_name_str} {sub_fields} fields") # Links -- only dataset pairs (not field-level) link_nodes = root.findall("s:dataSetLink", NSMAP) if link_nodes: link_pairs = OrderedDict() for lnk in link_nodes: src_ds = (lnk.find("s:sourceDataSet", NSMAP).text or "") dst_ds = (lnk.find("s:destinationDataSet", NSMAP).text or "") key = f"{src_ds} -> {dst_ds}" if key not in link_pairs: link_pairs[key] = 0 link_pairs[key] += 1 link_strs = [] for key, cnt in link_pairs.items(): if cnt > 1: link_strs.append(f"{key} ({cnt} fields)") else: link_strs.append(key) lines.append("Links: " + ", ".join(link_strs)) # Calculated fields -- count only calc_fields = root.findall("s:calculatedField", NSMAP) if calc_fields: lines.append(f"Calculated: {len(calc_fields)}") # Totals -- count + group flag total_fields = root.findall("s:totalField", NSMAP) if total_fields: has_grouped = False unique_paths = {} for tf in total_fields: tf_path = (tf.find("s:dataPath", NSMAP).text or "") unique_paths[tf_path] = True if tf.find("s:group", NSMAP) is not None: has_grouped = True group_note = ", with group formulas" if has_grouped else "" if len(unique_paths) == len(total_fields): lines.append(f"Resources: {len(total_fields)}{group_note}") else: lines.append(f"Resources: {len(total_fields)} ({len(unique_paths)} fields{group_note})") # Templates -- count with binding types tpl_defs = root.findall("s:template", NSMAP) field_tpls = root.findall("s:fieldTemplate", NSMAP) group_tpls = root.findall("s:groupTemplate", NSMAP) group_header_tpls = root.findall("s:groupHeaderTemplate", NSMAP) group_footer_tpls = root.findall("s:groupFooterTemplate", NSMAP) if tpl_defs: parts = [] if field_tpls: parts.append(f"{len(field_tpls)} field") grp_count = len(group_tpls) + len(group_header_tpls) + len(group_footer_tpls) if grp_count > 0: parts.append(f"{grp_count} group") if parts: lines.append(f"Templates: {len(tpl_defs)} defined ({', '.join(parts)} bindings)") else: lines.append(f"Templates: {len(tpl_defs)} defined") # Parameters -- split visible/hidden params = root.findall("s:parameter", NSMAP) if params: visible_names = [] hidden_count = 0 for p in params: p_name = (p.find("s:name", NSMAP).text or "") use_restrict = p.find("s:useRestriction", NSMAP) is_hidden = (use_restrict is not None and (use_restrict.text or "").strip() == "true") if is_hidden: hidden_count += 1 else: visible_names.append(p_name) param_line = f"Params: {len(params)}" if hidden_count > 0 and visible_names: param_line += f" ({len(visible_names)} visible, {hidden_count} hidden)" elif hidden_count == len(params): param_line += " (all hidden)" if visible_names and len(visible_names) <= 8: param_line += ": " + ", ".join(visible_names) lines.append(param_line) else: lines.append("Params: (none)") lines.append("") # Variants variants = root.findall("s:settingsVariant", NSMAP) if variants: lines.append("Variants:") for var_idx, v in enumerate(variants, 1): v_name = (v.find("dcsset:name", NSMAP).text or "") v_pres = v.find("dcsset:presentation", NSMAP) v_pres_str = "" if v_pres is not None: pt = get_ml_text(v_pres) if pt: v_pres_str = f' "{pt}"' settings = v.find("dcsset:settings", NSMAP) struct_items = [] if settings is not None: for si in settings.findall("dcsset:item", NSMAP): si_type = get_structure_item_type(si) gf = get_group_fields(si) g_str = "(" + ",".join(gf) + ")" if gf else "(detail)" struct_items.append(f"{si_type}{g_str}") # Compact: if many identical items, show count if len(struct_items) > 3: from collections import Counter counted = Counter(struct_items) compact_parts = [] for name, count in counted.most_common(): if count > 1: compact_parts.append(f"{count}x {name}") else: compact_parts.append(name) struct_items = compact_parts struct_str = " " + ", ".join(struct_items) if struct_items else "" filter_count = 0 if settings is not None: filter_count = len(settings.findall("dcsset:filter/dcsset:item", NSMAP)) filter_str = f" {filter_count} filters" if filter_count > 0 else "" lines.append(f" [{var_idx}] {v_name}{v_pres_str}{struct_str}{filter_str}") def show_overview_hints(): lines.append("") hints = [] # Collect query dataset names for hint query_ds_names = [] for ds in root.findall("s:dataSet", NSMAP): ds_type = get_dataset_type(ds) if ds_type == "Query": query_ds_names.append((ds.find("s:name", NSMAP).text or "")) elif ds_type == "Union": for sub_ds in ds.findall("s:item", NSMAP): if get_dataset_type(sub_ds) == "Query": sn = sub_ds.find("s:name", NSMAP) if sn is not None: query_ds_names.append((sn.text or "")) if len(query_ds_names) == 1: hints.append("-Mode query query text") elif len(query_ds_names) > 1: hints.append(f"-Mode query -Name query text ({', '.join(query_ds_names)})") hints.append("-Mode fields field tables by dataset") link_count = len(root.findall("s:dataSetLink", NSMAP)) if link_count > 0: hints.append(f"-Mode links dataset connections ({link_count})") calc_count = len(root.findall("s:calculatedField", NSMAP)) total_count = len(root.findall("s:totalField", NSMAP)) if calc_count > 0: hints.append(f"-Mode calculated calculated field expressions ({calc_count})") if total_count > 0: hints.append(f"-Mode resources resource aggregation ({total_count})") params = root.findall("s:parameter", NSMAP) if params: hints.append("-Mode params parameter details") variants = root.findall("s:settingsVariant", NSMAP) if len(variants) == 1: hints.append("-Mode variant variant structure") elif len(variants) > 1: hints.append(f"-Mode variant -Name variant structure (1..{len(variants)})") tpl_defs = root.findall("s:template", NSMAP) if tpl_defs: hints.append("-Mode templates template bindings and expressions") hints.append("-Mode trace -Name trace field origin (by name or title)") hints.append("-Mode full all sections at once") lines.append("Next:") for h in hints: lines.append(f" {h}") def show_query(): # Find dataset data_sets = root.findall("s:dataSet", NSMAP) target_ds = None if args.Name: # Search by name: prefer nested Query items over parent Union # Pass 1: search nested items first for ds in data_sets: for sub_ds in ds.findall("s:item", NSMAP): sub_name_node = sub_ds.find("s:name", NSMAP) if sub_name_node is not None and (sub_name_node.text or "") == args.Name: target_ds = sub_ds break if target_ds is not None: break # Pass 2: search top-level if target_ds is None: for ds in data_sets: ds_name_node = ds.find("s:name", NSMAP) if ds_name_node is not None and (ds_name_node.text or "") == args.Name: target_ds = ds break if target_ds is None: print(f"Dataset '{args.Name}' not found", file=sys.stderr) sys.exit(1) else: # Take first Query dataset for ds in data_sets: ds_type = get_dataset_type(ds) if ds_type == "Query": target_ds = ds break if ds_type == "Union": for sub_ds in ds.findall("s:item", NSMAP): if get_dataset_type(sub_ds) == "Query": target_ds = sub_ds break if target_ds is not None: break if target_ds is None: print("No Query dataset found", file=sys.stderr) sys.exit(1) query_node = target_ds.find("s:query", NSMAP) if query_node is None: # If this is a Union, list nested query datasets ds_type = get_dataset_type(target_ds) if ds_type == "Union": sub_names = [] for sub_ds in target_ds.findall("s:item", NSMAP): sn = sub_ds.find("s:name", NSMAP) if sn is not None: sub_names.append((sn.text or "")) ds_name_text = (target_ds.find("s:name", NSMAP).text or "") print(f"Dataset '{ds_name_text}' is a Union. Specify nested: {', '.join(sub_names)}", file=sys.stderr) else: print("Dataset has no query element", file=sys.stderr) sys.exit(1) raw_query = unescape_xml("".join(query_node.itertext())) # Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip if args.Raw: for ql in raw_query.strip().split("\n"): lines.append(ql.rstrip()) return ds_name_str = (target_ds.find("s:name", NSMAP).text or "") # Split into batches batches = [] batch_texts = re.split(r';\s*\r?\n\s*/{16,}\s*\r?\n', raw_query) for bt in batch_texts: trimmed = bt.strip() if trimmed: batches.append(trimmed) total_query_lines = len(raw_query.split("\n")) if len(batches) <= 1: # Single query lines.append(f"=== Query: {ds_name_str} ({total_query_lines} lines) ===") lines.append("") for ql in raw_query.strip().split("\n"): lines.append(ql.rstrip()) else: lines.append(f"=== Query: {ds_name_str} ({total_query_lines} lines, {len(batches)} batches) ===") if args.Batch == 0: # Show TOC line_num = 1 for bi in range(len(batches)): batch_lines = batches[bi].split("\n") end_line = line_num + len(batch_lines) - 1 # Detect target target = "" for bl in batch_lines: m = re.match(r'^\s*(?:\u041F\u041E\u041C\u0415\u0421\u0422\u0418\u0422\u042C|INTO)\s+(\S+)', bl) if m: target = "\u2192 " + m.group(1) break lines.append(f" Batch {bi + 1}: lines {line_num}-{end_line} {target}") line_num = end_line + 3 # +separator lines.append("") # Show all batches for bi in range(len(batches)): lines.append(f"--- Batch {bi + 1} ---") for ql in batches[bi].split("\n"): lines.append(ql.rstrip()) lines.append("") else: # Show specific batch if args.Batch > len(batches): print(f"Batch {args.Batch} not found (total: {len(batches)})", file=sys.stderr) sys.exit(1) lines.append("") lines.append(f"--- Batch {args.Batch} ---") for ql in batches[args.Batch - 1].split("\n"): lines.append(ql.rstrip()) def show_fields(): data_sets = root.findall("s:dataSet", NSMAP) def show_dataset_fields(ds_node): ds_type = get_dataset_type(ds_node) ds_name_str = (ds_node.find("s:name", NSMAP).text or "") fields = ds_node.findall("s:field", NSMAP) lines.append(f"=== Fields: {ds_name_str} [{ds_type}] ({len(fields)}) ===") lines.append(" dataPath title role restrict format") for f in fields: dp = f.find("s:dataPath", NSMAP) dp_str = (dp.text or "").strip() if dp is not None else "-" title_node = f.find("s:title", NSMAP) title_str = get_ml_text(title_node) if title_node is not None else "" if not title_str: title_str = "-" # Role role = f.find("s:role", NSMAP) role_str = "-" if role is not None: role_parts = [] for child in role: if isinstance(child.tag, str) and (child.text or "").strip() == "true": role_parts.append(localname(child)) if role_parts: role_str = ",".join(role_parts) # UseRestriction restrict = f.find("s:useRestriction", NSMAP) restrict_str = "-" if restrict is not None: restrict_parts = [] for child in restrict: if isinstance(child.tag, str) and (child.text or "").strip() == "true": ln = localname(child) restrict_parts.append(ln[:4]) if restrict_parts: restrict_str = ",".join(restrict_parts) # Appearance format format_str = "-" appearance = f.find("s:appearance", NSMAP) if appearance is not None: for app_item in appearance.findall("dcscor:item", NSMAP): param_node = app_item.find("dcscor:parameter", NSMAP) val_node = app_item.find("dcscor:value", NSMAP) if param_node is not None and val_node is not None: param_text = (param_node.text or "").strip() if param_text in ("\u0424\u043E\u0440\u043C\u0430\u0442", "Format"): format_str = (val_node.text or "").strip() # presentationExpression pres_expr = f.find("s:presentationExpression", NSMAP) pres_str = " presExpr" if pres_expr is not None else "" dp_pad = dp_str.ljust(35) title_pad = title_str.ljust(22) role_pad = role_str.ljust(10) restrict_pad = restrict_str.ljust(12) lines.append(f" {dp_pad} {title_pad} {role_pad} {restrict_pad} {format_str}{pres_str}") def collect_field_info(ds_node): ds_type = get_dataset_type(ds_node) ds_name_str = (ds_node.find("s:name", NSMAP).text or "") for f in ds_node.findall("s:field", NSMAP): dp = f.find("s:dataPath", NSMAP) if dp is None or (dp.text or "") != args.Name: continue info = {"dataset": f"{ds_name_str} [{ds_type}]"} title_node = f.find("s:title", NSMAP) info["title"] = get_ml_text(title_node) if title_node is not None else "" # ValueType vt = f.find("s:valueType", NSMAP) info["type"] = get_compact_type(vt) if vt is not None else "" # Role role = f.find("s:role", NSMAP) role_parts = [] if role is not None: for child in role: if not isinstance(child.tag, str): continue txt = (child.text or "").strip() if txt == "true": role_parts.append(localname(child)) elif txt == "false": pass else: role_parts.append(f"{localname(child)}={txt}") info["role"] = ", ".join(role_parts) # UseRestriction restrict = f.find("s:useRestriction", NSMAP) restrict_parts = [] if restrict is not None: for child in restrict: if isinstance(child.tag, str) and (child.text or "").strip() == "true": restrict_parts.append(localname(child)) info["restrict"] = ", ".join(restrict_parts) # Format format_str = "" appearance = f.find("s:appearance", NSMAP) if appearance is not None: for app_item in appearance.findall("dcscor:item", NSMAP): pn = app_item.find("dcscor:parameter", NSMAP) vn = app_item.find("dcscor:value", NSMAP) if pn is not None and vn is not None: pn_text = (pn.text or "").strip() if pn_text in ("\u0424\u043E\u0440\u043C\u0430\u0442", "Format"): format_str = (vn.text or "").strip() info["format"] = format_str # PresentationExpression pres_expr = f.find("s:presentationExpression", NSMAP) info["presExpr"] = "".join(pres_expr.itertext()) if pres_expr is not None else "" return info return None if args.Name: # Detail for specific field by dataPath -- search all datasets field_infos = [] for ds in data_sets: info = collect_field_info(ds) if info: field_infos.append(info) ds_type = get_dataset_type(ds) if ds_type == "Union": for sub_ds in ds.findall("s:item", NSMAP): info = collect_field_info(sub_ds) if info: field_infos.append(info) if not field_infos: print(f"Field '{args.Name}' not found in any dataset", file=sys.stderr) sys.exit(1) # Use first match for detail first = field_infos[0] title_str = f' "{first["title"]}"' if first["title"] else "" lines.append(f"=== Field: {args.Name}{title_str} ===") lines.append("") # Datasets ds_list = ", ".join(fi["dataset"] for fi in field_infos) lines.append(f"Dataset: {ds_list}") if first["type"]: lines.append(f"Type: {first['type']}") if first["role"]: lines.append(f"Role: {first['role']}") if first["restrict"]: lines.append(f"Restrict: {first['restrict']}") if first["format"]: lines.append(f"Format: {first['format']}") if first["presExpr"]: lines.append("PresentationExpression:") for el in first["presExpr"].split("\n"): lines.append(f" {el.rstrip()}") else: # Compact map: field names per dataset lines.append("=== Fields map ===") def show_dataset_field_map(ds_node, indent): ds_type = get_dataset_type(ds_node) ds_name_str = (ds_node.find("s:name", NSMAP).text or "") fields = ds_node.findall("s:field", NSMAP) field_names = [] for f in fields: dp = f.find("s:dataPath", NSMAP) if dp is not None: field_names.append((dp.text or "")) name_list = ", ".join(field_names) if len(name_list) > 100: name_list = name_list[:97] + "..." lines.append(f"{indent}{ds_name_str} [{ds_type}] ({len(fields)}): {name_list}") for ds in data_sets: show_dataset_field_map(ds, "") ds_type = get_dataset_type(ds) if ds_type == "Union": for sub_ds in ds.findall("s:item", NSMAP): show_dataset_field_map(sub_ds, " ") lines.append("") lines.append("Use -Name for details.") def show_links(): link_nodes = root.findall("s:dataSetLink", NSMAP) if not link_nodes: lines.append("(no links)") else: lines.append(f"=== Links ({len(link_nodes)}) ===") lines.append("") # Group by source->dest pair current_pair = "" for lnk in link_nodes: src_ds = (lnk.find("s:sourceDataSet", NSMAP).text or "") dst_ds = (lnk.find("s:destinationDataSet", NSMAP).text or "") src_expr = (lnk.find("s:sourceExpression", NSMAP).text or "") dst_expr = (lnk.find("s:destinationExpression", NSMAP).text or "") param_node = lnk.find("s:parameter", NSMAP) pair = f"{src_ds} -> {dst_ds}" if pair != current_pair: if current_pair: lines.append("") lines.append(f"{pair} :") current_pair = pair param_str = "" if param_node is not None: param_str = f" param={param_node.text or ''}" lines.append(f" {src_expr} -> {dst_expr}{param_str}") def show_calculated(): calc_fields = root.findall("s:calculatedField", NSMAP) if not calc_fields: lines.append("(no calculated fields)") elif args.Name: found = False for cf in calc_fields: cf_path = (cf.find("s:dataPath", NSMAP).text or "") if cf_path == args.Name: lines.append(f"=== Calculated: {cf_path} ===") lines.append("") cf_expr = "".join(cf.find("s:expression", NSMAP).itertext()) lines.append("Expression:") for el in cf_expr.split("\n"): lines.append(f" {el.rstrip()}") cf_title = cf.find("s:title", NSMAP) if cf_title is not None: t = get_ml_text(cf_title) if t: lines.append(f"Title: {t}") cf_restrict = cf.find("s:useRestriction", NSMAP) if cf_restrict is not None: parts = [] for child in cf_restrict: if isinstance(child.tag, str) and (child.text or "").strip() == "true": parts.append(localname(child)) if parts: lines.append(f"Restrict: {', '.join(parts)}") found = True break if not found: print(f"Calculated field '{args.Name}' not found", file=sys.stderr) sys.exit(1) else: # Map lines.append(f"=== Calculated fields ({len(calc_fields)}) ===") for cf in calc_fields: cf_path = (cf.find("s:dataPath", NSMAP).text or "") cf_title = cf.find("s:title", NSMAP) title_str = "" if cf_title is not None: t = get_ml_text(cf_title) if t and t != cf_path: title_str = f' "{t}"' lines.append(f" {cf_path}{title_str}") lines.append("") lines.append("Use -Name for full expression.") def show_resources(): total_fields = root.findall("s:totalField", NSMAP) if not total_fields: lines.append("(no resources)") elif args.Name: matched = [] for tf in total_fields: tf_path = (tf.find("s:dataPath", NSMAP).text or "") if tf_path == args.Name: matched.append(tf) if not matched: print(f"Resource '{args.Name}' not found", file=sys.stderr) sys.exit(1) lines.append(f"=== Resource: {args.Name} ===") lines.append("") for tf in matched: tf_expr = (tf.find("s:expression", NSMAP).text or "") tf_group = tf.find("s:group", NSMAP) group_str = "(overall)" if tf_group is not None: group_str = (tf_group.text or "") lines.append(f" [{group_str}] {tf_expr}") else: # Map lines.append(f"=== Resources ({len(total_fields)}) ===") res_map = OrderedDict() for tf in total_fields: tf_path = (tf.find("s:dataPath", NSMAP).text or "") tf_group = tf.find("s:group", NSMAP) if tf_path not in res_map: res_map[tf_path] = {"hasGroup": False} if tf_group is not None: res_map[tf_path]["hasGroup"] = True for key, val in res_map.items(): group_mark = " *" if val["hasGroup"] else "" lines.append(f" {key}{group_mark}") lines.append("") lines.append(" * = has group-level formulas") lines.append("") lines.append("Use -Name for full formula.") def show_params(): params = root.findall("s:parameter", NSMAP) lines.append(f"=== Parameters ({len(params)}) ===") lines.append(" Name Type Default Visible Expression") for p in params: p_name = (p.find("s:name", NSMAP).text or "") p_type = get_compact_type(p.find("s:valueType", NSMAP)) if not p_type: p_type = "-" # Default value val_node = p.find("s:value", NSMAP) val_str = "-" if val_node is not None: nil_attr = val_node.get(f"{{{XSI_NS}}}nil", "") if nil_attr == "true": val_str = "null" else: raw = "".join(val_node.itertext()).strip() if raw == "0001-01-01T00:00:00": val_str = "-" elif raw: # Check for StandardPeriod variant variant = val_node.find("v8:variant", NSMAP) if variant is not None: val_str = (variant.text or "") else: val_str = raw if len(val_str) > 15: val_str = val_str[:12] + "..." # Visibility use_restrict = p.find("s:useRestriction", NSMAP) vis_str = "yes" if use_restrict is not None: ur_text = (use_restrict.text or "").strip() if ur_text == "true": vis_str = "hidden" elif ur_text == "false": vis_str = "yes" # Expression expr_node = p.find("s:expression", NSMAP) expr_str = "-" if expr_node is not None: expr_text = "".join(expr_node.itertext()).strip() if expr_text: expr_str = unescape_xml(expr_text) # availableAsField avail_field = p.find("s:availableAsField", NSMAP) avail_str = "" if avail_field is not None and (avail_field.text or "").strip() == "false": avail_str = " [noField]" name_pad = p_name.ljust(33) type_pad = p_type.ljust(22) val_pad = val_str.ljust(16) vis_pad = vis_str.ljust(8) lines.append(f" {name_pad} {type_pad} {val_pad} {vis_pad} {expr_str}{avail_str}") def show_variant(): variants = root.findall("s:settingsVariant", NSMAP) if not args.Name: # --- Variant list (map) --- if not variants: lines.append("=== Variants: (none) ===") else: lines.append(f"=== Variants ({len(variants)}) ===") for var_idx, v in enumerate(variants, 1): v_name = (v.find("dcsset:name", NSMAP).text or "") v_pres = v.find("dcsset:presentation", NSMAP) v_pres_str = "" if v_pres is not None: pt = get_ml_text(v_pres) if pt: v_pres_str = f' "{pt}"' settings = v.find("dcsset:settings", NSMAP) struct_items = [] if settings is not None: for si in settings.findall("dcsset:item", NSMAP): si_type = get_structure_item_type(si) gf = get_group_fields(si) g_str = "(" + ",".join(gf) + ")" if gf else "(detail)" struct_items.append(f"{si_type}{g_str}") if len(struct_items) > 3: from collections import Counter counted = Counter(struct_items) compact_parts = [] for name, count in counted.most_common(): if count > 1: compact_parts.append(f"{count}x {name}") else: compact_parts.append(name) struct_items = compact_parts struct_str = " " + ", ".join(struct_items) if struct_items else "" filter_count = 0 if settings is not None: filter_count = len(settings.findall("dcsset:filter/dcsset:item", NSMAP)) filter_str = f" {filter_count} filters" if filter_count > 0 else "" # Selection fields sel_fields = [] if settings is not None: sel_fields = get_selection_fields(settings) sel_str = " sel: " + ", ".join(sel_fields) if sel_fields else "" lines.append(f" [{var_idx}] {v_name}{v_pres_str}{struct_str}{filter_str}") if sel_str: lines.append(f" {sel_str}") else: # --- Variant detail --- target_variant = None match_idx = 0 for var_idx, v in enumerate(variants, 1): v_name = (v.find("dcsset:name", NSMAP).text or "") if v_name == args.Name or str(var_idx) == args.Name: target_variant = v match_idx = var_idx break if target_variant is None: print(f"Variant '{args.Name}' not found. Use -Mode variant without -Name to see list.", file=sys.stderr) sys.exit(1) v_name = (target_variant.find("dcsset:name", NSMAP).text or "") v_pres = target_variant.find("dcsset:presentation", NSMAP) v_pres_str = "" if v_pres is not None: pt = get_ml_text(v_pres) if pt: v_pres_str = f' "{pt}"' lines.append(f"=== Variant [{match_idx}]: {v_name}{v_pres_str} ===") settings = target_variant.find("dcsset:settings", NSMAP) if settings is None: lines.append(" (empty settings)") else: # Selection at settings level top_sel = get_selection_fields(settings) if top_sel: lines.append("") lines.append("Selection: " + ", ".join(top_sel)) # Structure struct_items = settings.findall("dcsset:item", NSMAP) has_struct = False for si in struct_items: si_xsi_type = si.get(f"{{{XSI_NS}}}type", "") if "StructureItem" in si_xsi_type: has_struct = True break if has_struct: lines.append("") lines.append("Structure:") for si in struct_items: si_xsi_type = si.get(f"{{{XSI_NS}}}type", "") if "StructureItem" in si_xsi_type: build_structure_tree(si, " ", False, lines) # Filter filters = get_filter_summary(settings) if filters: lines.append("") lines.append("Filter:") for f in filters: lines.append(f" {f}") # Data parameters data_params = settings.findall("dcsset:dataParameters/dcsset:item", NSMAP) if data_params: dp_strs = [] for dp in data_params: dp_param = dp.find("dcscor:parameter", NSMAP) dp_val = dp.find("dcscor:value", NSMAP) if dp_param is not None and dp_val is not None: dp_strs.append(f'{(dp_param.text or "")}="{(dp_val.text or "")}"') if dp_strs: lines.append("") lines.append("DataParams: " + ", ".join(dp_strs)) # Output parameters out_params = settings.findall("dcsset:outputParameters/dcscor:item", NSMAP) if out_params: op_strs = [] for op in out_params: op_param = op.find("dcscor:parameter", NSMAP) op_val = op.find("dcscor:value", NSMAP) if op_param is not None and op_val is not None: param_name = (op_param.text or "") param_val = (op_val.text or "") # Shorten known long names short_map = { "\u041C\u0430\u043A\u0435\u0442\u041E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F": "style", "\u0420\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u041F\u043E\u043B\u0435\u0439\u0413\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0438": "groups", "\u0413\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B\u044C\u043D\u043E\u0435\u0420\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u041E\u0431\u0449\u0438\u0445\u0418\u0442\u043E\u0433\u043E\u0432": "totalsH", "\u0412\u0435\u0440\u0442\u0438\u043A\u0430\u043B\u044C\u043D\u043E\u0435\u0420\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u041E\u0431\u0449\u0438\u0445\u0418\u0442\u043E\u0433\u043E\u0432": "totalsV", "\u0412\u044B\u0432\u043E\u0434\u0438\u0442\u044C\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A": "header", "\u0412\u044B\u0432\u043E\u0434\u0438\u0442\u044C\u041E\u0442\u0431\u043E\u0440": "filter", "\u0412\u044B\u0432\u043E\u0434\u0438\u0442\u044C\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B\u0414\u0430\u043D\u043D\u044B\u0445": "dataParams", "\u0420\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u0420\u0435\u043A\u0432\u0438\u0437\u0438\u0442\u043E\u0432": "attrs", } short = short_map.get(param_name, None) if short: op_strs.append(f"{short}={param_val}") else: op_strs.append(f"{param_name}={param_val}") if op_strs: lines.append("") lines.append("Output: " + " ".join(op_strs)) def show_trace(): if not args.Name: print("Trace mode requires -Name ", file=sys.stderr) sys.exit(1) # --- Build field index --- ds_fields = {} # dataPath -> { "datasets": [], "title": "" } calc_fields_map = {} # dataPath -> { "expression": "", "title": "" } res_fields = {} # dataPath -> [{ "expression": "", "group": "" }] title_map = {} # title -> dataPath # Scan dataset fields (including nested Union items) data_sets = root.findall("s:dataSet", NSMAP) for ds in data_sets: ds_name = (ds.find("s:name", NSMAP).text or "") ds_type = get_dataset_type(ds) for f in ds.findall("s:field", NSMAP): dp = f.find("s:dataPath", NSMAP) if dp is None: continue dp_str = (dp.text or "") if dp_str not in ds_fields: ds_fields[dp_str] = {"datasets": [], "title": ""} ds_fields[dp_str]["datasets"].append(f"{ds_name} [{ds_type}]") title_node = f.find("s:title", NSMAP) if title_node is not None: t = get_ml_text(title_node) if t: if not ds_fields[dp_str]["title"]: ds_fields[dp_str]["title"] = t if t not in title_map: title_map[t] = dp_str if ds_type == "Union": for sub_ds in ds.findall("s:item", NSMAP): sub_name = (sub_ds.find("s:name", NSMAP).text or "") sub_type = get_dataset_type(sub_ds) for f in sub_ds.findall("s:field", NSMAP): dp = f.find("s:dataPath", NSMAP) if dp is None: continue dp_str = (dp.text or "") if dp_str not in ds_fields: ds_fields[dp_str] = {"datasets": [], "title": ""} ds_fields[dp_str]["datasets"].append(f"{sub_name} [{sub_type}]") title_node = f.find("s:title", NSMAP) if title_node is not None: t = get_ml_text(title_node) if t: if not ds_fields[dp_str]["title"]: ds_fields[dp_str]["title"] = t if t not in title_map: title_map[t] = dp_str # Scan calculated fields for cf in root.findall("s:calculatedField", NSMAP): dp_str = (cf.find("s:dataPath", NSMAP).text or "") expr = "".join(cf.find("s:expression", NSMAP).itertext()) cf_title = cf.find("s:title", NSMAP) t = "" if cf_title is not None: t = get_ml_text(cf_title) calc_fields_map[dp_str] = {"expression": expr, "title": t} if t and t not in title_map: title_map[t] = dp_str # Scan resources for tf in root.findall("s:totalField", NSMAP): dp_str = (tf.find("s:dataPath", NSMAP).text or "") expr = (tf.find("s:expression", NSMAP).text or "") grp = tf.find("s:group", NSMAP) group_str = "(overall)" if grp is not None: group_str = (grp.text or "") if dp_str not in res_fields: res_fields[dp_str] = [] res_fields[dp_str].append({"expression": expr, "group": group_str}) # --- Resolve name: try dataPath, then exact title, then substring title --- target_path = args.Name known_paths = set() known_paths.update(ds_fields.keys()) known_paths.update(calc_fields_map.keys()) known_paths.update(res_fields.keys()) is_known = args.Name in known_paths if not is_known: if args.Name in title_map: target_path = title_map[args.Name] else: # Substring match in titles matched_title = None for key in title_map: if args.Name in key: matched_title = key break if matched_title: target_path = title_map[matched_title] else: print(f"Field '{args.Name}' not found by dataPath or title", file=sys.stderr) sys.exit(1) # --- Build output --- title = "" if target_path in calc_fields_map and calc_fields_map[target_path]["title"]: title = calc_fields_map[target_path]["title"] elif target_path in ds_fields and ds_fields[target_path]["title"]: title = ds_fields[target_path]["title"] title_str = f' "{title}"' if title else "" lines.append(f"=== Trace: {target_path}{title_str} ===") lines.append("") # Dataset origin if target_path in ds_fields: unique_ds = list(dict.fromkeys(ds_fields[target_path]["datasets"])) lines.append(f"Dataset: {', '.join(unique_ds)}") else: lines.append("Dataset: (schema-level only, not in dataset fields)") # Calculated field if target_path in calc_fields_map: cf = calc_fields_map[target_path] lines.append("") lines.append("Calculated:") for el in cf["expression"].split("\n"): lines.append(f" {el.rstrip()}") # Extract operands: find known field names in expression operands = [] all_known = set() all_known.update(ds_fields.keys()) all_known.update(calc_fields_map.keys()) all_known.discard(target_path) # Sort by length descending to match longer names first all_known_sorted = sorted(all_known, key=len, reverse=True) for field_name in all_known_sorted: escaped = re.escape(field_name) pattern = f"(? calculated") elif op in ds_fields: op_ds = list(dict.fromkeys(ds_fields[op]["datasets"])) lines.append(f" {op} -> {', '.join(op_ds)}") else: lines.append(f" {op}") # Resource if target_path in res_fields: lines.append("") lines.append("Resource:") for r in res_fields[target_path]: lines.append(f" [{r['group']}] {r['expression']}") # Simple dataset field, no calc/resource if target_path not in calc_fields_map and target_path not in res_fields: if target_path in ds_fields: lines.append("") lines.append("(direct dataset field, no calculated expression or resource)") def show_templates(): # --- Helper: check if expression is trivial --- def is_trivial_expr(param_name, expr): e = expr.strip() n = param_name.strip() if e == n: return True # Представление(...) if e == f"\u041F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435({n})": return True return False # --- Helper: parse template content (rows/cells) --- def get_template_content(tpl_node): inner_t = tpl_node.find("s:template", NSMAP) if inner_t is None: return {"rows": 0, "cells": [], "params": [], "nonTrivial": []} rows = inner_t.findall("dcsat:item", NSMAP) row_count = len(rows) cell_data = [] for row_idx, row in enumerate(rows, 1): row_cells = [] for cell in row.findall("dcsat:tableCell", NSMAP): field = cell.find("dcsat:item", NSMAP) if field is None: row_cells.append("(empty)") continue val = field.find("dcsat:value", NSMAP) if val is None: row_cells.append("(empty)") continue xsi_type = val.get(f"{{{XSI_NS}}}type", "") if "LocalStringType" in xsi_type: text = get_ml_text(val) if text: row_cells.append(f'"{text}"') else: row_cells.append("(empty)") elif "Parameter" in xsi_type: row_cells.append(f"{{{val.text or ''}}}") else: row_cells.append("(?)") cell_data.append({"row": row_idx, "cells": row_cells}) # Parameters param_nodes = tpl_node.findall("s:parameter", NSMAP) param_list = [] non_trivial_list = [] for p in param_nodes: pn = p.find("dcsat:name", NSMAP) pe = p.find("dcsat:expression", NSMAP) if pn is not None and pe is not None: p_name = (pn.text or "") p_expr = (pe.text or "") param_list.append({"name": p_name, "expression": p_expr}) if not is_trivial_expr(p_name, p_expr): non_trivial_list.append({"name": p_name, "expression": p_expr}) return { "rows": row_count, "cells": cell_data, "params": param_list, "nonTrivial": non_trivial_list, } # --- Build template name -> node index --- tpl_index = {} for t in root.findall("s:template", NSMAP): tn = t.find("s:name", NSMAP) if tn is not None: tpl_index[(tn.text or "")] = t # --- Parse bindings --- # Group bindings: groupTemplate + groupHeaderTemplate + groupFooterTemplate group_bindings = OrderedDict() for gt in root.findall("s:groupTemplate", NSMAP): gn = gt.find("s:groupName", NSMAP) gf = gt.find("s:groupField", NSMAP) gn_str = (gn.text or "") if gn is not None else ((gf.text or "") if gf is not None else "(default)") tt = gt.find("s:templateType", NSMAP) tn = gt.find("s:template", NSMAP) tt_str = (tt.text or "") if tt is not None else "-" tn_str = (tn.text or "") if tn is not None else "-" if gn_str not in group_bindings: group_bindings[gn_str] = [] group_bindings[gn_str].append({"type": tt_str, "tplName": tn_str}) for ght in root.findall("s:groupHeaderTemplate", NSMAP): gn = ght.find("s:groupName", NSMAP) gf = ght.find("s:groupField", NSMAP) gn_str = (gn.text or "") if gn is not None else ((gf.text or "") if gf is not None else "(default)") tn = ght.find("s:template", NSMAP) tn_str = (tn.text or "") if tn is not None else "-" if gn_str not in group_bindings: group_bindings[gn_str] = [] group_bindings[gn_str].append({"type": "GroupHeader", "tplName": tn_str}) for gft in root.findall("s:groupFooterTemplate", NSMAP): gn = gft.find("s:groupName", NSMAP) gf = gft.find("s:groupField", NSMAP) gn_str = (gn.text or "") if gn is not None else ((gf.text or "") if gf is not None else "(default)") tn = gft.find("s:template", NSMAP) tn_str = (tn.text or "") if tn is not None else "-" if gn_str not in group_bindings: group_bindings[gn_str] = [] group_bindings[gn_str].append({"type": "GroupFooter", "tplName": tn_str}) # Field bindings: fieldTemplate field_bindings = OrderedDict() field_non_trivial = [] for ft in root.findall("s:fieldTemplate", NSMAP): fn = ft.find("s:field", NSMAP) tn = ft.find("s:template", NSMAP) if fn is not None and tn is not None: f_name = (fn.text or "") t_name = (tn.text or "") field_bindings[f_name] = t_name # Check params for non-trivial expressions if t_name in tpl_index: content = get_template_content(tpl_index[t_name]) for nt in content["nonTrivial"]: field_non_trivial.append({ "field": f_name, "template": t_name, "name": nt["name"], "expression": nt["expression"], }) total_tpl = len(tpl_index) field_count = len(field_bindings) group_bind_count = sum(len(v) for v in group_bindings.values()) if not args.Name: # --- MAP mode --- lines.append(f"=== Templates ({total_tpl} defined: {field_count} field, {group_bind_count} group) ===") # Field bindings if field_bindings: lines.append("") if not field_non_trivial: field_names = list(field_bindings.keys()) if len(field_names) <= 8: lines.append(f"Field bindings ({field_count}): {', '.join(field_names)} (all trivial)") else: lines.append(f"Field bindings ({field_count}): (all trivial)") lines.append(f" {', '.join(field_names[:8])}, ...") else: unique_nt_fields = set(nt["field"] for nt in field_non_trivial) trivial_count = len(field_bindings) - len(unique_nt_fields) lines.append(f"Field bindings ({field_count}, {trivial_count} trivial):") for nt in field_non_trivial: lines.append(f" {nt['field']}: {nt['name']} = {nt['expression']}") # Group bindings if group_bindings: lines.append("") lines.append(f"Group bindings ({group_bind_count}):") for g_name, bindings in group_bindings.items(): parts = [] for b in bindings: info = f"{b['type']} -> {b['tplName']}" if b["tplName"] in tpl_index: content = get_template_content(tpl_index[b["tplName"]]) # Check if any cell has content has_content = False for r in content["cells"]: for c in r["cells"]: if c != "(empty)": has_content = True break if has_content: break info += f" ({content['rows']} rows" if content["params"]: info += f", {len(content['params'])} params" info += ")" if not has_content and not content["params"]: info += " spacer" if content["nonTrivial"]: nt_names = ", ".join(nt["name"] for nt in content["nonTrivial"]) info += f" *{nt_names}" parts.append(info) lines.append(f" {g_name}") for p in parts: lines.append(f" {p}") if field_bindings or group_bindings: lines.append("") lines.append("Use -Name for template details.") else: # --- DETAIL mode --- found = False # Check group bindings first if args.Name in group_bindings: found = True bindings = group_bindings[args.Name] lines.append(f"=== Templates: {args.Name} ===") for b in bindings: lines.append("") t_name = b["tplName"] if t_name not in tpl_index: lines.append(f"{b['type']} -> {t_name} (template not found)") continue content = get_template_content(tpl_index[t_name]) cell_count = sum(len(r["cells"]) for r in content["cells"]) lines.append(f"{b['type']} -> {t_name} [{content['rows']} rows, {cell_count} cells]:") for r in content["cells"]: cell_str = " | ".join(r["cells"]) lines.append(f" Row {r['row']}: {cell_str}") if content["nonTrivial"]: lines.append(" Params:") for nt in content["nonTrivial"]: lines.append(f" {nt['name']} = {nt['expression']}") # Check field bindings if args.Name in field_bindings: if found: lines.append("") found = True t_name = field_bindings[args.Name] lines.append(f"=== Field template: {args.Name} -> {t_name} ===") if t_name in tpl_index: content = get_template_content(tpl_index[t_name]) cell_count = sum(len(r["cells"]) for r in content["cells"]) lines.append(f"[{content['rows']} rows, {cell_count} cells]") for r in content["cells"]: cell_str = " | ".join(r["cells"]) lines.append(f" Row {r['row']}: {cell_str}") if content["nonTrivial"]: lines.append(" Non-trivial params:") for nt in content["nonTrivial"]: lines.append(f" {nt['name']} = {nt['expression']}") else: lines.append(" (all params trivial)") if not found: print(f"Group or field '{args.Name}' not found in template bindings", file=sys.stderr) sys.exit(1) # === Execute mode === mode = args.Mode if mode == "overview": show_overview() show_overview_hints() elif mode == "query": show_query() elif mode == "fields": show_fields() elif mode == "links": show_links() elif mode == "calculated": show_calculated() elif mode == "resources": show_resources() elif mode == "params": show_params() elif mode == "variant": show_variant() elif mode == "trace": show_trace() elif mode == "templates": show_templates() elif mode == "full": show_overview() lines.append("") lines.append("--- query ---") lines.append("") if root.findall(".//s:dataSet[@xsi:type='DataSetQuery']", NSMAP): show_query() else: obj_names = [n.text for n in root.findall(".//s:dataSet[@xsi:type='DataSetObject']/s:objectName", NSMAP) if n.text] if obj_names: lines.append(f"(no query datasets; external datasets: {', '.join(obj_names)})") else: lines.append("(no query datasets)") lines.append("") lines.append("--- fields ---") lines.append("") show_fields() lines.append("") lines.append("--- resources ---") lines.append("") show_resources() lines.append("") lines.append("--- params ---") lines.append("") show_params() lines.append("") lines.append("--- variant ---") lines.append("") show_variant() # --- Output --- result = lines total_lines = len(result) # OutFile if args.OutFile: out_path = args.OutFile if not os.path.isabs(out_path): out_path = os.path.join(os.getcwd(), out_path) with open(out_path, "w", encoding="utf-8-sig") as fh: fh.write("\n".join(result)) print(f"Written {total_lines} lines to {args.OutFile}") sys.exit(0) # Pagination if args.Offset > 0: if args.Offset >= total_lines: print(f"[INFO] Offset {args.Offset} exceeds total lines ({total_lines}). Nothing to show.") sys.exit(0) result = result[args.Offset:] if not args.Raw and len(result) > args.Limit: shown = result[:args.Limit] for line in shown: print(line) print("") print(f"[TRUNCATED] Shown {args.Limit} of {total_lines} lines. Use -Offset {args.Offset + args.Limit} to continue.") else: for line in result: print(line) if __name__ == "__main__": main()