@dforge-core/dforge-mcp 0.2.44 → 0.2.46-next

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/dist/server.js +109 -84
  4. package/package.json +48 -46
  5. package/resources/docs/conventions.md +17 -14
  6. package/resources/docs/dsl-reference.md +15 -5
  7. package/resources/schemas/entity.schema.json +1 -1
  8. package/resources/schemas/registers.schema.json +8 -3
  9. package/scripts/install-skills.mjs +1 -0
  10. package/skills/dforge-1c-import/SKILL.md +374 -0
  11. package/skills/dforge-1c-import/references/environment.md +97 -0
  12. package/skills/dforge-1c-import/references/loading.md +161 -0
  13. package/skills/dforge-1c-import/references/measurements.md +210 -0
  14. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  15. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  16. package/skills/dforge-1c-import/scripts/README.md +110 -0
  17. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  18. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  19. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  20. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  21. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  22. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  23. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  24. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  25. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  26. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  27. package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
  28. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  29. package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
  30. package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
  31. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  32. package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
  33. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  34. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  35. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  36. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  37. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  38. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  39. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  40. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  41. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  42. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  43. package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
  44. package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
  45. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  46. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
  49. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  50. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  51. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  52. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  53. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  54. package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
  55. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  56. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  57. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  58. package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  61. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  62. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  63. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  64. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  65. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  66. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  67. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  68. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  69. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  70. package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
  71. package/skills/dforge-mcp-author/SKILL.md +8 -2
  72. package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
  73. package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
  74. package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
  75. package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
  76. package/skills/dforge-mcp-author/references/conventions.md +7 -1
  77. package/skills/dforge-mcp-author/references/field-types.md +3 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +5 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +5 -2
  80. package/skills/dforge-mcp-author/references/queries.md +1 -1
  81. package/skills/dforge-mcp-author/references/registers.md +18 -3
  82. package/skills/dforge-mcp-author/references/reports.md +4 -1
  83. package/skills/dforge-mcp-author/references/schema-import.md +1 -1
  84. package/skills/dforge-mcp-author/references/security.md +1 -1
  85. package/skills/dforge-mcp-author/references/translations.md +38 -15
  86. package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
  87. package/skills/dforge-module-build/SKILL.md +10 -1
  88. package/skills/dforge-module-ship/SKILL.md +31 -3
  89. package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Module dependency graph, and whether it can be installed.
5
+
6
+ dForge installs modules in dependency order, so a cycle between two modules is not a
7
+ style problem - it is a package that cannot be installed at all. A cycle is broken with
8
+ an extension or a bridge module (see MODULE_CONVENTIONS.md), but first it has to be seen.
9
+
10
+ python tools/deps.py
11
+ """
12
+ from __future__ import print_function
13
+
14
+ import codecs
15
+ import collections
16
+ import json
17
+ import os
18
+
19
+ import paths
20
+
21
+ ROOT = paths.ROOT
22
+ MODULES = paths.MODULES
23
+
24
+
25
+ def load(path):
26
+ with codecs.open(path, "r", "utf-8") as f:
27
+ return json.load(f)
28
+
29
+
30
+ def graph():
31
+ out = collections.OrderedDict()
32
+ for code in sorted(os.listdir(MODULES)):
33
+ manifest = os.path.join(MODULES, code, "manifest.json")
34
+ if os.path.isfile(manifest):
35
+ out[code] = set(load(manifest).get("dependencies", {}))
36
+ return out
37
+
38
+
39
+ def referenced(code):
40
+ """Modules this one actually points at: `extends`, and every `entity` in a link,
41
+ reference or set. A dependency the manifest forgets to declare still binds the
42
+ install order - masterdata carried a lookup into shipping while declaring nothing,
43
+ and the pair was uninstallable in either order."""
44
+ out = set()
45
+
46
+ def walk(node):
47
+ if isinstance(node, dict):
48
+ for key, value in node.items():
49
+ if key in ("entity", "extends") and isinstance(value, str) and "." in value:
50
+ out.add(value.split(".", 1)[0])
51
+ else:
52
+ walk(value)
53
+ elif isinstance(node, list):
54
+ for item in node:
55
+ walk(item)
56
+
57
+ directory = os.path.join(MODULES, code, "entities")
58
+ if os.path.isdir(directory):
59
+ for name in sorted(os.listdir(directory)):
60
+ walk(load(os.path.join(directory, name)))
61
+ return out - set([code])
62
+
63
+
64
+ def cycles(edges):
65
+ """Every cycle reachable in the graph, as a list of module names."""
66
+ found, path, state = [], [], {}
67
+
68
+ def walk(node):
69
+ state[node] = "open"
70
+ path.append(node)
71
+ for target in sorted(edges.get(node, ())):
72
+ if target not in edges:
73
+ continue
74
+ if state.get(target) == "open":
75
+ found.append(path[path.index(target):] + [target])
76
+ elif state.get(target) != "done":
77
+ walk(target)
78
+ path.pop()
79
+ state[node] = "done"
80
+
81
+ for node in edges:
82
+ if state.get(node) != "done":
83
+ walk(node)
84
+ return found
85
+
86
+
87
+ def order(edges):
88
+ """Install order: a module after everything it depends on."""
89
+ done, out = set(), []
90
+ remaining = dict((k, set(v) & set(edges)) for k, v in edges.items())
91
+ while remaining:
92
+ ready = sorted(k for k, v in remaining.items() if v <= done)
93
+ if not ready:
94
+ return out, sorted(remaining)
95
+ for code in ready:
96
+ out.append(code)
97
+ done.add(code)
98
+ del remaining[code]
99
+ return out, []
100
+
101
+
102
+ def main():
103
+ edges = graph()
104
+ print("%-12s %s" % ("module", "depends on"))
105
+ print("-" * 60)
106
+ undeclared = collections.OrderedDict()
107
+ for code, deps in edges.items():
108
+ missing = sorted(referenced(code) - deps - set([code]))
109
+ if missing:
110
+ undeclared[code] = missing
111
+ deps.update(missing)
112
+ print("%-12s %s%s" % (code, ", ".join(sorted(deps)) or "-",
113
+ " (undeclared: %s)" % ", ".join(missing) if missing else ""))
114
+
115
+ if undeclared:
116
+ print("\n%d module(s) reference a module they do not declare - add the"
117
+ " dependency, or move the reference to the other side:" % len(undeclared))
118
+ for code, missing in undeclared.items():
119
+ print(" %s -> %s" % (code, ", ".join(missing)))
120
+
121
+ found = cycles(edges)
122
+ if found:
123
+ print("\n%d cycle(s) - these cannot be installed as they stand:" % len(found))
124
+ for cycle in found:
125
+ print(" %s" % " -> ".join(cycle))
126
+ else:
127
+ print("\nno cycles")
128
+
129
+ sequence, stuck = order(edges)
130
+ print("\ninstall order:\n %s" % " -> ".join(sequence))
131
+ if stuck:
132
+ print("stuck: %s" % ", ".join(stuck))
133
+
134
+
135
+ if __name__ == "__main__":
136
+ main()
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """How much each document type is used: last date, last twelve months, posted.
4
+
5
+ Usage does not narrow what gets ported - a document nobody has posted in years is still
6
+ done - but it orders the work, and it says which types have a history to prove a port on.
7
+ A type with no posted documents has nothing for the oracle to compare against.
8
+
9
+ The twelve months are counted back from the newest document in the infobase, not from
10
+ today: an extraction is a snapshot, and "the last year" should mean the last year the
11
+ client actually worked in it.
12
+
13
+ python tools/1c.py docusage # the table, and out/docusage.json
14
+
15
+ Read-only. The physical table comes from `DBNames` by the object's GUID, never guessed.
16
+ """
17
+ from __future__ import print_function
18
+
19
+ import codecs
20
+ import io
21
+ import json
22
+ import os
23
+ import sys
24
+
25
+ import handlers
26
+ import meta
27
+ import migrate
28
+ import paths
29
+
30
+ TARGET = os.path.join(paths.OUT, "docusage.json")
31
+
32
+
33
+ def documents():
34
+ """(name, physical table) for every document in the configuration."""
35
+ with io.open(os.path.join(paths.OUT, "objects.json"), encoding="utf-8") as f:
36
+ objects = json.load(f)
37
+ index = meta.dbnames_index()
38
+ found = []
39
+ for obj in objects:
40
+ if obj.get("family") != "Document":
41
+ continue
42
+ entry = index.get(obj["guid"])
43
+ if entry and entry["type"] == "Document":
44
+ found.append((obj["name"], entry["physical"]))
45
+ return sorted(found)
46
+
47
+
48
+ def measure(cur, table):
49
+ cur.execute("SELECT COUNT(*), MAX(_Date_Time) FROM dbo.[%s]" % table)
50
+ rows, last = cur.fetchone()
51
+ return rows, last
52
+
53
+
54
+ def main():
55
+ migrate.load_env()
56
+ cur = migrate.mssql_connect().cursor()
57
+ docs = documents()
58
+ newest = None
59
+ raw = []
60
+ for name, table in docs:
61
+ rows, last = measure(cur, table)
62
+ raw.append((name, table, rows, last))
63
+ if last is not None and (newest is None or last > newest):
64
+ newest = last
65
+ cutoff = newest.replace(year=newest.year - 1) if newest else None
66
+ result = []
67
+ for name, table, rows, last in raw:
68
+ recent = posted = 0
69
+ if rows:
70
+ cur.execute("SELECT SUM(CASE WHEN _Date_Time >= ? THEN 1 ELSE 0 END),"
71
+ " SUM(CASE WHEN _Posted = 0x01 THEN 1 ELSE 0 END) FROM dbo.[%s]"
72
+ % table, cutoff)
73
+ recent, posted = [int(v or 0) for v in cur.fetchone()]
74
+ result.append({
75
+ "name": name,
76
+ "table": table,
77
+ "client": name.startswith(handlers.CLIENT),
78
+ "rows": rows,
79
+ "posted": posted,
80
+ "last12m": recent,
81
+ # 1C stores dates shifted by the year offset; the snapshot keeps them as stored.
82
+ "last": last.strftime("%Y-%m-%d") if last else None,
83
+ })
84
+ result.sort(key=lambda r: (-r["last12m"], -r["posted"], r["name"]))
85
+ with codecs.open(TARGET, "w", "utf-8") as f:
86
+ json.dump({"newest": newest.strftime("%Y-%m-%d") if newest else None,
87
+ "cutoff": cutoff.strftime("%Y-%m-%d") if cutoff else None,
88
+ "documents": result}, f, ensure_ascii=False, indent="\t")
89
+ out = codecs.getwriter("utf-8")(sys.stdout.buffer)
90
+ vendor = [r for r in result if not r["client"]]
91
+ out.write("%d document types, %d vendor; newest document %s, last 12 months from %s\n"
92
+ % (len(result), len(vendor), newest, cutoff))
93
+ out.write("vendor with rows %d, posted %d, used in last 12m %d\n\n" % (
94
+ sum(1 for r in vendor if r["rows"]), sum(1 for r in vendor if r["posted"]),
95
+ sum(1 for r in vendor if r["last12m"])))
96
+ for r in result:
97
+ if r["rows"]:
98
+ out.write("%-6s %-52s %9d %9d %8d %s\n" % (
99
+ "client" if r["client"] else "", r["name"], r["rows"], r["posted"],
100
+ r["last12m"], r["last"]))
101
+ out.write("\nwritten %s\n" % TARGET)
102
+
103
+
104
+ if __name__ == "__main__":
105
+ main()
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Read a 1C:Enterprise 8 infobase hosted on MS SQL Server *without* any 1C software.
5
+
6
+ Everything needed to rebuild the data model lives in two service tables:
7
+ Params -> row FileName='DBNames': GUID -> physical table/column name map
8
+ Config -> the configuration itself, one row (8.2) or several PartNo rows (8.3) per file
9
+
10
+ File bodies are raw-deflate compressed (no zlib header).
11
+
12
+ Credentials come from env DF1C_USER / DF1C_PASS; without them, Windows auth is used.
13
+ Server and database default to DF1C_SERVER / DF1C_DB.
14
+ """
15
+ import argparse
16
+ import codecs
17
+ import json
18
+ import os
19
+ import re
20
+ import struct
21
+ import sys
22
+ import zlib
23
+
24
+ import paths
25
+
26
+ OUT = paths.OUT
27
+
28
+ PREFERRED_DRIVERS = (
29
+ "ODBC Driver 18 for SQL Server",
30
+ "ODBC Driver 17 for SQL Server",
31
+ "ODBC Driver 13 for SQL Server",
32
+ "SQL Server Native Client 11.0",
33
+ "SQL Server",
34
+ )
35
+
36
+
37
+ def connect(server, db):
38
+ import pyodbc
39
+ available = set(pyodbc.drivers())
40
+ driver = next((d for d in PREFERRED_DRIVERS if d in available), None)
41
+ if driver is None:
42
+ raise RuntimeError("no SQL Server ODBC driver found: %r" % sorted(available))
43
+ parts = ["DRIVER={%s}" % driver, "SERVER=%s" % server, "DATABASE=%s" % db,
44
+ "TrustServerCertificate=yes"]
45
+ user, pwd = os.environ.get("DF1C_USER"), os.environ.get("DF1C_PASS")
46
+ if user:
47
+ parts += ["UID=%s" % user, "PWD=%s" % pwd]
48
+ else:
49
+ parts += ["Trusted_Connection=yes"]
50
+ sys.stderr.write("[i] %s / %s via %s\n" % (server, db, driver))
51
+ return pyodbc.connect(";".join(parts), autocommit=True)
52
+
53
+
54
+ def inflate(data):
55
+ """1C stores file bodies as raw deflate; a few small ones are stored plain."""
56
+ if not data:
57
+ return b""
58
+ data = bytes(data)
59
+ for attempt in (lambda: zlib.decompressobj(-15).decompress(data), lambda: zlib.decompress(data)):
60
+ try:
61
+ return attempt()
62
+ except zlib.error:
63
+ continue
64
+ return data
65
+
66
+
67
+ _HAS_PARTNO = {}
68
+
69
+
70
+ def has_partno(cur, table):
71
+ """8.3 splits large files across rows by PartNo; 8.2 stores one row per file."""
72
+ if table not in _HAS_PARTNO:
73
+ cur.execute("SELECT COUNT(*) FROM sys.columns WHERE object_id = OBJECT_ID(?) AND name = 'PartNo'", table)
74
+ _HAS_PARTNO[table] = cur.fetchone()[0] > 0
75
+ return _HAS_PARTNO[table]
76
+
77
+
78
+ def read_file(cur, table, name):
79
+ """Concatenate a file's parts in order, then inflate."""
80
+ order = " ORDER BY PartNo" if has_partno(cur, table) else ""
81
+ cur.execute("SELECT BinaryData FROM %s WHERE FileName = ?%s" % (table, order), name)
82
+ blob = b"".join(bytes(r[0]) for r in cur.fetchall() if r[0] is not None)
83
+ return inflate(blob)
84
+
85
+
86
+ # --------------------------------------------------------------------------- DBNames
87
+
88
+ DBNAME_ENTRY = re.compile(r'\{([0-9a-fA-F-]{36}),"([^"]*)",(\d+)\}')
89
+
90
+
91
+ def parse_dbnames(buf):
92
+ """8.2 writes DBNames as UTF-8 text: {ver, {count, {guid,"Kind",N}, ...}}."""
93
+ if buf.startswith(codecs.BOM_UTF8) or buf[:1] == b"{":
94
+ txt = buf.decode("utf-8-sig", "replace")
95
+ entries = [{"guid": g, "type": k, "number": int(n)} for g, k, n in DBNAME_ENTRY.findall(txt)]
96
+ if entries:
97
+ return entries
98
+ raise ValueError("text DBNames matched no entries")
99
+ return parse_dbnames_binary(buf)
100
+
101
+
102
+ def parse_dbnames_binary(buf):
103
+ """8.3 layout: <u32 count> then count * { <u32 chars> utf16le, <u32 chars> utf16le, <u32 n> }"""
104
+ out, pos = [], 0
105
+ (count,) = struct.unpack_from("<I", buf, pos)
106
+ pos += 4
107
+ if count == 0 or count > 500000:
108
+ raise ValueError("implausible entry count %d" % count)
109
+ for _ in range(count):
110
+ vals = []
111
+ for _ in range(2):
112
+ (ln,) = struct.unpack_from("<I", buf, pos)
113
+ pos += 4
114
+ vals.append(buf[pos:pos + ln * 2].decode("utf-16-le"))
115
+ pos += ln * 2
116
+ (num,) = struct.unpack_from("<I", buf, pos)
117
+ pos += 4
118
+ out.append({"guid": vals[0], "type": vals[1], "number": num})
119
+ return out
120
+
121
+
122
+ def physical_name(entry):
123
+ """DBNames kind + number -> the SQL object name 1C generated for it."""
124
+ kind, number = entry.get("type"), entry.get("number")
125
+ if not kind or number is None:
126
+ return None
127
+ return "_%s%d" % (kind, number)
128
+
129
+
130
+ # --------------------------------------------------------------------------- commands
131
+
132
+ def cmd_databases(cur, args):
133
+ """Which databases does this login actually see? Separates a bad password from missing rights."""
134
+ cur.execute("SELECT name, state_desc, HAS_DBACCESS(name) FROM sys.databases ORDER BY name")
135
+ for name, state, access in cur.fetchall():
136
+ print("%-44s %-10s access=%s" % (name, state, access))
137
+
138
+
139
+ def cmd_probe(cur, args):
140
+ cur.execute("SELECT @@VERSION")
141
+ print(cur.fetchone()[0].splitlines()[0])
142
+ for table in ("Config", "ConfigSave", "Params", "DBSchema", "_YearOffset"):
143
+ try:
144
+ cur.execute("SELECT COUNT(*) FROM [%s]" % table)
145
+ print(" %-12s rows=%s" % (table, cur.fetchone()[0]))
146
+ except Exception as exc:
147
+ print(" %-12s -- %s" % (table, str(exc).split("]")[-1].strip()[:70]))
148
+ cur.execute("""SELECT LEFT(name, PATINDEX('%[0-9]%', name + '0') - 1) AS kind, COUNT(*)
149
+ FROM sys.tables WHERE name LIKE '[_]%'
150
+ GROUP BY LEFT(name, PATINDEX('%[0-9]%', name + '0') - 1)
151
+ ORDER BY 2 DESC""")
152
+ print("\n physical table families:")
153
+ for kind, n in cur.fetchall():
154
+ print(" %-24s %d" % (kind, n))
155
+
156
+
157
+ def cmd_schema(cur, args):
158
+ cur.execute("""SELECT t.name, c.name, ty.name, c.max_length, c.precision, c.scale, c.is_nullable
159
+ FROM sys.tables t
160
+ JOIN sys.columns c ON c.object_id = t.object_id
161
+ JOIN sys.types ty ON ty.user_type_id = c.user_type_id
162
+ ORDER BY t.name, c.column_id""")
163
+ tables = {}
164
+ for tn, cn, ty, ml, pr, sc, nullable in cur.fetchall():
165
+ tables.setdefault(tn, []).append({
166
+ "column": cn, "type": ty, "len": ml,
167
+ "precision": pr, "scale": sc, "nullable": bool(nullable),
168
+ })
169
+ cur.execute("""SELECT t.name, SUM(p.rows) FROM sys.tables t
170
+ JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
171
+ GROUP BY t.name""")
172
+ rows = dict(cur.fetchall())
173
+ write_json("schema.json", {"tables": [
174
+ {"name": name, "rows": rows.get(name), "columns": cols}
175
+ for name, cols in sorted(tables.items())
176
+ ]})
177
+ print("tables=%d" % len(tables))
178
+
179
+
180
+ def cmd_dbnames(cur, args):
181
+ buf = read_file(cur, "Params", "DBNames")
182
+ save("dbnames.bin", buf)
183
+ entries = parse_dbnames(buf)
184
+ for entry in entries:
185
+ entry["physical"] = physical_name(entry)
186
+ write_json("dbnames.json", {"count": len(entries), "entries": entries})
187
+ kinds = {}
188
+ for entry in entries:
189
+ kinds[entry["type"]] = kinds.get(entry["type"], 0) + 1
190
+ print("entries=%d distinct kinds=%d" % (len(entries), len(kinds)))
191
+ for kind, n in sorted(kinds.items(), key=lambda kv: -kv[1]):
192
+ print(" %-28s %d" % (kind, n))
193
+
194
+
195
+ def cmd_extract(cur, args):
196
+ target = os.path.join(OUT, "config")
197
+ if not os.path.isdir(target):
198
+ os.makedirs(target)
199
+ cur.execute("SELECT DISTINCT FileName FROM Config")
200
+ names = [r[0] for r in cur.fetchall()]
201
+ text = binary = 0
202
+ for i, name in enumerate(names):
203
+ buf = read_file(cur, "Config", name)
204
+ safe = re.sub(r"[^0-9A-Za-z._-]", "_", name)[:120]
205
+ if buf.startswith(codecs.BOM_UTF8) or b"{" in buf[:64]:
206
+ suffix, text = ".txt", text + 1
207
+ else:
208
+ suffix, binary = ".bin", binary + 1
209
+ with open(os.path.join(target, safe + suffix), "wb") as f:
210
+ f.write(buf)
211
+ if i and i % 2000 == 0:
212
+ sys.stderr.write("[i] %d/%d\n" % (i, len(names)))
213
+ print("files=%d text=%d binary=%d -> %s" % (len(names), text, binary, target))
214
+
215
+
216
+ def save(name, data):
217
+ if not os.path.isdir(OUT):
218
+ os.makedirs(OUT)
219
+ with open(os.path.join(OUT, name), "wb") as f:
220
+ f.write(data)
221
+
222
+
223
+ def write_json(name, doc):
224
+ if not os.path.isdir(OUT):
225
+ os.makedirs(OUT)
226
+ with codecs.open(os.path.join(OUT, name), "w", "utf-8") as f:
227
+ json.dump(doc, f, ensure_ascii=False, indent="\t")
228
+
229
+
230
+ COMMANDS = {
231
+ "databases": cmd_databases,
232
+ "probe": cmd_probe,
233
+ "schema": cmd_schema,
234
+ "dbnames": cmd_dbnames,
235
+ "extract": cmd_extract,
236
+ }
237
+
238
+
239
+ def main():
240
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
241
+ ap.add_argument("command", choices=sorted(COMMANDS))
242
+ ap.add_argument("--server", default=os.environ.get("DF1C_SERVER")
243
+ or paths.required("DF1C_SERVER", "which MS SQL server holds it"))
244
+ ap.add_argument("--db", default=os.environ.get("DF1C_DB")
245
+ or paths.required("DF1C_DB", "which MS SQL infobase to read"))
246
+ args = ap.parse_args()
247
+ COMMANDS[args.command](connect(args.server, args.db).cursor(), args)
248
+
249
+
250
+ if __name__ == "__main__":
251
+ main()
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """Dump one metadata object's parse tree so its attribute layout can be read off."""
4
+ from __future__ import print_function
5
+
6
+ import codecs
7
+ import json
8
+ import os
9
+ import sys
10
+
11
+ import meta
12
+
13
+ OUT = meta.OUT
14
+
15
+
16
+ def find(name):
17
+ with codecs.open(os.path.join(OUT, "objects.json"), "r", "utf-8") as f:
18
+ for row in json.load(f):
19
+ if row.get("name") == name:
20
+ return row["guid"]
21
+ return None
22
+
23
+
24
+ def shape(node, depth, max_depth, path):
25
+ pad = " " * depth
26
+ if isinstance(node, list):
27
+ strings = [x for x in node if isinstance(x, str) and x and not meta.GUID.match(x)
28
+ and not x.isdigit()]
29
+ hint = (" <- " + ", ".join(repr(s)[:34] for s in strings[:4])) if strings else ""
30
+ print("%s%s list[%d]%s" % (pad, path, len(node), hint))
31
+ if depth >= max_depth:
32
+ return
33
+ for i, item in enumerate(node):
34
+ if isinstance(item, list):
35
+ shape(item, depth + 1, max_depth, "[%d]" % i)
36
+ else:
37
+ if node:
38
+ print("%s%s = %s" % (pad, path, repr(node)[:70]))
39
+
40
+
41
+ if __name__ == "__main__":
42
+ name = sys.argv[1]
43
+ depth = int(sys.argv[2]) if len(sys.argv) > 2 else 4
44
+ guid = find(name)
45
+ print("%s -> %s\n" % (name, guid))
46
+ node = meta.parse(meta.load(guid))
47
+ label = "root"
48
+ if len(sys.argv) > 3: # drill into e.g. 6.2
49
+ for part in sys.argv[3].split("."):
50
+ node = node[int(part)]
51
+ label = "root." + sys.argv[3]
52
+ if "--json" in sys.argv:
53
+ print(json.dumps(node, ensure_ascii=False, indent=1))
54
+ else:
55
+ shape(node, 0, depth, label)