@dforge-core/dforge-mcp 0.2.45 → 0.2.47
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.
- package/CHANGELOG.md +45 -0
- package/README.md +2 -1
- package/dist/server.js +71 -3
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +1 -1
- package/resources/schemas/data-views.schema.json +50 -0
- package/scripts/install-skills.mjs +1 -0
- package/skills/dforge-1c-import/SKILL.md +375 -0
- package/skills/dforge-1c-import/references/environment.md +98 -0
- package/skills/dforge-1c-import/references/loading.md +164 -0
- package/skills/dforge-1c-import/references/measurements.md +210 -0
- package/skills/dforge-1c-import/references/print-forms.md +148 -0
- package/skills/dforge-1c-import/scripts/1c.py +178 -0
- package/skills/dforge-1c-import/scripts/README.md +164 -0
- package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
- package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
- package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
- package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
- package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
- package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
- package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
- package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
- package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
- package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +770 -0
- package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
- package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
- package/skills/dforge-1c-import/scripts/lib/external.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
- package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
- package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
- package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
- package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
- package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
- package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
- package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
- package/skills/dforge-1c-import/scripts/lib/meta.py +325 -0
- package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -0
- package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
- package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
- package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +68 -0
- package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
- package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
- package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
- package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
- package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
- package/skills/dforge-1c-import/scripts/lib/recorders.py +91 -0
- package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
- package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
- package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
- package/skills/dforge-1c-import/scripts/lib/seedstate.py +493 -0
- package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
- package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
- package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
- package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
- package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
- package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
- package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
- package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
- package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
- package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
- package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
- package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
- package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
- package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
- package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
- package/skills/dforge-mcp-author/SKILL.md +5 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
- package/skills/dforge-mcp-author/references/data-views.md +38 -2
- package/skills/dforge-mcp-author/references/formulas.md +32 -1
- package/skills/dforge-mcp-author/references/print-templates.md +30 -5
- package/skills/dforge-mcp-author/references/queries.md +17 -2
- package/skills/dforge-mcp-author/references/registers.md +1 -0
- package/skills/dforge-mcp-author/references/reports.md +1 -1
- package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# The 1C porting toolkit
|
|
2
|
+
|
|
3
|
+
A Python library that reads a 1C:Enterprise 8 infobase, either a server infobase on MS SQL
|
|
4
|
+
Server or a file infobase (`1Cv8.1CD`). It needs no 1C software and no licence. From the
|
|
5
|
+
infobase it:
|
|
6
|
+
|
|
7
|
+
- generates dForge modules;
|
|
8
|
+
- copies the rows;
|
|
9
|
+
- checks the result.
|
|
10
|
+
|
|
11
|
+
It was built on a real port: an УТП-based configuration, 16 contours, 30 M+ rows,
|
|
12
|
+
reconciled to the row and the kopeck. It is meant to be pointed at the next infobase
|
|
13
|
+
unchanged.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
scripts/
|
|
17
|
+
1c.py the one entry point: python 1c.py <command> [args]
|
|
18
|
+
lib/ the library: 1C storage, metadata, generation, loading, checks
|
|
19
|
+
project-template/ what a new port starts its project package from
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Two layers: export, then everything else
|
|
23
|
+
|
|
24
|
+
Only one step reads 1C itself. `dump1c` exports the infobase to `out/`, and every later
|
|
25
|
+
step reads that export, never the infobase.
|
|
26
|
+
|
|
27
|
+
| layer | modules | reads | writes |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| export | `dump1c` · `source_mssql` · `source_file` · `onecd` | the infobase: MS SQL (`DF1C_SERVER`/`DF1C_DB`) or a `.1CD` file (`DF1C_FILE`) | `out/config/`, `dbnames.json`, `schema.json`, `data/<table>.parquet`, `export.json` |
|
|
30
|
+
| everything else | `store` and every other module | `out/`. Rows are queried with DuckDB through `store.connect()` | modules, the tenant, reports |
|
|
31
|
+
|
|
32
|
+
The export looks the same whichever kind of infobase it came from:
|
|
33
|
+
|
|
34
|
+
- names are spelt the MS SQL way (`_Reference12`, `_Fld345RRef`). A file infobase keeps
|
|
35
|
+
them in capitals, and DBNames supplies the spelling back;
|
|
36
|
+
- the schema uses MS SQL type names (`binary(16)`, `nvarchar`, `numeric`, `datetime`);
|
|
37
|
+
- booleans are `binary(1)`.
|
|
38
|
+
|
|
39
|
+
Rows are kept as 1C stores them. `export.json` records the infobase's year offset: usually
|
|
40
|
+
2000 on MS SQL, always 0 in a file infobase. It also records the horizon (`MIN_ACTIVE_ROWVERSION()`)
|
|
41
|
+
taken before each table was read.
|
|
42
|
+
|
|
43
|
+
- **SQL over the export is DuckDB's.** That includes a project's `sources.QUERIES`. Quote
|
|
44
|
+
names with double quotes (case does not matter), use `LIMIT` instead of `TOP`, write
|
|
45
|
+
a binary constant as `from_hex('01')` instead of `0x01`, and use `CAST` instead of
|
|
46
|
+
`CONVERT`. A port whose queries were written in T-SQL has to rewrite them.
|
|
47
|
+
- **`dump1c data --changes` reads only what 1C changed** since the export (or `--since
|
|
48
|
+
<day>`). It works table by table:
|
|
49
|
+
- objects are re-read by `_Version` above the mark, and deletions are found by key;
|
|
50
|
+
- tabular parts are re-read through their changed owners;
|
|
51
|
+
- register movements are re-read through their changed recorders;
|
|
52
|
+
- anything else is re-read whole.
|
|
53
|
+
|
|
54
|
+
It rewrites each table's file to what a full export would hold. On an infobase of 2.7 M
|
|
55
|
+
objects, it reads the keys in a few seconds instead of a full export of 1.5 h.
|
|
56
|
+
|
|
57
|
+
On an accounting register it also compares each recorder's count of inactive entries.
|
|
58
|
+
Switching entries on or off ("Включить/выключить проводки") rewrites them without
|
|
59
|
+
writing the document, so `_Version` does not show it.
|
|
60
|
+
- **Totals and document journals are left out by default** (`_AccumRgT`, `_AccumRgTn`,
|
|
61
|
+
`_AccRgAT`, `_AccRgCT`, `_DocumentJournal`). 1C derives them from other tables, they
|
|
62
|
+
have no `_Version`, and `--changes` would read them in full on every run: 53.7 M of
|
|
63
|
+
313 M rows. Name one, or pass `--derived`, to export it. `seedstate verify` says which
|
|
64
|
+
totals table it needs.
|
|
65
|
+
- **`delta` needs an MS SQL export.** In a file infobase `_Version` is a counter per table,
|
|
66
|
+
not a rowversion for the whole database, so a file export has no horizon. Re-export it
|
|
67
|
+
and reload with `migrate run <module> --truncate`.
|
|
68
|
+
- **Read a file infobase while 1C is not writing to it.** Copy the `.1CD` first, or close
|
|
69
|
+
the infobase.
|
|
70
|
+
|
|
71
|
+
## Library and project
|
|
72
|
+
|
|
73
|
+
- **`lib/` knows 1C and dForge**: how a configuration is stored, how its metadata
|
|
74
|
+
parses, what the physical tables mean, and how rows are copied and checked.
|
|
75
|
+
- **The project knows this infobase**:
|
|
76
|
+
- which object goes to which module (`contours.py`);
|
|
77
|
+
- which 1C column feeds an authored column (`sources.py`);
|
|
78
|
+
- the integrator's prefixes;
|
|
79
|
+
- the hand-written modules (`authored/<module>/`).
|
|
80
|
+
|
|
81
|
+
`lib/binding.py` lists everything a project may provide. Only `contours` and `sources`
|
|
82
|
+
are required.
|
|
83
|
+
|
|
84
|
+
**Never put project facts in `lib/`.** That covers infobase names, tenant names, the
|
|
85
|
+
company's abbreviations and prefixes. `python 1c.py --boundary` checks that no library
|
|
86
|
+
module imports a project module or names the project's world.
|
|
87
|
+
|
|
88
|
+
**Never hand-edit generated output.** Put the change in the project, either as a spec
|
|
89
|
+
override or as an authored file under `authored/<module>/` that `genmodule` merges.
|
|
90
|
+
Then regenerate. If a port keeps needing hand edits, the generator is wrong.
|
|
91
|
+
|
|
92
|
+
## Setup
|
|
93
|
+
|
|
94
|
+
- Python 3.7+.
|
|
95
|
+
- `pyarrow` and `duckdb`, to write and read the export (on 3.7: `pyarrow==12.0.1`,
|
|
96
|
+
`duckdb==1.1.3`).
|
|
97
|
+
- `pyodbc` and an ODBC driver for SQL Server (driver 17 or 18), only to export a server
|
|
98
|
+
infobase. A file infobase needs nothing more.
|
|
99
|
+
- `psycopg2`, to load and check the tenant.
|
|
100
|
+
- `jsonschema`, for `validate`.
|
|
101
|
+
- Node with `liquidjs` in `PRINT_NODE_DIR`, only for `printgen preview`.
|
|
102
|
+
|
|
103
|
+
On Windows, set `PYTHONIOENCODING=utf-8`.
|
|
104
|
+
|
|
105
|
+
Lay out a port directory, copy `project-template/.env.example` to `<root>/.env` and fill it in, and export `DF1C_ROOT` / `DF1C_PROJECT` in the shell:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
myport/
|
|
109
|
+
.env credentials (never committed)
|
|
110
|
+
project/ copied from project-template/, then filled in
|
|
111
|
+
out/ what the extraction writes (DF1C_OUT, default <root>/out)
|
|
112
|
+
modules/ generated modules (DF1C_MODULES, default <root>/modules)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
DF1C_ROOT=/path/to/myport
|
|
117
|
+
DF1C_PROJECT=/path/to/myport/project
|
|
118
|
+
DF1C_SERVER=<sql server> DF1C_DB=<infobase db> DF1C_USER= DF1C_PASS= # read-only login; Windows auth when USER is empty
|
|
119
|
+
DF1C_FILE=<path to 1Cv8.1CD, or its folder> # a file infobase instead of the two above
|
|
120
|
+
DF_TENANT=<tenant code> DF_PG_HOST= DF_PG_PORT= DF_PG_USER= DF_PG_PASS= # the tenant's Postgres, for load and checks
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
None of these has a default. A default that named one infobase would make the toolkit
|
|
124
|
+
quietly read or write that infobase when a variable was forgotten.
|
|
125
|
+
|
|
126
|
+
## The pipeline
|
|
127
|
+
|
|
128
|
+
Each step reads what the previous one wrote to `out/`. Every command has `--help` or a
|
|
129
|
+
docstring with its options, and `python 1c.py --list` lists them all.
|
|
130
|
+
|
|
131
|
+
| # | step | command |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| 1 | Export the infobase: configuration, DBNames, schema, rows | `dump1c probe` · `dump1c all`, or one at a time: `dump1c dbnames` · `schema` · `extract` · `data [table ...]` |
|
|
134
|
+
| 2 | Parse the metadata, resolve type GUIDs, join the physical schema | `meta allobjects` · `typemap` · `analyze` · `attrs <Object>` · `explore <Object>` |
|
|
135
|
+
| 3 | Extract the module texts (posting handlers, print procedures) | `extract_code` · `unpack <Object> --code` |
|
|
136
|
+
| 4 | Inventory: what exists, what is used, what is the client's | `inventory` · `docusage` · `custom` · `sizing` · `subsystems` |
|
|
137
|
+
| 5 | Read the posting handlers: which registers each document writes and from where | `handlers [--client]` · `callgraph` · `recorders` · `recorded` · `idioms` · `spec` |
|
|
138
|
+
| 6 | Decide the module boundaries | `subsystems --contours --acyclic`, copied into `project/contours.py` and corrected |
|
|
139
|
+
| 7 | Generate and validate the modules | `autospec [module]` · `genmodule` · `validate` · `deps` · `layers` |
|
|
140
|
+
| 8 | Install them | `npx -y @dforge-core/dforge-cli@latest module install ...`, or `rebuild --code <tenant> --install` |
|
|
141
|
+
| 9 | Copy the rows and reconcile | `migrate plan\|check\|run\|verify\|fill <module>` · `integrity` · `columnmap <module>` |
|
|
142
|
+
| 10 | Post state and registers | `posted [--mark]` · `posting` · `seedstate` · `totals` · `coverage` |
|
|
143
|
+
| 11 | Catch up with what 1C changed (MS SQL exports only) | once: `dump1c data --objects` (delta needs every object's keys); then `dump1c data --changes`, `delta --baseline` · `delta [--apply]` |
|
|
144
|
+
| 12 | Print forms and reports | `mxl list [--client]` · `mxl dump <Object>` · `printgen <spec>` · `printgen preview <spec> <form> <key>` · `reportgen <spec>` |
|
|
145
|
+
| 13 | External reports and processings stored in the infobase | `external` |
|
|
146
|
+
|
|
147
|
+
`rebuild --code <tenant>` runs steps 8–10 from nothing, which makes a tenant
|
|
148
|
+
reproducible.
|
|
149
|
+
|
|
150
|
+
## Rules the toolkit assumes
|
|
151
|
+
|
|
152
|
+
- **The infobase is read-only.** Use a SELECT-only login. Read a file infobase from a copy.
|
|
153
|
+
- **Open ad-hoc database connections in autocommit.** A read transaction left open
|
|
154
|
+
blocks the next install's `ALTER`.
|
|
155
|
+
- **Resolve a physical column (`_FldNNN`) only through DBNames** (`attrs`, `columnmap`).
|
|
156
|
+
Never resolve it by cardinality.
|
|
157
|
+
- **Stop on a 401 from the tenant.** Ask for a fresh login, then re-run
|
|
158
|
+
`posted --mark` for documents loaded in the meantime.
|
|
159
|
+
|
|
160
|
+
## Source
|
|
161
|
+
|
|
162
|
+
This copy is kept in step with the port it was developed on (`tools/lib`, `tools/1c.py`
|
|
163
|
+
there). Fixes made in a port go back to the library here. A project's own files never
|
|
164
|
+
come here.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Join the physical schema against DBNames to see which parts of the configuration
|
|
5
|
+
the client actually uses. A stock UTP ships hundreds of catalogs and documents;
|
|
6
|
+
only the populated ones are worth porting.
|
|
7
|
+
"""
|
|
8
|
+
import codecs
|
|
9
|
+
import collections
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import paths
|
|
16
|
+
|
|
17
|
+
OUT = paths.OUT
|
|
18
|
+
|
|
19
|
+
# Physical table families that carry business data, in the order we care about.
|
|
20
|
+
DATA_KINDS = ("Reference", "Document", "InfoRg", "AccumRg", "AccRg", "Chrc", "Acc",
|
|
21
|
+
"Enum", "DocumentJournal", "Node", "BPr", "Task", "Const")
|
|
22
|
+
|
|
23
|
+
SPLIT = re.compile(r"^_([A-Za-z]+?)(\d+)(?:_(.+))?$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load(name):
|
|
27
|
+
with codecs.open(os.path.join(OUT, name), "r", "utf-8") as f:
|
|
28
|
+
return json.load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def classify(table_name):
|
|
32
|
+
"""_Document123_VT456 -> ('Document', 123, 'VT456')"""
|
|
33
|
+
m = SPLIT.match(table_name)
|
|
34
|
+
if not m:
|
|
35
|
+
return None, None, None
|
|
36
|
+
return m.group(1), int(m.group(2)), m.group(3)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main():
|
|
40
|
+
schema = load("schema.json")
|
|
41
|
+
tables = {t["name"]: t for t in schema["tables"]}
|
|
42
|
+
|
|
43
|
+
summary = collections.OrderedDict((k, {"tables": 0, "used": 0, "rows": 0}) for k in DATA_KINDS)
|
|
44
|
+
detail = collections.defaultdict(list)
|
|
45
|
+
|
|
46
|
+
for name, t in tables.items():
|
|
47
|
+
kind, number, suffix = classify(name)
|
|
48
|
+
if kind not in summary or suffix: # skip tabular sections, roll them up separately
|
|
49
|
+
continue
|
|
50
|
+
rows = t["rows"] or 0
|
|
51
|
+
# a document's tabular sections belong to the document's footprint
|
|
52
|
+
child_rows = sum((tables[n]["rows"] or 0) for n in tables
|
|
53
|
+
if n.startswith("_%s%d_" % (kind, number)))
|
|
54
|
+
summary[kind]["tables"] += 1
|
|
55
|
+
summary[kind]["rows"] += rows
|
|
56
|
+
if rows:
|
|
57
|
+
summary[kind]["used"] += 1
|
|
58
|
+
detail[kind].append({"table": name, "number": number, "rows": rows,
|
|
59
|
+
"childRows": child_rows, "columns": len(t["columns"])})
|
|
60
|
+
|
|
61
|
+
print("%-18s %8s %8s %14s" % ("kind", "tables", "used", "rows"))
|
|
62
|
+
print("-" * 52)
|
|
63
|
+
for kind, s in summary.items():
|
|
64
|
+
if s["tables"]:
|
|
65
|
+
print("%-18s %8d %8d %14s" % (kind, s["tables"], s["used"], "{:,}".format(s["rows"])))
|
|
66
|
+
|
|
67
|
+
top = int(sys.argv[1]) if len(sys.argv) > 1 else 25
|
|
68
|
+
for kind in ("Document", "Reference", "AccumRg", "InfoRg"):
|
|
69
|
+
rows = sorted(detail[kind], key=lambda d: -(d["rows"] + d["childRows"]))[:top]
|
|
70
|
+
print("\n== %s: top %d by volume ==" % (kind, top))
|
|
71
|
+
for d in rows:
|
|
72
|
+
if not (d["rows"] or d["childRows"]):
|
|
73
|
+
continue
|
|
74
|
+
print(" %-22s rows=%-12s child=%-12s cols=%d"
|
|
75
|
+
% (d["table"], "{:,}".format(d["rows"]),
|
|
76
|
+
"{:,}".format(d["childRows"]), d["columns"]))
|
|
77
|
+
|
|
78
|
+
with codecs.open(os.path.join(OUT, "usage.json"), "w", "utf-8") as f:
|
|
79
|
+
json.dump({"summary": summary, "detail": detail}, f, ensure_ascii=False, indent="\t")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
main()
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Extract the field list of a metadata object, with references resolved.
|
|
5
|
+
|
|
6
|
+
Every typed thing in a 1C object file - a catalog attribute, a register dimension
|
|
7
|
+
or resource, a tabular-section column - is encoded the same way:
|
|
8
|
+
|
|
9
|
+
["2", ["1", ["0","0",<own guid>], "<Name>", ["1","ru","<Synonym>"], ...],
|
|
10
|
+
["Pattern", <type...>]]
|
|
11
|
+
|
|
12
|
+
so the collector looks for that shape anywhere in the tree rather than hard-coding
|
|
13
|
+
per-kind offsets, and takes the nearest named ancestor as the tabular section.
|
|
14
|
+
|
|
15
|
+
python attrs.py <ObjectName> one object, to the console
|
|
16
|
+
python attrs.py --model <file> <names...> several objects, to a markdown file
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import print_function
|
|
19
|
+
|
|
20
|
+
import codecs
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import paths
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
import meta
|
|
27
|
+
|
|
28
|
+
OUT = meta.OUT
|
|
29
|
+
|
|
30
|
+
PRIMITIVE = {"S": "string", "N": "number", "B": "bool", "D": "date"}
|
|
31
|
+
|
|
32
|
+
# Collection class GUIDs -> what the fields in them mean, confirmed by checking a
|
|
33
|
+
# register whose meaning is unambiguous (ТоварыНаСкладах: Количество is the resource,
|
|
34
|
+
# Склад/Номенклатура the dimensions). Unknown GUIDs print truncated so they show up.
|
|
35
|
+
ROLE = {
|
|
36
|
+
"cf4abea7-37b2-11d4-940f-008048da11f9": "attr", # catalog attribute
|
|
37
|
+
"45e46cbc-3e24-4165-8b7b-cc98a6f80211": "attr", # document attribute
|
|
38
|
+
"888744e1-b616-11d4-9436-004095e12fc7": "column", # tabular section column
|
|
39
|
+
"b64d9a41-1642-11d6-a3c7-0050bae0a776": "resource", # accumulation register
|
|
40
|
+
"b64d9a42-1642-11d6-a3c7-0050bae0a776": "attr",
|
|
41
|
+
"b64d9a43-1642-11d6-a3c7-0050bae0a776": "dimension",
|
|
42
|
+
"13134202-f60b-11d5-a3c7-0050bae0a776": "resource", # information register
|
|
43
|
+
"13134203-f60b-11d5-a3c7-0050bae0a776": "dimension",
|
|
44
|
+
"13134204-f60b-11d5-a3c7-0050bae0a776": "attr",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ROLE_ORDER = ("dimension", "resource", "attr", "column")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def is_identity(node):
|
|
51
|
+
"""["1", ["0","0",<guid>], "<Name>", ["1","ru","<Synonym>"], "", "0", "0"]"""
|
|
52
|
+
if not (isinstance(node, list) and len(node) >= 3 and isinstance(node[1], list)):
|
|
53
|
+
return False
|
|
54
|
+
if len(node[1]) != 3 or not isinstance(node[1][2], str) or not meta.GUID.match(node[1][2]):
|
|
55
|
+
return False
|
|
56
|
+
return isinstance(node[2], str) and bool(node[2]) and not node[2].isdigit()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def find_identity(node):
|
|
60
|
+
"""First identity block in a header subtree, depth first."""
|
|
61
|
+
if not isinstance(node, list):
|
|
62
|
+
return None
|
|
63
|
+
if is_identity(node):
|
|
64
|
+
return node
|
|
65
|
+
for item in node:
|
|
66
|
+
found = find_identity(item)
|
|
67
|
+
if found is not None:
|
|
68
|
+
return found
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_field(node):
|
|
73
|
+
"""A field is an identity block immediately followed by its type pattern."""
|
|
74
|
+
return (isinstance(node, list) and len(node) >= 3 and is_identity(node[1])
|
|
75
|
+
and isinstance(node[2], list) and node[2] and node[2][0] == "Pattern")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def name_of(node):
|
|
79
|
+
return node[1][2]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def guid_of(node):
|
|
83
|
+
"""The attribute's own GUID, from its identity block.
|
|
84
|
+
|
|
85
|
+
This is what DBNames keys its `Fld` entries by, so it is how a ported column is
|
|
86
|
+
traced back to the `_FldNNN` the data actually sits in.
|
|
87
|
+
"""
|
|
88
|
+
ident = node[1]
|
|
89
|
+
head = ident[1] if len(ident) > 1 else None
|
|
90
|
+
return head[2] if isinstance(head, list) and len(head) > 2 else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def synonym_of(node):
|
|
94
|
+
syn = node[1][3] if len(node[1]) > 3 else None
|
|
95
|
+
return syn[2] if isinstance(syn, list) and len(syn) >= 3 else None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def is_collection(node):
|
|
99
|
+
"""[<class guid>, <count>, member, member, ...] - attributes, dimensions, resources."""
|
|
100
|
+
return (isinstance(node, list) and len(node) > 2 and isinstance(node[0], str)
|
|
101
|
+
and meta.GUID.match(node[0]) and isinstance(node[1], str) and node[1].isdigit())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def decode_type(pattern, types):
|
|
105
|
+
"""['Pattern', ['S','30','1'], guid, ...] -> readable type."""
|
|
106
|
+
parts, refs = [], []
|
|
107
|
+
for item in pattern[1:]:
|
|
108
|
+
if isinstance(item, str) and meta.GUID.match(item):
|
|
109
|
+
refs.append(item)
|
|
110
|
+
elif isinstance(item, list) and item:
|
|
111
|
+
head = item[0]
|
|
112
|
+
if head == "S":
|
|
113
|
+
parts.append("string(%s)" % (item[1] if len(item) > 1 else "?"))
|
|
114
|
+
elif head == "N":
|
|
115
|
+
parts.append("number(%s,%s)" % (item[1] if len(item) > 1 else "?",
|
|
116
|
+
item[2] if len(item) > 2 else "0"))
|
|
117
|
+
elif head == "B":
|
|
118
|
+
parts.append("bool")
|
|
119
|
+
elif head == "D":
|
|
120
|
+
parts.append("date(%s)" % (item[1] if len(item) > 1 else "DateTime"))
|
|
121
|
+
else:
|
|
122
|
+
refs.extend(s for s in item if isinstance(s, str) and meta.GUID.match(s))
|
|
123
|
+
for guid in refs:
|
|
124
|
+
target = types.get(guid)
|
|
125
|
+
parts.append("-> %s.%s" % (target["family"], target["name"]) if target
|
|
126
|
+
else "-> <%s>" % guid[:8])
|
|
127
|
+
return ", ".join(parts) or "?"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def has_pattern(node):
|
|
131
|
+
if not isinstance(node, list):
|
|
132
|
+
return False
|
|
133
|
+
if node and node[0] == "Pattern":
|
|
134
|
+
return True
|
|
135
|
+
return any(has_pattern(item) for item in node)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def tabular_section_info(node):
|
|
139
|
+
"""A tabular section is [<header>, ..., <columns collection>]: a named header that
|
|
140
|
+
carries no type of its own, followed by a collection of typed columns.
|
|
141
|
+
|
|
142
|
+
Returns (name, guid). The GUID is what DBNames keys the section's `_VT` table and
|
|
143
|
+
its `_LineNo` column by.
|
|
144
|
+
"""
|
|
145
|
+
if len(node) < 3 or not isinstance(node[0], list) or not is_collection(node[2]):
|
|
146
|
+
return None
|
|
147
|
+
if has_pattern(node[0]):
|
|
148
|
+
return None
|
|
149
|
+
ident = find_identity(node[0])
|
|
150
|
+
if ident is None:
|
|
151
|
+
return None
|
|
152
|
+
head = ident[1] if len(ident) > 1 else None
|
|
153
|
+
guid = head[2] if isinstance(head, list) and len(head) > 2 else None
|
|
154
|
+
return ident[2], guid
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def tabular_section_name(node):
|
|
158
|
+
found = tabular_section_info(node)
|
|
159
|
+
return found[0] if found else None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def collect(node, section, collection, types, fields, section_guid=None):
|
|
163
|
+
if not isinstance(node, list):
|
|
164
|
+
return
|
|
165
|
+
if is_field(node):
|
|
166
|
+
fields.append({
|
|
167
|
+
"section": section,
|
|
168
|
+
"sectionGuid": section_guid,
|
|
169
|
+
"collection": collection,
|
|
170
|
+
"name": name_of(node),
|
|
171
|
+
"guid": guid_of(node),
|
|
172
|
+
"synonym": synonym_of(node),
|
|
173
|
+
"type": decode_type(node[2], types),
|
|
174
|
+
})
|
|
175
|
+
return
|
|
176
|
+
found = tabular_section_info(node)
|
|
177
|
+
if found:
|
|
178
|
+
section, section_guid = found
|
|
179
|
+
if is_collection(node):
|
|
180
|
+
collection = node[0]
|
|
181
|
+
node = node[2:]
|
|
182
|
+
for item in node:
|
|
183
|
+
collect(item, section, collection, types, fields, section_guid)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def enum_values(tree, object_name):
|
|
187
|
+
"""Enum members carry no type pattern, so they are plain identity blocks."""
|
|
188
|
+
values = []
|
|
189
|
+
def walk(node):
|
|
190
|
+
if not isinstance(node, list):
|
|
191
|
+
return
|
|
192
|
+
if is_identity(node):
|
|
193
|
+
if node[2] != object_name and node[2] not in values:
|
|
194
|
+
values.append(node[2])
|
|
195
|
+
return
|
|
196
|
+
for item in node:
|
|
197
|
+
walk(item)
|
|
198
|
+
walk(tree)
|
|
199
|
+
return values
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_json(name):
|
|
203
|
+
with codecs.open(os.path.join(OUT, name), "r", "utf-8") as f:
|
|
204
|
+
return json.load(f)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def fields_of(name, objects, types):
|
|
208
|
+
guid = next((o["guid"] for o in objects if o.get("name") == name), None)
|
|
209
|
+
if not guid:
|
|
210
|
+
return None, None
|
|
211
|
+
text = meta.load(guid)
|
|
212
|
+
if text is None:
|
|
213
|
+
return guid, None
|
|
214
|
+
fields = []
|
|
215
|
+
collect(meta.parse(text), None, None, types, fields)
|
|
216
|
+
return guid, fields
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main():
|
|
220
|
+
objects = load_json("objects.json")
|
|
221
|
+
types = load_json("typemap.json")
|
|
222
|
+
report = {r["name"]: r for r in load_json("report.json")}
|
|
223
|
+
|
|
224
|
+
if sys.argv[1] == "--model":
|
|
225
|
+
out_path, names = sys.argv[2], sys.argv[3:]
|
|
226
|
+
lines = ["# Transport & shipping contour - extracted model", "",
|
|
227
|
+
"Generated from the `%s` infobase without 1C software."
|
|
228
|
+
% (os.environ.get("DF1C_DB") or os.path.basename(os.environ.get("DF1C_FILE") or "source")), ""]
|
|
229
|
+
for name in names:
|
|
230
|
+
guid, fields = fields_of(name, objects, types)
|
|
231
|
+
row = report.get(name, {})
|
|
232
|
+
if fields is None:
|
|
233
|
+
lines += ["## %s *(not found)*" % name, ""]
|
|
234
|
+
continue
|
|
235
|
+
lines += ["## %s.%s - %s" % (row.get("family", "?"), name, row.get("synonym") or ""),
|
|
236
|
+
"",
|
|
237
|
+
"`%s` - %s rows, %s child rows" % (
|
|
238
|
+
row.get("physical"),
|
|
239
|
+
"{:,}".format(row.get("rows") or 0),
|
|
240
|
+
"{:,}".format(row.get("childRows") or 0)), ""]
|
|
241
|
+
if not fields:
|
|
242
|
+
values = enum_values(meta.parse(meta.load(guid)), name)
|
|
243
|
+
if values:
|
|
244
|
+
lines += ["Values: " + ", ".join(values), ""]
|
|
245
|
+
continue
|
|
246
|
+
sections = [None] + sorted({f["section"] for f in fields if f["section"]})
|
|
247
|
+
for section in sections:
|
|
248
|
+
chunk = [f for f in fields if f["section"] == section]
|
|
249
|
+
if not chunk:
|
|
250
|
+
continue
|
|
251
|
+
chunk.sort(key=lambda f: ROLE_ORDER.index(ROLE[f["collection"]])
|
|
252
|
+
if ROLE.get(f["collection"]) in ROLE_ORDER else 9)
|
|
253
|
+
lines += ["**%s**" % (section or "header"), "",
|
|
254
|
+
"| role | field | synonym | type |", "|---|---|---|---|"]
|
|
255
|
+
for f in chunk:
|
|
256
|
+
lines.append("| %s | %s | %s | %s |" % (
|
|
257
|
+
ROLE.get(f["collection"], "?"), f["name"],
|
|
258
|
+
f["synonym"] or "", f["type"]))
|
|
259
|
+
lines.append("")
|
|
260
|
+
with codecs.open(out_path, "w", "utf-8") as f:
|
|
261
|
+
f.write("\n".join(lines))
|
|
262
|
+
print("wrote %s (%d objects)" % (out_path, len(names)))
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
name = sys.argv[1]
|
|
266
|
+
guid, fields = fields_of(name, objects, types)
|
|
267
|
+
if fields is None:
|
|
268
|
+
print("no such object: %s" % name)
|
|
269
|
+
return
|
|
270
|
+
print("%s (%s) fields=%d\n" % (name, guid, len(fields)))
|
|
271
|
+
for f in fields:
|
|
272
|
+
print(" %-10s %-22s %-34s %-28s %s"
|
|
273
|
+
% (ROLE.get(f["collection"], (f["collection"] or "")[:8]),
|
|
274
|
+
f["section"] or "", f["name"], f["synonym"] or "", f["type"]))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|