keepset 0.38.0 → 0.39.0
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/README.md +1 -1
- package/dist/{bundled-U2l4WT5T.js → bundled-6dYCPVD-.js} +157 -25
- package/dist/{categories-gZh8cn-G.d.ts → categories-DijttH_M.d.ts} +157 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node/fetch-dataset.d.ts +2 -2
- package/dist/node/fetch-dataset.js +322 -53
- package/dist-cli/keepset.mjs +2 -2
- package/package.json +1 -1
- package/schemas/entry.schema.json +1 -1
- package/schemas/manifest.schema.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as ArchiveError, H as getCollectionTarget, L as getOwn, U as getTarget, at as RecipeError, it as NativeParseError, rt as KeepsetError, t as getDataset, z as countTarget } from "../bundled-
|
|
1
|
+
import { $ as ArchiveError, H as getCollectionTarget, L as getOwn, U as getTarget, at as RecipeError, it as NativeParseError, rt as KeepsetError, t as getDataset, z as countTarget } from "../bundled-6dYCPVD-.js";
|
|
2
2
|
import { RoundTripMismatchError, assertRoundTrip, encode } from "crossmap";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -127,6 +127,15 @@ function endpoint$1(fields, column, lineNumber) {
|
|
|
127
127
|
return value.trim();
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
+
* Whether a line meets one of the recipe's `where` conditions.
|
|
131
|
+
* @param fields - The line's fields, at least as many as the highest column the recipe names.
|
|
132
|
+
* @param condition - The condition.
|
|
133
|
+
* @returns True when the condition's column, trimmed, equals its value.
|
|
134
|
+
*/
|
|
135
|
+
function meets(fields, condition) {
|
|
136
|
+
return fields[condition.column]?.trim() === condition.equals;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
130
139
|
* Resolves `detect` to a fixed delimiter from the first data line: tab when it holds a tab, comma when it holds a comma, whitespace otherwise. The rule is a function of the file's bytes, so a recipe that detects still reads the same way every time.
|
|
131
140
|
* @param dataLine - The first line that is neither skipped, blank nor a comment.
|
|
132
141
|
* @returns The fixed delimiter.
|
|
@@ -149,31 +158,33 @@ function multiplicity(fields, column, lineNumber) {
|
|
|
149
158
|
return Number(value);
|
|
150
159
|
}
|
|
151
160
|
/**
|
|
152
|
-
* Parses a delimited edge list: one edge per line, source and target in the configured columns, after `skipLines` header lines, with blank lines and lines whose first non-blank characters are a comment prefix skipped. A `detect` delimiter is fixed from the first data line. A recipe that names a `keep` column reads only the lines whose field there is one of its values
|
|
153
|
-
* @param text - The file text.
|
|
161
|
+
* Parses a delimited edge list: one edge per line, source and target in the configured columns, after `skipLines` header lines, with blank lines and lines whose first non-blank characters are a comment prefix skipped. A `detect` delimiter is fixed from the first data line. A recipe that names a `keep` column reads only the lines whose field there is one of its values, and one that states `where` reads only the lines whose listed columns, trimmed, equal their values, so a file that records several kinds of row (an interaction file listing genetic beside physical interactions) is read for the rows the dataset is made of; a line that is not read is still checked for the fields it needs. A recipe that names a `multiplicityColumn` emits as many edges for a line as the field says, none for a zero; one that says `mirrored` emits each pair of mirror-image lines once. Nodes are the endpoints of the lines read, in order of first appearance. The text is read line by line, so a file longer than the engine's longest string is read too.
|
|
162
|
+
* @param text - The file text, whole or in pieces.
|
|
154
163
|
* @param options - The recipe's edge-list parser.
|
|
155
164
|
* @returns The native graph.
|
|
156
|
-
* @throws NativeParseError naming the line when a data line has too few fields, an empty endpoint or a multiplicity that is not a whole number, when a mirrored table has a line without its mirror, or when
|
|
165
|
+
* @throws NativeParseError naming the line when a data line has too few fields, an empty endpoint or a multiplicity that is not a whole number, when a mirrored table has a line without its mirror, or when no line is an edge.
|
|
157
166
|
*/
|
|
158
167
|
function parseEdgeList(text, options) {
|
|
159
168
|
let split = options.delimiter === "detect" ? void 0 : splitters[options.delimiter];
|
|
160
|
-
const
|
|
169
|
+
const { where } = options;
|
|
170
|
+
const needed = Math.max(options.sourceColumn, options.targetColumn, options.keep?.column ?? 0, options.multiplicityColumn ?? 0, ...where === void 0 ? [] : where.map((condition) => condition.column)) + 1;
|
|
161
171
|
const order = new NodeOrder();
|
|
162
172
|
const edges = [];
|
|
163
173
|
const awaiting = /* @__PURE__ */ new Map();
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
let lastLine = 0;
|
|
175
|
+
forEachLine(typeof text === "string" ? [text] : text, (line, lineNumber) => {
|
|
176
|
+
lastLine = lineNumber;
|
|
167
177
|
const content = line.trimStart();
|
|
168
|
-
if (
|
|
178
|
+
if (lineNumber <= options.skipLines || content === "" || options.commentPrefixes.some((prefix) => content.startsWith(prefix))) return;
|
|
169
179
|
split ??= splitters[detect(line)];
|
|
170
180
|
const fields = split(line);
|
|
171
181
|
if (fields.length < needed) throw new NativeParseError("edge-list", lineNumber, `expected at least ${String(needed)} fields, found ${String(fields.length)}`);
|
|
172
|
-
if (options.keep !== void 0 && !options.keep.values.includes(fields[options.keep.column]?.trim() ?? ""))
|
|
182
|
+
if (options.keep !== void 0 && !options.keep.values.includes(fields[options.keep.column]?.trim() ?? "")) return;
|
|
183
|
+
if (where !== void 0 && !where.every((condition) => meets(fields, condition))) return;
|
|
173
184
|
const source = endpoint$1(fields, options.sourceColumn, lineNumber);
|
|
174
185
|
const target = endpoint$1(fields, options.targetColumn, lineNumber);
|
|
175
186
|
const count = options.multiplicityColumn === void 0 ? 1 : multiplicity(fields, options.multiplicityColumn, lineNumber);
|
|
176
|
-
if (count === 0)
|
|
187
|
+
if (count === 0) return;
|
|
177
188
|
order.add(source);
|
|
178
189
|
order.add(target);
|
|
179
190
|
if (options.mirrored === true && source !== target) {
|
|
@@ -185,7 +196,7 @@ function parseEdgeList(text, options) {
|
|
|
185
196
|
const waiting = awaiting.get(key);
|
|
186
197
|
if (waiting !== void 0 && waiting.length > 0) {
|
|
187
198
|
waiting.shift();
|
|
188
|
-
|
|
199
|
+
return;
|
|
189
200
|
}
|
|
190
201
|
const mirror = JSON.stringify([
|
|
191
202
|
target,
|
|
@@ -198,12 +209,12 @@ function parseEdgeList(text, options) {
|
|
|
198
209
|
source,
|
|
199
210
|
target
|
|
200
211
|
});
|
|
201
|
-
}
|
|
212
|
+
});
|
|
202
213
|
for (const [key, waiting] of awaiting) {
|
|
203
214
|
const [first] = waiting;
|
|
204
215
|
if (first !== void 0) throw new NativeParseError("edge-list", first, `the table is mirrored but this line has no mirror image (${key} is never listed)`);
|
|
205
216
|
}
|
|
206
|
-
if (edges.length === 0) throw new NativeParseError("edge-list",
|
|
217
|
+
if (edges.length === 0) throw new NativeParseError("edge-list", lastLine, where === void 0 ? "the file holds no edges" : "no line of the file meets the recipe's where conditions");
|
|
207
218
|
return {
|
|
208
219
|
nodes: order.nodes(),
|
|
209
220
|
edges
|
|
@@ -211,7 +222,7 @@ function parseEdgeList(text, options) {
|
|
|
211
222
|
}
|
|
212
223
|
//#endregion
|
|
213
224
|
//#region src/parsers/gd-contest.ts
|
|
214
|
-
const FORMAT$
|
|
225
|
+
const FORMAT$15 = "gd-contest";
|
|
215
226
|
const NUMBER$9 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
|
|
216
227
|
/**
|
|
217
228
|
* Reads a vertex number, which the format numbers from 0 to N - 1.
|
|
@@ -220,7 +231,7 @@ const NUMBER$9 = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?$/;
|
|
|
220
231
|
* @returns The vertex id as text.
|
|
221
232
|
*/
|
|
222
233
|
function vertex$1(token, count) {
|
|
223
|
-
if (!/^(?:0|[1-9]\d*)$/.test(token.text) || Number(token.text) >= count) throw new NativeParseError(FORMAT$
|
|
234
|
+
if (!/^(?:0|[1-9]\d*)$/.test(token.text) || Number(token.text) >= count) throw new NativeParseError(FORMAT$15, token.line, `expected a vertex number from 0 to ${String(count - 1)}, found ${JSON.stringify(token.text)}`);
|
|
224
235
|
return token.text;
|
|
225
236
|
}
|
|
226
237
|
/**
|
|
@@ -240,17 +251,17 @@ function parseGdContest(text) {
|
|
|
240
251
|
});
|
|
241
252
|
}
|
|
242
253
|
const [first] = tokens;
|
|
243
|
-
if (first === void 0) throw new NativeParseError(FORMAT$
|
|
244
|
-
if (!/^(?:0|[1-9]\d*)$/.test(first.text)) throw new NativeParseError(FORMAT$
|
|
254
|
+
if (first === void 0) throw new NativeParseError(FORMAT$15, 1, "the file holds no vertex count");
|
|
255
|
+
if (!/^(?:0|[1-9]\d*)$/.test(first.text)) throw new NativeParseError(FORMAT$15, first.line, `the vertex count must be a non-negative integer, found ${JSON.stringify(first.text)}`);
|
|
245
256
|
const count = Number(first.text);
|
|
246
257
|
const coordinates = tokens.slice(1, 1 + 2 * count);
|
|
247
|
-
if (coordinates.length < 2 * count) throw new NativeParseError(FORMAT$
|
|
248
|
-
for (const coordinate of coordinates) if (!NUMBER$9.test(coordinate.text)) throw new NativeParseError(FORMAT$
|
|
258
|
+
if (coordinates.length < 2 * count) throw new NativeParseError(FORMAT$15, lines(text).length, `expected ${String(2 * count)} coordinates for ${String(count)} vertices, found ${String(coordinates.length)}`);
|
|
259
|
+
for (const coordinate of coordinates) if (!NUMBER$9.test(coordinate.text)) throw new NativeParseError(FORMAT$15, coordinate.line, `coordinate ${JSON.stringify(coordinate.text)} is not a number`);
|
|
249
260
|
const ends = tokens.slice(1 + 2 * count);
|
|
250
261
|
const edges = [];
|
|
251
262
|
for (let source = ends.shift(); source !== void 0; source = ends.shift()) {
|
|
252
263
|
const target = ends.shift();
|
|
253
|
-
if (target === void 0) throw new NativeParseError(FORMAT$
|
|
264
|
+
if (target === void 0) throw new NativeParseError(FORMAT$15, source.line, `edge from vertex ${source.text} has no second vertex`);
|
|
254
265
|
edges.push({
|
|
255
266
|
source: vertex$1(source, count),
|
|
256
267
|
target: vertex$1(target, count)
|
|
@@ -420,7 +431,7 @@ function parseGml(text) {
|
|
|
420
431
|
}
|
|
421
432
|
//#endregion
|
|
422
433
|
//#region src/parsers/hgb.ts
|
|
423
|
-
const FORMAT$
|
|
434
|
+
const FORMAT$14 = "hgb";
|
|
424
435
|
/** A type code or node id as the HGB files write them: a non-negative decimal integer with no sign or leading zero. */
|
|
425
436
|
const CODE = /^(?:0|[1-9]\d*)$/;
|
|
426
437
|
/** A number as a decimal literal with an optional sign and exponent, so `Number` cannot read hex, an empty string or infinity. */
|
|
@@ -440,7 +451,7 @@ const LINK_FIELDS = 4;
|
|
|
440
451
|
*/
|
|
441
452
|
function resolve(field, types, where) {
|
|
442
453
|
const type = types[CODE.test(field) ? Number(field) : -1];
|
|
443
|
-
if (type === void 0) throw new NativeParseError(FORMAT$
|
|
454
|
+
if (type === void 0) throw new NativeParseError(FORMAT$14, where.line, `${where.file}: the ${where.kind} type ${JSON.stringify(field)} is not one of the ${String(types.length)} the recipe names`);
|
|
444
455
|
return type;
|
|
445
456
|
}
|
|
446
457
|
/**
|
|
@@ -458,9 +469,9 @@ function parseHgb(parts, options) {
|
|
|
458
469
|
forEachLine(parts.nodes, (line, number) => {
|
|
459
470
|
const fields = line.split(" ");
|
|
460
471
|
const [id, , type] = fields;
|
|
461
|
-
if (fields.length !== NODE_FIELDS && fields.length !== NODE_FIELDS_WITH_ATTRIBUTES || id === void 0 || type === void 0) throw new NativeParseError(FORMAT$
|
|
462
|
-
if (!CODE.test(id)) throw new NativeParseError(FORMAT$
|
|
463
|
-
if (typeOf.has(id)) throw new NativeParseError(FORMAT$
|
|
472
|
+
if (fields.length !== NODE_FIELDS && fields.length !== NODE_FIELDS_WITH_ATTRIBUTES || id === void 0 || type === void 0) throw new NativeParseError(FORMAT$14, number, `node.dat: expected ${String(NODE_FIELDS)} or ${String(NODE_FIELDS_WITH_ATTRIBUTES)} fields separated by tabs, found ${String(fields.length)}`);
|
|
473
|
+
if (!CODE.test(id)) throw new NativeParseError(FORMAT$14, number, `node.dat: the node id ${JSON.stringify(id)} is not a non-negative integer`);
|
|
474
|
+
if (typeOf.has(id)) throw new NativeParseError(FORMAT$14, number, `node.dat: the node id ${id} is repeated`);
|
|
464
475
|
const nodeType = resolve(type, options.nodeTypes, {
|
|
465
476
|
file: "node.dat",
|
|
466
477
|
line: number,
|
|
@@ -472,12 +483,12 @@ function parseHgb(parts, options) {
|
|
|
472
483
|
type: nodeType
|
|
473
484
|
});
|
|
474
485
|
});
|
|
475
|
-
if (nodes.length === 0) throw new NativeParseError(FORMAT$
|
|
486
|
+
if (nodes.length === 0) throw new NativeParseError(FORMAT$14, 1, "node.dat: the file holds no nodes");
|
|
476
487
|
const edges = [];
|
|
477
488
|
forEachLine(parts.links, (line, number) => {
|
|
478
489
|
const fields = line.split(" ");
|
|
479
490
|
const [source, target, type, weight] = fields;
|
|
480
|
-
if (fields.length !== LINK_FIELDS || source === void 0 || target === void 0 || type === void 0 || weight === void 0) throw new NativeParseError(FORMAT$
|
|
491
|
+
if (fields.length !== LINK_FIELDS || source === void 0 || target === void 0 || type === void 0 || weight === void 0) throw new NativeParseError(FORMAT$14, number, `link.dat: expected ${String(LINK_FIELDS)} fields separated by tabs, found ${String(fields.length)}`);
|
|
481
492
|
const linkType = resolve(type, options.linkTypes, {
|
|
482
493
|
file: "link.dat",
|
|
483
494
|
line: number,
|
|
@@ -493,17 +504,17 @@ function parseHgb(parts, options) {
|
|
|
493
504
|
linkType.target
|
|
494
505
|
]]) {
|
|
495
506
|
const actual = typeOf.get(id);
|
|
496
|
-
if (actual === void 0) throw new NativeParseError(FORMAT$
|
|
497
|
-
if (actual !== expected) throw new NativeParseError(FORMAT$
|
|
507
|
+
if (actual === void 0) throw new NativeParseError(FORMAT$14, number, `link.dat: the ${end} ${JSON.stringify(id)} is not a node of node.dat`);
|
|
508
|
+
if (actual !== expected) throw new NativeParseError(FORMAT$14, number, `link.dat: a ${linkType.name} link's ${end} must be a ${expected} node, and ${id} is a ${actual} node`);
|
|
498
509
|
}
|
|
499
|
-
if (!DECIMAL$5.test(weight) || Number(weight) !== 1) throw new NativeParseError(FORMAT$
|
|
510
|
+
if (!DECIMAL$5.test(weight) || Number(weight) !== 1) throw new NativeParseError(FORMAT$14, number, `link.dat: the weight ${JSON.stringify(weight)} is not 1; a weighted HGB file would lose its weights`);
|
|
500
511
|
edges.push({
|
|
501
512
|
source,
|
|
502
513
|
target,
|
|
503
514
|
type: linkType.name
|
|
504
515
|
});
|
|
505
516
|
});
|
|
506
|
-
if (edges.length === 0) throw new NativeParseError(FORMAT$
|
|
517
|
+
if (edges.length === 0) throw new NativeParseError(FORMAT$14, 1, "link.dat: the file holds no links");
|
|
507
518
|
return {
|
|
508
519
|
nodes,
|
|
509
520
|
edges
|
|
@@ -511,7 +522,7 @@ function parseHgb(parts, options) {
|
|
|
511
522
|
}
|
|
512
523
|
//#endregion
|
|
513
524
|
//#region src/parsers/kg-triples.ts
|
|
514
|
-
const FORMAT$
|
|
525
|
+
const FORMAT$13 = "kg-triples";
|
|
515
526
|
/** The character each delimiter option splits a line on. A line is split on exactly that character, so a field cannot hide a second delimiter. */
|
|
516
527
|
const DELIMITERS$1 = {
|
|
517
528
|
tab: " ",
|
|
@@ -533,8 +544,8 @@ const QUOTE$2 = "\"";
|
|
|
533
544
|
*/
|
|
534
545
|
function value(field, quoted, where) {
|
|
535
546
|
const inner = quoted ? field.slice(1, -1) : field;
|
|
536
|
-
if (quoted && (field.length < 2 || !field.startsWith(QUOTE$2) || !field.endsWith(QUOTE$2) || inner.includes(QUOTE$2))) throw new NativeParseError(FORMAT$
|
|
537
|
-
if (inner === "") throw new NativeParseError(FORMAT$
|
|
547
|
+
if (quoted && (field.length < 2 || !field.startsWith(QUOTE$2) || !field.endsWith(QUOTE$2) || inner.includes(QUOTE$2))) throw new NativeParseError(FORMAT$13, where.line, `${where.part}the field ${JSON.stringify(field)} is not one value wrapped in double quotes`);
|
|
548
|
+
if (inner === "") throw new NativeParseError(FORMAT$13, where.line, `${where.part}a field is empty`);
|
|
538
549
|
return inner;
|
|
539
550
|
}
|
|
540
551
|
/**
|
|
@@ -558,16 +569,16 @@ function parseKgTriples(parts, options) {
|
|
|
558
569
|
let lastLine = 0;
|
|
559
570
|
forEachLine(part.pieces, (line, number) => {
|
|
560
571
|
lastLine = number;
|
|
561
|
-
if (line.trim() === "") throw new NativeParseError(FORMAT$
|
|
572
|
+
if (line.trim() === "") throw new NativeParseError(FORMAT$13, number, `${label}the line is blank`);
|
|
562
573
|
if (number === 1 && options.header === "names") return;
|
|
563
574
|
if (number === 1 && options.header === "count") {
|
|
564
|
-
if (!COUNT$1.test(line)) throw new NativeParseError(FORMAT$
|
|
575
|
+
if (!COUNT$1.test(line)) throw new NativeParseError(FORMAT$13, number, `${label}expected the number of triples on the first line, found ${JSON.stringify(line)}`);
|
|
565
576
|
declared = Number(line);
|
|
566
577
|
return;
|
|
567
578
|
}
|
|
568
579
|
const fields = line.split(delimiter);
|
|
569
580
|
const [head, middle, last] = fields;
|
|
570
|
-
if (fields.length !== TRIPLE_FIELDS || head === void 0 || middle === void 0 || last === void 0) throw new NativeParseError(FORMAT$
|
|
581
|
+
if (fields.length !== TRIPLE_FIELDS || head === void 0 || middle === void 0 || last === void 0) throw new NativeParseError(FORMAT$13, number, `${label}expected ${String(TRIPLE_FIELDS)} fields separated by ${options.delimiter}, found ${String(fields.length)}`);
|
|
571
582
|
const where = {
|
|
572
583
|
line: number,
|
|
573
584
|
part: label
|
|
@@ -593,8 +604,8 @@ function parseKgTriples(parts, options) {
|
|
|
593
604
|
});
|
|
594
605
|
triples += 1;
|
|
595
606
|
});
|
|
596
|
-
if (triples === 0) throw new NativeParseError(FORMAT$
|
|
597
|
-
if (declared !== void 0 && declared !== triples) throw new NativeParseError(FORMAT$
|
|
607
|
+
if (triples === 0) throw new NativeParseError(FORMAT$13, lastLine, `${label}the file holds no triples`);
|
|
608
|
+
if (declared !== void 0 && declared !== triples) throw new NativeParseError(FORMAT$13, lastLine, `${label}the first line declares ${String(declared)} triples, the file holds ${String(triples)}`);
|
|
598
609
|
}
|
|
599
610
|
return {
|
|
600
611
|
nodes: order.nodes(),
|
|
@@ -603,7 +614,7 @@ function parseKgTriples(parts, options) {
|
|
|
603
614
|
}
|
|
604
615
|
//#endregion
|
|
605
616
|
//#region src/parsers/konect.ts
|
|
606
|
-
const FORMAT$
|
|
617
|
+
const FORMAT$12 = "konect";
|
|
607
618
|
/** The network formats of KONECT's first line (Kunegis, "The KONECT Handbook", table 1): undirected, directed and bipartite. */
|
|
608
619
|
const NETWORK_FORMATS = /* @__PURE__ */ new Set([
|
|
609
620
|
"sym",
|
|
@@ -678,9 +689,9 @@ function parseKonect(pieces, options) {
|
|
|
678
689
|
}));
|
|
679
690
|
});
|
|
680
691
|
const { header } = state;
|
|
681
|
-
if (header === void 0) throw new NativeParseError(FORMAT$
|
|
682
|
-
if (edges.length === 0) throw new NativeParseError(FORMAT$
|
|
683
|
-
if (header.counts !== void 0 && header.counts.edges !== edges.length) throw new NativeParseError(FORMAT$
|
|
692
|
+
if (header === void 0) throw new NativeParseError(FORMAT$12, lastLine, "the file has no first line naming its format and weights");
|
|
693
|
+
if (edges.length === 0) throw new NativeParseError(FORMAT$12, lastLine, "the file holds no edges");
|
|
694
|
+
if (header.counts !== void 0 && header.counts.edges !== edges.length) throw new NativeParseError(FORMAT$12, lastLine, `the second line declares ${String(header.counts.edges)} edges, the file holds ${String(edges.length)}`);
|
|
684
695
|
return {
|
|
685
696
|
nodes: order.nodes(),
|
|
686
697
|
edges
|
|
@@ -698,9 +709,9 @@ function readHeader(content, number, options) {
|
|
|
698
709
|
const match = FORMAT_LINE.exec(content);
|
|
699
710
|
const network = match?.[1];
|
|
700
711
|
const weights = match?.[2];
|
|
701
|
-
if (network === void 0 || weights === void 0 || !NETWORK_FORMATS.has(network)) throw new NativeParseError(FORMAT$
|
|
702
|
-
if (!WEIGHTED_TYPES.has(weights) && !UNWEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$
|
|
703
|
-
if (options.weighted !== WEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$
|
|
712
|
+
if (network === void 0 || weights === void 0 || !NETWORK_FORMATS.has(network)) throw new NativeParseError(FORMAT$12, number, `expected a first line "% FORMAT WEIGHTS" with FORMAT one of ${[...NETWORK_FORMATS].join(", ")}, found ${JSON.stringify(content)}`);
|
|
713
|
+
if (!WEIGHTED_TYPES.has(weights) && !UNWEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$12, number, `the weight type ${JSON.stringify(weights)} is not one this parser reads (its third column is not an edge weight or a repetition count)`);
|
|
714
|
+
if (options.weighted !== WEIGHTED_TYPES.has(weights)) throw new NativeParseError(FORMAT$12, number, `the file's weight type is ${weights}, so a recipe with weighted ${String(WEIGHTED_TYPES.has(weights))} reads it, not one with weighted ${String(options.weighted)}`);
|
|
704
715
|
return {
|
|
705
716
|
line: number,
|
|
706
717
|
network
|
|
@@ -720,15 +731,15 @@ function readEdge(content, number, context) {
|
|
|
720
731
|
const [from, to, third, time] = fields;
|
|
721
732
|
const expected = options.timestamped ? TIMESTAMPED_COLUMNS : options.weighted ? WEIGHTED_COLUMNS : void 0;
|
|
722
733
|
const fits = expected === void 0 ? fields.length === ENDPOINT_COLUMNS || fields.length === WEIGHTED_COLUMNS : fields.length === expected;
|
|
723
|
-
if (from === void 0 || to === void 0 || !fits) throw new NativeParseError(FORMAT$
|
|
734
|
+
if (from === void 0 || to === void 0 || !fits) throw new NativeParseError(FORMAT$12, number, `expected ${expected === void 0 ? `${String(ENDPOINT_COLUMNS)} or ${String(WEIGHTED_COLUMNS)}` : String(expected)} columns for this recipe, found ${String(fields.length)}`);
|
|
724
735
|
const bipartite = header.network === "bip";
|
|
725
736
|
const source = endpoint(from, bipartite ? LEFT_PREFIX : "", header.counts?.left, number);
|
|
726
737
|
const target = endpoint(to, bipartite ? RIGHT_PREFIX : "", header.counts?.right, number);
|
|
727
738
|
order.add(source);
|
|
728
739
|
order.add(target);
|
|
729
|
-
if (third !== void 0 && !options.weighted && (!DECIMAL$4.test(third) || Number(third) !== 1)) throw new NativeParseError(FORMAT$
|
|
730
|
-
if (third !== void 0 && options.weighted && !DECIMAL$4.test(third)) throw new NativeParseError(FORMAT$
|
|
731
|
-
if (time !== void 0 && !DECIMAL$4.test(time)) throw new NativeParseError(FORMAT$
|
|
740
|
+
if (third !== void 0 && !options.weighted && (!DECIMAL$4.test(third) || Number(third) !== 1)) throw new NativeParseError(FORMAT$12, number, `the third column ${JSON.stringify(third)} is not 1; an edge that stands for several would be read as one`);
|
|
741
|
+
if (third !== void 0 && options.weighted && !DECIMAL$4.test(third)) throw new NativeParseError(FORMAT$12, number, `the weight ${JSON.stringify(third)} is not a number`);
|
|
742
|
+
if (time !== void 0 && !DECIMAL$4.test(time)) throw new NativeParseError(FORMAT$12, number, `the time ${JSON.stringify(time)} is not a number`);
|
|
732
743
|
const edge = {
|
|
733
744
|
source,
|
|
734
745
|
target
|
|
@@ -747,10 +758,215 @@ function readEdge(content, number, context) {
|
|
|
747
758
|
* @throws NativeParseError when the token is not a positive integer or exceeds the set's size.
|
|
748
759
|
*/
|
|
749
760
|
function endpoint(token, prefix, size, number) {
|
|
750
|
-
if (!NODE_ID.test(token) || size !== void 0 && Number(token) > size) throw new NativeParseError(FORMAT$
|
|
761
|
+
if (!NODE_ID.test(token) || size !== void 0 && Number(token) > size) throw new NativeParseError(FORMAT$12, number, `${JSON.stringify(token)} is not a node id from 1${size === void 0 ? "" : ` to ${String(size)}`}`);
|
|
751
762
|
return `${prefix}${token}`;
|
|
752
763
|
}
|
|
753
764
|
//#endregion
|
|
765
|
+
//#region src/parsers/ogb.ts
|
|
766
|
+
const FORMAT$11 = "ogb";
|
|
767
|
+
/** The files of an OGB raw directory this reader takes, by their path under the dataset's root directory. A relation's files sit in `raw/relations/<head>___<relation>___<tail>/`. */
|
|
768
|
+
const RAW = /^(?<root>(?:[^/]+\/)*?)raw\/(?<name>num-node-list|num-node-dict|edge|num-edge-list|triplet-type-list|relations\/[^/]+\/(?:edge|num-edge-list))\.csv\.gz$/u;
|
|
769
|
+
const INDEX = /^(?:0|[1-9]\d*)$/;
|
|
770
|
+
/** The fields of a line of `triplet-type-list.csv.gz`: a head type, a relation and a tail type. */
|
|
771
|
+
const TRIPLET_PARTS = 3;
|
|
772
|
+
/**
|
|
773
|
+
* Orders two names by code point, the order Python's `sorted` gives the ASCII type and relation names OGB uses.
|
|
774
|
+
* @param left - A name.
|
|
775
|
+
* @param right - Another.
|
|
776
|
+
* @returns Negative, zero or positive.
|
|
777
|
+
*/
|
|
778
|
+
function compare(left, right) {
|
|
779
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Visits the non-blank lines of a file, read a piece at a time.
|
|
783
|
+
* @param file - The file.
|
|
784
|
+
* @param visit - Called with each non-blank line and its number.
|
|
785
|
+
*/
|
|
786
|
+
function forEachRow(file, visit) {
|
|
787
|
+
forEachLine(file.text(), (text, line) => {
|
|
788
|
+
if (text.trim() !== "") visit({
|
|
789
|
+
text,
|
|
790
|
+
line
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* The non-blank lines of a small file, such as a count or the triplet list.
|
|
796
|
+
* @param file - The file.
|
|
797
|
+
* @returns Each non-blank line and its number.
|
|
798
|
+
*/
|
|
799
|
+
function rows(file) {
|
|
800
|
+
const all = [];
|
|
801
|
+
forEachRow(file, (row) => {
|
|
802
|
+
all.push(row);
|
|
803
|
+
});
|
|
804
|
+
return all;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Reads a file that holds one line, such as a count.
|
|
808
|
+
* @param file - The file.
|
|
809
|
+
* @returns The line's text.
|
|
810
|
+
* @throws NativeParseError when the file holds no line or more than one.
|
|
811
|
+
*/
|
|
812
|
+
function onlyLine(file) {
|
|
813
|
+
const all = rows(file);
|
|
814
|
+
const [only] = all;
|
|
815
|
+
if (all.length !== 1 || only === void 0) throw new NativeParseError(FORMAT$11, all[1]?.line ?? 1, `${file.path} must hold one line, found ${String(all.length)}`);
|
|
816
|
+
return only.text.trim();
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Reads a non-negative integer.
|
|
820
|
+
* @param text - The field.
|
|
821
|
+
* @param file - The file, for the error.
|
|
822
|
+
* @param line - The 1-based line, for the error.
|
|
823
|
+
* @returns The integer.
|
|
824
|
+
* @throws NativeParseError when the field is not a non-negative integer.
|
|
825
|
+
*/
|
|
826
|
+
function index(text, file, line) {
|
|
827
|
+
const trimmed = text.trim();
|
|
828
|
+
if (!INDEX.test(trimmed)) throw new NativeParseError(FORMAT$11, line, `${file.path}: expected a non-negative integer, found ${JSON.stringify(text)}`);
|
|
829
|
+
return Number(trimmed);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Reads an `edge.csv.gz` file, one `<source>,<target>` pair of node indices per line, checking each index against its node type's count and the line count against the relation's `num-edge-list.csv.gz`.
|
|
833
|
+
* @param edges - The edge file.
|
|
834
|
+
* @param count - The relation's `num-edge-list.csv.gz`.
|
|
835
|
+
* @param ends - The source and target node types' counts and how each index is named.
|
|
836
|
+
* @param emit - Called with each edge, in file order, so a large file is never held twice.
|
|
837
|
+
* @throws NativeParseError naming the file and line for a line that is not a pair of indices, an index outside its type's nodes, or a line count other than the recorded one.
|
|
838
|
+
*/
|
|
839
|
+
function readEdges(edges, count, ends, emit) {
|
|
840
|
+
const recorded = index(onlyLine(count), count, 1);
|
|
841
|
+
let read = 0;
|
|
842
|
+
forEachRow(edges, ({ text, line }) => {
|
|
843
|
+
const fields = text.split(",");
|
|
844
|
+
const [first, second] = fields;
|
|
845
|
+
if (fields.length !== 2 || first === void 0 || second === void 0) throw new NativeParseError(FORMAT$11, line, `${edges.path}: expected a source and a target index, found ${JSON.stringify(text)}`);
|
|
846
|
+
const source = index(first, edges, line);
|
|
847
|
+
const target = index(second, edges, line);
|
|
848
|
+
if (source >= ends.sources || target >= ends.targets) throw new NativeParseError(FORMAT$11, line, `${edges.path}: edge ${text} names a node outside the ${String(ends.sources)} sources and ${String(ends.targets)} targets the dataset records`);
|
|
849
|
+
emit({
|
|
850
|
+
source: ends.source(source),
|
|
851
|
+
target: ends.target(target)
|
|
852
|
+
});
|
|
853
|
+
read += 1;
|
|
854
|
+
});
|
|
855
|
+
if (read !== recorded) throw new NativeParseError(FORMAT$11, read, `${edges.path} holds ${String(read)} edges, but ${count.path} records ${String(recorded)}`);
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Takes one file of the dataset directory by its name under `raw/`.
|
|
859
|
+
* @param files - The files by name.
|
|
860
|
+
* @param name - The name, such as `edge` or `relations/paper___cites___paper/edge`.
|
|
861
|
+
* @param root - The dataset's root directory, for the error.
|
|
862
|
+
* @returns The file.
|
|
863
|
+
* @throws NativeParseError when the archive's selection holds no such file.
|
|
864
|
+
*/
|
|
865
|
+
function required$1(files, name, root) {
|
|
866
|
+
const file = files.get(name);
|
|
867
|
+
if (file === void 0) throw new NativeParseError(FORMAT$11, 1, `the dataset directory ${root} has no raw/${name}.csv.gz among the files the recipe selects`);
|
|
868
|
+
return file;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Parses an Open Graph Benchmark dataset from the files of its raw directory, as OGB's own `read_csv_graph_raw` and `read_csv_heterograph_raw` read them (ogb/io/read_graph_raw.py in snap-stanford/ogb). A homogeneous dataset has `raw/num-node-list.csv.gz` (its one graph's node count), `raw/edge.csv.gz` (one `<source>,<target>` pair of node indices per line, no header) and `raw/num-edge-list.csv.gz`: its nodes are `0` to `N - 1`, isolated ones included, and its edges the pairs in file order. A heterogeneous dataset has `raw/num-node-dict.csv.gz` (a header of node types over one row of their counts), `raw/triplet-type-list.csv.gz` (one `<head>,<relation>,<tail>` type triplet per line) and, per triplet, `raw/relations/<head>___<relation>___<tail>/edge.csv.gz` and `num-edge-list.csv.gz`, whose indices count within the head and tail types: its nodes are `<type>:<index>` for each type in sorted order and each index from 0, and its edges are each relation's pairs in file order, relations in the sorted order of their triplets, which is the order OGB reads them in. Edges keep the file's direction. Relation names, node features, labels and splits are not carried, as no graph parser in this version carries more than topology.
|
|
872
|
+
* @param files - The files the recipe's archive selects, gunzipped and decoded: every one must be a file of one dataset's raw directory named above.
|
|
873
|
+
* @returns The native graph.
|
|
874
|
+
* @throws NativeParseError naming the file and line for a file outside the raw layout, files of two datasets, a dataset with neither or both node-count files, a missing file, a graph count other than one, a malformed count or triplet, a relation directory the triplet list does not name, an edge that is not a pair of indices of the dataset's nodes, or an edge file whose line count differs from its recorded count.
|
|
875
|
+
*/
|
|
876
|
+
function parseOgb(files) {
|
|
877
|
+
const byName = /* @__PURE__ */ new Map();
|
|
878
|
+
const roots = /* @__PURE__ */ new Set();
|
|
879
|
+
for (const file of files) {
|
|
880
|
+
const match = RAW.exec(file.path);
|
|
881
|
+
const name = match?.groups?.["name"];
|
|
882
|
+
const root = match?.groups?.["root"];
|
|
883
|
+
if (name === void 0 || root === void 0) throw new NativeParseError(FORMAT$11, 1, `${file.path} is not a file of an OGB raw directory this reader takes`);
|
|
884
|
+
roots.add(root);
|
|
885
|
+
byName.set(name, file);
|
|
886
|
+
}
|
|
887
|
+
const [root] = roots;
|
|
888
|
+
if (roots.size !== 1 || root === void 0) throw new NativeParseError(FORMAT$11, 1, `the recipe must select the raw files of one dataset, found ${roots.size === 0 ? "none" : `the directories ${[...roots].join(", ")}`}`);
|
|
889
|
+
const homogeneous = byName.get("num-node-list");
|
|
890
|
+
const heterogeneous = byName.get("num-node-dict");
|
|
891
|
+
const neitherOrBoth = `the dataset directory ${root} must have exactly one of raw/num-node-list.csv.gz (one node type) and raw/num-node-dict.csv.gz (several)`;
|
|
892
|
+
const edges = [];
|
|
893
|
+
if (homogeneous !== void 0) {
|
|
894
|
+
if (heterogeneous !== void 0) throw new NativeParseError(FORMAT$11, 1, neitherOrBoth);
|
|
895
|
+
const [relation] = [...byName.keys()].filter((name) => name.startsWith("relations/"));
|
|
896
|
+
if (relation !== void 0 || byName.has("triplet-type-list")) throw new NativeParseError(FORMAT$11, 1, `the dataset directory ${root} has raw/num-node-list.csv.gz, so it has one node type, but the recipe selects ${relation === void 0 ? "raw/triplet-type-list.csv.gz" : `raw/${relation}.csv.gz`} as well`);
|
|
897
|
+
const count = index(onlyLine(homogeneous), homogeneous, 1);
|
|
898
|
+
const name = (node) => String(node);
|
|
899
|
+
readEdges(required$1(byName, "edge", root), required$1(byName, "num-edge-list", root), {
|
|
900
|
+
sources: count,
|
|
901
|
+
targets: count,
|
|
902
|
+
source: name,
|
|
903
|
+
target: name
|
|
904
|
+
}, (edge) => {
|
|
905
|
+
edges.push(edge);
|
|
906
|
+
});
|
|
907
|
+
return {
|
|
908
|
+
nodes: Array.from({ length: count }, (_, node) => ({ id: String(node) })),
|
|
909
|
+
edges
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (heterogeneous === void 0) throw new NativeParseError(FORMAT$11, 1, neitherOrBoth);
|
|
913
|
+
for (const stray of ["edge", "num-edge-list"]) if (byName.has(stray)) throw new NativeParseError(FORMAT$11, 1, `the dataset directory ${root} has raw/num-node-dict.csv.gz, so its edges are in raw/relations/, but the recipe selects raw/${stray}.csv.gz as well`);
|
|
914
|
+
const header = rows(heterogeneous);
|
|
915
|
+
const [types, counts] = header;
|
|
916
|
+
if (header.length !== 2 || types === void 0 || counts === void 0) throw new NativeParseError(FORMAT$11, header[2]?.line ?? 1, `${heterogeneous.path} must hold a header of node types and one row of their counts, found ${String(header.length)} lines`);
|
|
917
|
+
const typeNames = types.text.split(",").map((type) => type.trim());
|
|
918
|
+
const typeCounts = counts.text.split(",");
|
|
919
|
+
if (typeNames.length !== typeCounts.length || typeNames.some((type) => type === "") || new Set(typeNames).size !== typeNames.length) throw new NativeParseError(FORMAT$11, counts.line, `${heterogeneous.path} must name each node type once, over one count per type`);
|
|
920
|
+
const nodeTypes = typeNames.map((type, position) => {
|
|
921
|
+
const count = typeCounts[position];
|
|
922
|
+
if (count === void 0) throw new NativeParseError(FORMAT$11, counts.line, `${heterogeneous.path} gives node type ${type} no count`);
|
|
923
|
+
return {
|
|
924
|
+
type,
|
|
925
|
+
count: index(count, heterogeneous, counts.line)
|
|
926
|
+
};
|
|
927
|
+
});
|
|
928
|
+
nodeTypes.sort((left, right) => compare(left.type, right.type));
|
|
929
|
+
const nodeCounts = new Map(nodeTypes.map(({ type, count }) => [type, count]));
|
|
930
|
+
const triplets = required$1(byName, "triplet-type-list", root);
|
|
931
|
+
const relations = [];
|
|
932
|
+
for (const { text, line } of rows(triplets)) {
|
|
933
|
+
const parts = text.split(",").map((part) => part.trim());
|
|
934
|
+
const [head, relation, tail] = parts;
|
|
935
|
+
if (parts.length !== TRIPLET_PARTS || head === void 0 || relation === void 0 || tail === void 0 || relation === "") throw new NativeParseError(FORMAT$11, line, `${triplets.path}: expected a head type, a relation and a tail type, found ${JSON.stringify(text)}`);
|
|
936
|
+
const sources = nodeCounts.get(head);
|
|
937
|
+
const targets = nodeCounts.get(tail);
|
|
938
|
+
if (sources === void 0 || targets === void 0) throw new NativeParseError(FORMAT$11, line, `${triplets.path}: ${text} names a node type that ${heterogeneous.path} does not count`);
|
|
939
|
+
relations.push({
|
|
940
|
+
head,
|
|
941
|
+
relation,
|
|
942
|
+
tail,
|
|
943
|
+
sources,
|
|
944
|
+
targets
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
relations.sort((left, right) => compare(left.head, right.head) || compare(left.relation, right.relation) || compare(left.tail, right.tail));
|
|
948
|
+
const listed = new Set(relations.map(({ head, relation, tail }) => `relations/${head}___${relation}___${tail}`));
|
|
949
|
+
for (const name of byName.keys()) {
|
|
950
|
+
const directory = name.slice(0, name.lastIndexOf("/"));
|
|
951
|
+
if (name.startsWith("relations/") && !listed.has(directory)) throw new NativeParseError(FORMAT$11, 1, `raw/${directory}/ is not one of the relations ${triplets.path} lists`);
|
|
952
|
+
}
|
|
953
|
+
for (const { head, relation, tail, sources, targets } of relations) {
|
|
954
|
+
const directory = `relations/${head}___${relation}___${tail}`;
|
|
955
|
+
readEdges(required$1(byName, `${directory}/edge`, root), required$1(byName, `${directory}/num-edge-list`, root), {
|
|
956
|
+
sources,
|
|
957
|
+
targets,
|
|
958
|
+
source: (node) => `${head}:${String(node)}`,
|
|
959
|
+
target: (node) => `${tail}:${String(node)}`
|
|
960
|
+
}, (edge) => {
|
|
961
|
+
edges.push(edge);
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
return {
|
|
965
|
+
nodes: nodeTypes.flatMap(({ type, count }) => Array.from({ length: count }, (_, node) => ({ id: `${type}:${String(node)}` }))),
|
|
966
|
+
edges
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
//#endregion
|
|
754
970
|
//#region src/parsers/matrix-market.ts
|
|
755
971
|
/** How many tokens an entry line of each field type holds: the coordinates, then the value's parts (none for a pattern, a real and an imaginary part for a complex value). */
|
|
756
972
|
const tokensPerEntry = /* @__PURE__ */ new Map([
|
|
@@ -2978,9 +3194,9 @@ function parseNative(bytes, parser) {
|
|
|
2978
3194
|
pieces: decodePieces(bytes, parser)
|
|
2979
3195
|
}], parser);
|
|
2980
3196
|
if (parser.format === "hgb") throw new NativeParseError(parser.format, 1, "an HGB dataset is two files, nodes and links, which a recipe reads from a parts archive");
|
|
3197
|
+
if (parser.format === "edge-list") return parseEdgeList(decodePieces(bytes, parser), parser);
|
|
2981
3198
|
const text = decode(bytes, parser);
|
|
2982
3199
|
if (parser.format === "geojson") return parseGeojson(text, parser);
|
|
2983
|
-
if (parser.format === "edge-list") return parseEdgeList(text, parser);
|
|
2984
3200
|
if (parser.format === "matrix-market") return parseMatrixMarket(text);
|
|
2985
3201
|
if (parser.format === "gml") return parseGml(text);
|
|
2986
3202
|
if (parser.format === "ucinet-dl") return parseUcinetDl(text, parser);
|
|
@@ -2994,6 +3210,7 @@ function parseNative(bytes, parser) {
|
|
|
2994
3210
|
if (parser.format === "verolog-2019") return parseVerolog2019(text);
|
|
2995
3211
|
if (parser.format === "vrp-rep") return parseVrpRep(text);
|
|
2996
3212
|
if (parser.format === "hhcrsp") return parseHhcrsp(text);
|
|
3213
|
+
if (parser.format === "ogb") throw new NativeParseError(parser.format, 1, "the format reads the several files a files archive selects, not one file");
|
|
2997
3214
|
return parsePajek(text);
|
|
2998
3215
|
}
|
|
2999
3216
|
/**
|
|
@@ -3019,6 +3236,20 @@ function parseNativeParts(parts, parser) {
|
|
|
3019
3236
|
}
|
|
3020
3237
|
throw new NativeParseError(parser.format, 1, `the ${parser.format} parser reads one file, not named parts`);
|
|
3021
3238
|
}
|
|
3239
|
+
/**
|
|
3240
|
+
* Reads the several files of a multi-file format into the one native document they make together.
|
|
3241
|
+
* @param files - The files a `files` archive selects, each with its path in the archive, in path order.
|
|
3242
|
+
* @param parser - The recipe's parser, which must be a multi-file format.
|
|
3243
|
+
* @returns The native graph.
|
|
3244
|
+
* @throws NativeParseError when a file is not valid in the stated encoding or the files are not in the stated format, or when the parser reads one file and not several.
|
|
3245
|
+
*/
|
|
3246
|
+
function parseNativeFiles(files, parser) {
|
|
3247
|
+
if (parser.format !== "ogb") throw new NativeParseError(parser.format, 1, "the format reads one file, not the several a files archive selects");
|
|
3248
|
+
return parseOgb(files.map((file) => ({
|
|
3249
|
+
path: file.path,
|
|
3250
|
+
text: () => decodePieces(file.bytes, parser)
|
|
3251
|
+
})));
|
|
3252
|
+
}
|
|
3022
3253
|
//#endregion
|
|
3023
3254
|
//#region src/node/bzip2.ts
|
|
3024
3255
|
/** The four bytes every bzip2 stream starts with, as ASCII: `BZh` and the block size digit, `1` to `9`. */
|
|
@@ -4912,6 +5143,30 @@ function sheetToCsv(bytes, archive) {
|
|
|
4912
5143
|
return gridToCsv(read(bytes, archive.sheet));
|
|
4913
5144
|
}
|
|
4914
5145
|
/**
|
|
5146
|
+
* Unpacks downloaded bytes into every file a multi-file format's selector matches, gunzipping each when the layout's `inner` says so. The paths stay the archive's own, so a gunzipped `edge.csv.gz` keeps its name.
|
|
5147
|
+
* @param bytes - The downloaded bytes.
|
|
5148
|
+
* @param archive - The recipe's archive layout, which selects files with `files`.
|
|
5149
|
+
* @returns Each matching file, in path order.
|
|
5150
|
+
* @throws ArchiveError when the bytes, or a file `inner` gunzips, are not well-formed, or the selector matches no member.
|
|
5151
|
+
*/
|
|
5152
|
+
function unpackFiles(bytes, archive) {
|
|
5153
|
+
const files = unpackMembers(bytes, {
|
|
5154
|
+
kind: archive.kind,
|
|
5155
|
+
members: archive.files
|
|
5156
|
+
});
|
|
5157
|
+
if (archive.inner === void 0) return files;
|
|
5158
|
+
return files.map((file) => {
|
|
5159
|
+
try {
|
|
5160
|
+
return {
|
|
5161
|
+
path: file.path,
|
|
5162
|
+
bytes: gunzip(file.bytes)
|
|
5163
|
+
};
|
|
5164
|
+
} catch (error) {
|
|
5165
|
+
throw new ArchiveError(`member ${file.path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
5166
|
+
}
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
5169
|
+
/**
|
|
4915
5170
|
* Gunzips bytes, rethrowing zlib's failure in the package's taxonomy.
|
|
4916
5171
|
* @param bytes - Gzip data.
|
|
4917
5172
|
* @returns The decompressed bytes.
|
|
@@ -5450,6 +5705,20 @@ async function runToNative(dataset, options) {
|
|
|
5450
5705
|
provenance: provenanceOf({ members: parts.map((part) => part.path) })
|
|
5451
5706
|
};
|
|
5452
5707
|
}
|
|
5708
|
+
if ("files" in archive) {
|
|
5709
|
+
const files = await step(entry.id, "extract", () => unpackFiles(bytes, archive));
|
|
5710
|
+
return {
|
|
5711
|
+
collection: false,
|
|
5712
|
+
documents: [{
|
|
5713
|
+
id: void 0,
|
|
5714
|
+
native: {
|
|
5715
|
+
...await step(entry.id, "parse", () => parseNativeFiles(files, recipe.parser)),
|
|
5716
|
+
meta: metaOf(entry)
|
|
5717
|
+
}
|
|
5718
|
+
}],
|
|
5719
|
+
provenance: provenanceOf({ members: files.map((file) => file.path) })
|
|
5720
|
+
};
|
|
5721
|
+
}
|
|
5453
5722
|
const unpacked = await step(entry.id, "extract", () => unpack(bytes, archive));
|
|
5454
5723
|
const parsed = await step(entry.id, "parse", () => parseNative(unpacked.bytes, recipe.parser));
|
|
5455
5724
|
return {
|
package/dist-cli/keepset.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { KeepsetError, NAME_SEPARATOR, countTarget, getDataset, listDatasets, li
|
|
|
4
4
|
import { fetchDataset, roundTripDataset } from "keepset/node";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
//#region package.json
|
|
7
|
-
var version = "0.
|
|
7
|
+
var version = "0.39.0";
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region cli/run.ts
|
|
10
10
|
/**
|
|
@@ -136,7 +136,7 @@ function formatShow(dataset) {
|
|
|
136
136
|
else {
|
|
137
137
|
const { archive } = recipe;
|
|
138
138
|
const innerText = "inner" in archive && archive.inner !== void 0 ? `, inner ${archive.inner.kind}${"member" in archive.inner ? `, member ${selectorText(archive.inner.member)}` : ""}` : "";
|
|
139
|
-
const memberText = "member" in archive ? `, member ${selectorText(archive.member)}${innerText}` : "members" in archive ? `, members ${selectorText(archive.members)}` : "parts" in archive ? `, parts ${archive.parts.map((part) => `${part.name} from ${selectorText(part.member)}`).join(", ")}` : "";
|
|
139
|
+
const memberText = "member" in archive ? `, member ${selectorText(archive.member)}${innerText}` : "members" in archive ? `, members ${selectorText(archive.members)}` : "parts" in archive ? `, parts ${archive.parts.map((part) => `${part.name} from ${selectorText(part.member)}`).join(", ")}` : "files" in archive ? `, files ${selectorText(archive.files)}${innerText}` : "";
|
|
140
140
|
const { format, ...options } = recipe.parser;
|
|
141
141
|
lines.push(` source: [${String(recipe.source)}]`, ` archive: ${archive.kind}${memberText}`, ` parser: ${format} ${JSON.stringify(options)}`);
|
|
142
142
|
}
|