@dforge-core/dforge-mcp 0.2.46-next → 0.2.48
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 +68 -1
- package/dist/server.js +71 -3
- package/package.json +47 -48
- package/resources/docs/dsl-reference.md +1 -1
- package/resources/schemas/data-views.schema.json +50 -0
- package/skills/dforge-1c-import/SKILL.md +3 -2
- package/skills/dforge-1c-import/references/environment.md +2 -1
- package/skills/dforge-1c-import/references/loading.md +5 -2
- package/skills/dforge-1c-import/references/measurements.md +1 -1
- package/skills/dforge-1c-import/scripts/README.md +88 -7
- package/skills/dforge-1c-import/scripts/lib/__pycache__/attrs.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/delta.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/meta.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/migrate.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/paths.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/posted.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/posting.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/seedstate.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/store.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/totals.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/__pycache__/transactions.cpython-313.pyc +0 -0
- package/skills/dforge-1c-import/scripts/lib/attrs.py +2 -2
- package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
- package/skills/dforge-1c-import/scripts/lib/delta.py +109 -52
- package/skills/dforge-1c-import/scripts/lib/docusage.py +5 -4
- package/skills/dforge-1c-import/scripts/lib/dump1c.py +249 -92
- package/skills/dforge-1c-import/scripts/lib/external.py +4 -3
- package/skills/dforge-1c-import/scripts/lib/meta.py +12 -2
- package/skills/dforge-1c-import/scripts/lib/migrate.py +93 -74
- package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
- package/skills/dforge-1c-import/scripts/lib/paths.py +21 -0
- package/skills/dforge-1c-import/scripts/lib/peek.py +3 -5
- package/skills/dforge-1c-import/scripts/lib/recorded.py +11 -11
- package/skills/dforge-1c-import/scripts/lib/recorders.py +9 -12
- package/skills/dforge-1c-import/scripts/lib/seedstate.py +21 -16
- 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/store.py +126 -0
- package/skills/dforge-1c-import/scripts/lib/totals.py +2 -4
- package/skills/dforge-1c-import/scripts/lib/transactions.py +28 -0
- package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
- package/skills/dforge-1c-import/scripts/project-template/sources.py +10 -0
- package/skills/dforge-mcp-author/references/action-dsl.md +49 -46
- package/skills/dforge-mcp-author/references/data-views.md +38 -2
- package/skills/dforge-mcp-author/references/formulas.md +38 -6
- package/skills/dforge-mcp-author/references/jobs.md +1 -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 +3 -0
- package/skills/dforge-mcp-author/references/reports.md +1 -1
- package/skills/dforge-mcp-author/references/security.md +2 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +7 -8
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Bring an MS SQL export up to date with what 1C changed since it was taken, reading only
|
|
4
|
+
the change: `dump1c data --changes`. A full export of a large infobase is an hour and a
|
|
5
|
+
half (313 M rows); the change of a few days is minutes, and the delta loader reads the
|
|
6
|
+
export either way.
|
|
7
|
+
|
|
8
|
+
python 1c.py dump1c data --changes # since the export's own mark
|
|
9
|
+
python 1c.py dump1c data --changes --since 2026-09-17 # as if exported that day
|
|
10
|
+
python 1c.py dump1c data --changes --since 0x... # from a given rowversion
|
|
11
|
+
|
|
12
|
+
What 1C gives to detect change is the same as `delta` reads, decided per TABLE here from
|
|
13
|
+
the schema dump alone - the export knows nothing of the column map:
|
|
14
|
+
|
|
15
|
+
object `_IDRRef` and a `_Version` rowversion (catalogs, documents, charts):
|
|
16
|
+
the rows at or above the mark replace theirs; a key 1C no longer holds
|
|
17
|
+
is a deletion. 1C writes an object whole, so this is the whole of its change.
|
|
18
|
+
lines a tabular part, `_Document12_VT34`, owned through `_Document12_IDRRef`:
|
|
19
|
+
the lines of a changed owner are replaced, those of a vanished one dropped.
|
|
20
|
+
movements a register with `_RecorderRRef`: the movements of a changed recorder are
|
|
21
|
+
replaced, read through the recorder index; those of a vanished one dropped.
|
|
22
|
+
With `_RecorderTRef` only the recorder types the register holds are asked.
|
|
23
|
+
whole anything else - an independent information register, totals, constants:
|
|
24
|
+
read again in full.
|
|
25
|
+
|
|
26
|
+
The mark is the export's oldest horizon (or `--since`); the new one is
|
|
27
|
+
`MIN_ACTIVE_ROWVERSION()` taken before anything is read, so a change made during the run
|
|
28
|
+
is read again by the next one, never lost. Each table's file is rebuilt in place - what
|
|
29
|
+
it held, less every replaced or deleted key, plus the fresh rows - so after a run the
|
|
30
|
+
export is what a full one would have been, and `export.json` keeps the run in `changes`.
|
|
31
|
+
|
|
32
|
+
A table whose columns changed (a configuration update) is read again in full: its old
|
|
33
|
+
rows no longer have the shape the new ones do. A row changed below the mark is not found:
|
|
34
|
+
the mark has to be one every earlier read was taken after.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import print_function
|
|
37
|
+
|
|
38
|
+
import datetime
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import sys
|
|
42
|
+
import time
|
|
43
|
+
|
|
44
|
+
import dump1c
|
|
45
|
+
|
|
46
|
+
OBJECT = re.compile(r"^_(Document|Reference|Acc|Chrc|CKinds|Node|BPr|Task)\d+$")
|
|
47
|
+
OWNER = re.compile(r"^(_(?:Document|Reference|Acc|Chrc|CKinds|Node|BPr|Task)\d+)_IDRRef$")
|
|
48
|
+
KEYS_IN_LIST = 500
|
|
49
|
+
# Registers whose entries 1C switches on and off without writing the recorder.
|
|
50
|
+
ACTIVE = re.compile(r"^_AccRg\d+$")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def hexed(raw):
|
|
54
|
+
return "0x" + bytes(raw).hex()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def classify(name, columns, schema):
|
|
58
|
+
"""(kind, detail) for one table, from its columns and its neighbours' - see the docstring."""
|
|
59
|
+
by = dict((c["column"], c) for c in columns)
|
|
60
|
+
versioned = (by.get("_Version") or {}).get("type") == "timestamp"
|
|
61
|
+
if versioned and "_IDRRef" in by:
|
|
62
|
+
return "object", None
|
|
63
|
+
for column in by:
|
|
64
|
+
match = OWNER.match(column)
|
|
65
|
+
if match and name.startswith(match.group(1) + "_") and match.group(1) in schema:
|
|
66
|
+
return "lines", (column, match.group(1))
|
|
67
|
+
if "_RecorderRRef" in by:
|
|
68
|
+
return "movements", "_RecorderTRef" in by
|
|
69
|
+
return "whole", None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class Objects(object):
|
|
73
|
+
"""Every object key 1C holds, and which of them changed at or after the mark.
|
|
74
|
+
|
|
75
|
+
Two reads per table rather than one: the key alone comes off the narrowest index, while
|
|
76
|
+
`_Version` lives only in the clustered one - measured on 2.7 M keys, 90 s for both
|
|
77
|
+
together, a few seconds apart.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, source, schema, mark):
|
|
81
|
+
started = time.time()
|
|
82
|
+
self.alive, self.changed = {}, {}
|
|
83
|
+
for name, (columns, _) in sorted(schema.items()):
|
|
84
|
+
if classify(name, columns, schema)[0] != "object":
|
|
85
|
+
continue
|
|
86
|
+
self.changed[name] = [bytes(k) for k in source.column(
|
|
87
|
+
"SELECT _IDRRef FROM [%s] WHERE _Version >= %s" % (name, hexed(mark)))]
|
|
88
|
+
self.alive[name] = [bytes(k) for k in source.column("SELECT _IDRRef FROM [%s]" % name)]
|
|
89
|
+
self.seconds = time.time() - started
|
|
90
|
+
self._arrow = {}
|
|
91
|
+
|
|
92
|
+
def alive_of(self, name=None):
|
|
93
|
+
"""The live keys of one object table - or of every one, for recorders - as Arrow,
|
|
94
|
+
built once: 2.7 M keys per register otherwise."""
|
|
95
|
+
import pyarrow as pa
|
|
96
|
+
if name not in self._arrow:
|
|
97
|
+
keys = self.alive[name] if name else [k for ks in self.alive.values() for k in ks]
|
|
98
|
+
self._arrow[name] = pa.array(keys, type=pa.binary())
|
|
99
|
+
return self._arrow[name]
|
|
100
|
+
|
|
101
|
+
def all_changed(self):
|
|
102
|
+
return [k for keys in self.changed.values() for k in keys]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def since(source, schema, day):
|
|
106
|
+
"""A mark for a run as if the export had been taken on `day`: the lowest `_Version` of
|
|
107
|
+
any document dated on or after it. Everything 1C wrote from about then on is above it."""
|
|
108
|
+
if day.startswith("0x"):
|
|
109
|
+
return bytes.fromhex(day[2:])
|
|
110
|
+
shifted = "%04d-%s" % (int(day[:4]) + source.year_offset, day[5:])
|
|
111
|
+
lowest = None
|
|
112
|
+
for name, (columns, _) in schema.items():
|
|
113
|
+
by = dict((c["column"], c) for c in columns)
|
|
114
|
+
if not name.startswith("_Document") or not OBJECT.match(name) or "_Date_Time" not in by \
|
|
115
|
+
or (by.get("_Version") or {}).get("type") != "timestamp":
|
|
116
|
+
continue
|
|
117
|
+
found = source.column("SELECT MIN(_Version) FROM [%s] WHERE _Date_Time >= '%s'" % (name, shifted))
|
|
118
|
+
if found and found[0] is not None and (lowest is None or bytes(found[0]) < lowest):
|
|
119
|
+
lowest = bytes(found[0])
|
|
120
|
+
if lowest is None:
|
|
121
|
+
raise SystemExit("changes: no document dated on or after %s" % day)
|
|
122
|
+
return lowest
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def in_lists(column, keys):
|
|
126
|
+
for at in range(0, len(keys), KEYS_IN_LIST):
|
|
127
|
+
yield "%s IN (%s)" % (column, ", ".join(hexed(k) for k in keys[at:at + KEYS_IN_LIST]))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def recorder_type(source, register, path, objects, cache={}):
|
|
131
|
+
"""The one table a TRef-less register's recorders come from, by one recorder it holds."""
|
|
132
|
+
import duckdb
|
|
133
|
+
con = duckdb.connect()
|
|
134
|
+
sample = con.execute("SELECT _RecorderRRef FROM read_parquet(?) LIMIT 1", [path]).fetchone()
|
|
135
|
+
con.close()
|
|
136
|
+
if not sample:
|
|
137
|
+
return None
|
|
138
|
+
key = bytes(sample[0])
|
|
139
|
+
if key not in cache:
|
|
140
|
+
cache[key] = next((name for name in sorted(objects.alive, key=lambda n: not n.startswith("_Document"))
|
|
141
|
+
if source.column("SELECT COUNT(*) FROM [%s] WHERE _IDRRef = %s"
|
|
142
|
+
% (name, hexed(key)))[0]), None)
|
|
143
|
+
return cache[key]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Plan(object):
|
|
147
|
+
"""What one table's rebuild reads fresh and which of its old rows it drops."""
|
|
148
|
+
|
|
149
|
+
def __init__(self, name, kind):
|
|
150
|
+
self.name, self.kind = name, kind
|
|
151
|
+
self.wheres = [] # T-SQL conditions; each one query against 1C
|
|
152
|
+
self.column = None # the key column old rows are judged by
|
|
153
|
+
self.replaced = [] # old rows with these keys go: they are read fresh
|
|
154
|
+
self.alive = None # old rows whose key is not among these go: 1C deleted them
|
|
155
|
+
self.whole = False
|
|
156
|
+
self.owners = 0
|
|
157
|
+
self.toggled = 0 # recorders read again because their entries were switched
|
|
158
|
+
self.switched = {} # {recorder table number: [key hex]} of those, for `delta`
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def plan(source, name, columns, schema, objects, path):
|
|
162
|
+
kind, detail = classify(name, columns, schema)
|
|
163
|
+
p = Plan(name, kind)
|
|
164
|
+
if kind == "object":
|
|
165
|
+
p.column, p.replaced, p.alive = "_IDRRef", objects.changed[name], objects.alive_of(name)
|
|
166
|
+
p.wheres = list(in_lists("_IDRRef", p.replaced))
|
|
167
|
+
elif kind == "lines":
|
|
168
|
+
p.column, owner = detail
|
|
169
|
+
p.replaced, p.alive = objects.changed[owner], objects.alive_of(owner)
|
|
170
|
+
p.wheres = list(in_lists(p.column, p.replaced))
|
|
171
|
+
elif kind == "movements":
|
|
172
|
+
p.column, p.alive = "_RecorderRRef", objects.alive_of()
|
|
173
|
+
if detail:
|
|
174
|
+
# Only the recorder types this register holds, off the recorder index.
|
|
175
|
+
numbers = set(int.from_bytes(bytes(t), "big") for t in
|
|
176
|
+
source.column("SELECT DISTINCT _RecorderTRef FROM [%s]" % name))
|
|
177
|
+
for number in sorted(numbers):
|
|
178
|
+
keys = objects.changed.get("_Document%d" % number) or []
|
|
179
|
+
p.replaced += keys
|
|
180
|
+
p.wheres += ["_RecorderTRef = 0x%08x AND %s" % (number, c)
|
|
181
|
+
for c in in_lists("_RecorderRRef", keys)]
|
|
182
|
+
else:
|
|
183
|
+
owner = recorder_type(source, name, path, objects) if os.path.exists(path) else None
|
|
184
|
+
keys = objects.changed[owner] if owner else objects.all_changed()
|
|
185
|
+
p.replaced = keys
|
|
186
|
+
p.wheres = list(in_lists("_RecorderRRef", keys))
|
|
187
|
+
else:
|
|
188
|
+
p.whole = True
|
|
189
|
+
if kind == "movements" and ACTIVE.match(name) and os.path.exists(path) \
|
|
190
|
+
and set(["_Active", "_RecorderTRef"]) <= set(c["column"] for c in columns):
|
|
191
|
+
already = set(p.replaced)
|
|
192
|
+
found = [(n, k) for n, k in switched(source, name, path) if k not in already]
|
|
193
|
+
toggled = [k for _, k in found]
|
|
194
|
+
p.replaced = p.replaced + toggled
|
|
195
|
+
p.wheres = p.wheres + list(in_lists("_RecorderRRef", toggled))
|
|
196
|
+
p.toggled = len(toggled)
|
|
197
|
+
# `delta` finds a changed recorder by `_Version` too, so it is told these by name.
|
|
198
|
+
for number, key in found:
|
|
199
|
+
p.switched.setdefault(str(number), []).append(key.hex())
|
|
200
|
+
p.owners = len(p.replaced)
|
|
201
|
+
return p
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def switched(source, name, path):
|
|
205
|
+
"""Recorders whose entries were switched on or off since the export.
|
|
206
|
+
|
|
207
|
+
`ПереключитьАктивностьПроводок` (the journal's "switch entries on/off") writes a
|
|
208
|
+
recorder's entry set again with another `_Active` and leaves the document alone, so
|
|
209
|
+
its `_Version` does not move and nothing else here would see it. The count of inactive
|
|
210
|
+
entries per recorder, 1C's against the export's, does: a scan of the register, which on
|
|
211
|
+
the ledger of 24 M entries is seconds, and only accounting registers have the command.
|
|
212
|
+
"""
|
|
213
|
+
import duckdb
|
|
214
|
+
now = dict(((int.from_bytes(bytes(t), "big"), bytes(k)), n) for t, k, n in source.triples(
|
|
215
|
+
"SELECT _RecorderTRef, _RecorderRRef, COUNT(*) FROM [%s] WHERE _Active = 0x00"
|
|
216
|
+
" GROUP BY _RecorderTRef, _RecorderRRef" % name))
|
|
217
|
+
con = duckdb.connect()
|
|
218
|
+
held = dict(((int.from_bytes(bytes(t), "big"), bytes(k)), n) for t, k, n in con.execute(
|
|
219
|
+
"SELECT _RecorderTRef, _RecorderRRef, COUNT(*) FROM read_parquet(?)"
|
|
220
|
+
" WHERE _Active = from_hex('00') GROUP BY _RecorderTRef, _RecorderRRef", [path]).fetchall())
|
|
221
|
+
con.close()
|
|
222
|
+
return sorted(r for r in set(now) | set(held) if now.get(r) != held.get(r))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def same_shape(path, arrow):
|
|
226
|
+
"""Do the file's columns still have the names and types the table has now?"""
|
|
227
|
+
import pyarrow.parquet as pq
|
|
228
|
+
if not os.path.exists(path):
|
|
229
|
+
return False
|
|
230
|
+
held = pq.read_schema(path)
|
|
231
|
+
return [(f.name, f.type) for f in held] == [(f.name, f.type) for f in arrow]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def rebuild(source, p, columns, path):
|
|
235
|
+
"""Old rows less the replaced and the deleted, plus the fresh: the file, rewritten."""
|
|
236
|
+
import duckdb
|
|
237
|
+
import pyarrow as pa
|
|
238
|
+
arrow = dump1c.arrow_schema(columns)
|
|
239
|
+
names = [c["column"] for c in columns]
|
|
240
|
+
fresh = [dump1c.arrow_batch(arrow, columns, batch)
|
|
241
|
+
for where in p.wheres for batch in source.rows(p.name, names, dump1c.CHUNK, where)]
|
|
242
|
+
fresh = pa.concat_tables(fresh) if fresh else arrow.empty_table()
|
|
243
|
+
con = duckdb.connect()
|
|
244
|
+
con.register("fresh", fresh)
|
|
245
|
+
con.register("replaced", pa.table({"k": pa.array(p.replaced, type=pa.binary())}))
|
|
246
|
+
con.register("alive", pa.table({"k": p.alive}))
|
|
247
|
+
key = '"%s"' % p.column
|
|
248
|
+
kept_sql = ("SELECT * FROM read_parquet(?) o WHERE o.%s IN (SELECT k FROM alive)"
|
|
249
|
+
" AND o.%s NOT IN (SELECT k FROM replaced)" % (key, key))
|
|
250
|
+
before = con.execute("SELECT COUNT(*) FROM read_parquet(?)", [path]).fetchone()[0]
|
|
251
|
+
kept = con.execute("SELECT COUNT(*) FROM (%s)" % kept_sql, [path]).fetchone()[0]
|
|
252
|
+
if not fresh.num_rows and kept == before:
|
|
253
|
+
# Nothing to replace and nothing deleted: the file stands. On a ledger of 100 M
|
|
254
|
+
# rows that is the difference between a lookup and a rewrite.
|
|
255
|
+
con.close()
|
|
256
|
+
return {"fresh": 0, "dropped": 0, "rows": before}
|
|
257
|
+
tmp = path + ".tmp"
|
|
258
|
+
con.execute("COPY (%s UNION ALL SELECT * FROM fresh) TO '%s' (FORMAT PARQUET, COMPRESSION ZSTD)"
|
|
259
|
+
% (kept_sql, tmp.replace("\\", "/").replace("'", "''")), [path])
|
|
260
|
+
con.close()
|
|
261
|
+
os.replace(tmp, path)
|
|
262
|
+
return {"fresh": fresh.num_rows, "dropped": before - kept, "rows": kept + fresh.num_rows}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def run(source, held, schema, args, whole_table):
|
|
266
|
+
"""`dump1c data --changes`. `whole_table(name)` re-exports one table in full."""
|
|
267
|
+
if source.kind != "mssql":
|
|
268
|
+
raise SystemExit("data --changes: a file infobase has no rowversion to read changes by"
|
|
269
|
+
" - export it again in full (`dump1c data`)")
|
|
270
|
+
exported = sorted(held["tables"])
|
|
271
|
+
if not exported:
|
|
272
|
+
raise SystemExit("data --changes: nothing exported yet - run `dump1c data` first")
|
|
273
|
+
marks = [held["tables"][t].get("horizon") for t in exported]
|
|
274
|
+
if args.since:
|
|
275
|
+
mark = since(source, schema, args.since)
|
|
276
|
+
elif any(m is None for m in marks):
|
|
277
|
+
raise SystemExit("data --changes: a table was exported without a horizon - use --since")
|
|
278
|
+
else:
|
|
279
|
+
mark = bytes.fromhex(min(marks))
|
|
280
|
+
now = source.horizon()
|
|
281
|
+
started = time.time()
|
|
282
|
+
objects = Objects(source, schema, mark)
|
|
283
|
+
changed = sum(len(k) for k in objects.changed.values())
|
|
284
|
+
print("mark %s, horizon %s: %d object(s) changed, %d alive, read in %.1fs"
|
|
285
|
+
% (hexed(mark), hexed(now), changed, len(objects.alive_of()), objects.seconds))
|
|
286
|
+
|
|
287
|
+
summary, totals = {}, {}
|
|
288
|
+
for at, name in enumerate(exported):
|
|
289
|
+
columns, _ = schema[name]
|
|
290
|
+
path = os.path.join(dump1c.DATA, name + ".parquet")
|
|
291
|
+
t0 = time.time()
|
|
292
|
+
p = plan(source, name, columns, schema, objects, path)
|
|
293
|
+
if not p.whole and not same_shape(path, dump1c.arrow_schema(columns)):
|
|
294
|
+
p.whole, p.kind = True, p.kind + " (reshaped)"
|
|
295
|
+
if p.whole:
|
|
296
|
+
rows = whole_table(name, columns)
|
|
297
|
+
result = {"fresh": rows, "dropped": None, "rows": rows}
|
|
298
|
+
else:
|
|
299
|
+
result = rebuild(source, p, columns, path)
|
|
300
|
+
held["tables"][name].update({"rows": result["rows"], "horizon": now.hex(),
|
|
301
|
+
"refreshed": datetime.datetime.now().replace(microsecond=0).isoformat()})
|
|
302
|
+
kind = p.kind.split()[0]
|
|
303
|
+
t = totals.setdefault(kind, [0, 0, 0, 0, 0.0])
|
|
304
|
+
t[0] += 1
|
|
305
|
+
t[1] += p.owners
|
|
306
|
+
t[2] += result["fresh"]
|
|
307
|
+
t[3] += result["dropped"] or 0
|
|
308
|
+
t[4] += time.time() - t0
|
|
309
|
+
if result["fresh"] and not p.whole or result["dropped"]:
|
|
310
|
+
summary[name] = dict(result, kind=p.kind, owners=p.owners, toggled=p.toggled)
|
|
311
|
+
if p.switched:
|
|
312
|
+
summary.setdefault(name, dict(result, kind=p.kind, owners=p.owners, toggled=p.toggled))
|
|
313
|
+
summary[name]["switched"] = p.switched
|
|
314
|
+
sys.stderr.write("\r %d/%d %-40s" % (at + 1, len(exported), name))
|
|
315
|
+
sys.stderr.write("\r" + " " * 60 + "\r")
|
|
316
|
+
held.setdefault("changes", []).append({
|
|
317
|
+
"from": mark.hex(), "to": now.hex(), "since": args.since,
|
|
318
|
+
"at": datetime.datetime.now().replace(microsecond=0).isoformat(),
|
|
319
|
+
"seconds": round(time.time() - started), "tables": summary})
|
|
320
|
+
print("%-10s %7s %10s %10s %10s %8s" % ("kind", "tables", "keys", "fresh", "dropped", "seconds"))
|
|
321
|
+
for kind in ("object", "lines", "movements", "whole"):
|
|
322
|
+
if kind in totals:
|
|
323
|
+
n, owners, fresh, dropped, seconds = totals[kind]
|
|
324
|
+
print("%-10s %7d %10d %10d %10s %8.0f" % (kind, n, owners, fresh,
|
|
325
|
+
dropped if kind != "whole" else "-", seconds))
|
|
326
|
+
print("%.0fs in all" % (time.time() - started))
|