keepset 0.20.0 → 0.22.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/dist/{bundled-BmqpHjOH.js → bundled-CAsUNCDb.js} +853 -88
- package/dist/{categories-BVROO4tM.d.ts → categories-CJZ2LiJh.d.ts} +43 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node/fetch-dataset.d.ts +1 -1
- package/dist/node/fetch-dataset.js +192 -20
- package/dist-cli/keepset.mjs +1 -1
- package/package.json +1 -1
- package/schemas/entry.schema.json +1 -1
- package/schemas/manifest.schema.json +1 -1
|
@@ -157,6 +157,7 @@ declare const ParserSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
157
157
|
factor: z.ZodNumber;
|
|
158
158
|
}, z.core.$strict>>;
|
|
159
159
|
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
160
|
+
nullValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
160
161
|
}, z.core.$strict>, z.ZodObject<{
|
|
161
162
|
format: z.ZodLiteral<"geojson">;
|
|
162
163
|
encoding: z.ZodLiteral<"utf-8">;
|
|
@@ -176,6 +177,12 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
176
177
|
paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
177
178
|
kind: z.ZodLiteral<"arcgis-query">;
|
|
178
179
|
orderBy: z.ZodString;
|
|
180
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
181
|
+
kind: z.ZodLiteral<"socrata-resource">;
|
|
182
|
+
pageSize: z.ZodInt;
|
|
183
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
184
|
+
kind: z.ZodLiteral<"ckan-datastore">;
|
|
185
|
+
pageSize: z.ZodInt;
|
|
179
186
|
}, z.core.$strict>], "kind">>;
|
|
180
187
|
archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
181
188
|
kind: z.ZodLiteral<"none">;
|
|
@@ -259,6 +266,7 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
259
266
|
factor: z.ZodNumber;
|
|
260
267
|
}, z.core.$strict>>;
|
|
261
268
|
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
269
|
+
nullValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
262
270
|
}, z.core.$strict>, z.ZodObject<{
|
|
263
271
|
format: z.ZodLiteral<"geojson">;
|
|
264
272
|
encoding: z.ZodLiteral<"utf-8">;
|
|
@@ -332,6 +340,12 @@ declare const EntrySchema: z.ZodObject<{
|
|
|
332
340
|
paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
333
341
|
kind: z.ZodLiteral<"arcgis-query">;
|
|
334
342
|
orderBy: z.ZodString;
|
|
343
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
344
|
+
kind: z.ZodLiteral<"socrata-resource">;
|
|
345
|
+
pageSize: z.ZodInt;
|
|
346
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
347
|
+
kind: z.ZodLiteral<"ckan-datastore">;
|
|
348
|
+
pageSize: z.ZodInt;
|
|
335
349
|
}, z.core.$strict>], "kind">>;
|
|
336
350
|
archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
337
351
|
kind: z.ZodLiteral<"none">;
|
|
@@ -415,6 +429,7 @@ declare const EntrySchema: z.ZodObject<{
|
|
|
415
429
|
factor: z.ZodNumber;
|
|
416
430
|
}, z.core.$strict>>;
|
|
417
431
|
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
432
|
+
nullValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
418
433
|
}, z.core.$strict>, z.ZodObject<{
|
|
419
434
|
format: z.ZodLiteral<"geojson">;
|
|
420
435
|
encoding: z.ZodLiteral<"utf-8">;
|
|
@@ -518,6 +533,7 @@ declare const EntrySchema: z.ZodObject<{
|
|
|
518
533
|
factor: number;
|
|
519
534
|
}[];
|
|
520
535
|
columns?: string[] | undefined;
|
|
536
|
+
nullValues?: string[] | undefined;
|
|
521
537
|
} | {
|
|
522
538
|
format: "geojson";
|
|
523
539
|
encoding: "utf-8";
|
|
@@ -541,6 +557,12 @@ declare const EntrySchema: z.ZodObject<{
|
|
|
541
557
|
paging?: {
|
|
542
558
|
kind: "arcgis-query";
|
|
543
559
|
orderBy: string;
|
|
560
|
+
} | {
|
|
561
|
+
kind: "socrata-resource";
|
|
562
|
+
pageSize: number;
|
|
563
|
+
} | {
|
|
564
|
+
kind: "ckan-datastore";
|
|
565
|
+
pageSize: number;
|
|
544
566
|
} | undefined;
|
|
545
567
|
} | {
|
|
546
568
|
status: "not-fetchable";
|
|
@@ -695,6 +717,12 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
695
717
|
paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
696
718
|
kind: z.ZodLiteral<"arcgis-query">;
|
|
697
719
|
orderBy: z.ZodString;
|
|
720
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
721
|
+
kind: z.ZodLiteral<"socrata-resource">;
|
|
722
|
+
pageSize: z.ZodInt;
|
|
723
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
724
|
+
kind: z.ZodLiteral<"ckan-datastore">;
|
|
725
|
+
pageSize: z.ZodInt;
|
|
698
726
|
}, z.core.$strict>], "kind">>;
|
|
699
727
|
archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
700
728
|
kind: z.ZodLiteral<"none">;
|
|
@@ -778,6 +806,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
778
806
|
factor: z.ZodNumber;
|
|
779
807
|
}, z.core.$strict>>;
|
|
780
808
|
columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
809
|
+
nullValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
781
810
|
}, z.core.$strict>, z.ZodObject<{
|
|
782
811
|
format: z.ZodLiteral<"geojson">;
|
|
783
812
|
encoding: z.ZodLiteral<"utf-8">;
|
|
@@ -881,6 +910,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
881
910
|
factor: number;
|
|
882
911
|
}[];
|
|
883
912
|
columns?: string[] | undefined;
|
|
913
|
+
nullValues?: string[] | undefined;
|
|
884
914
|
} | {
|
|
885
915
|
format: "geojson";
|
|
886
916
|
encoding: "utf-8";
|
|
@@ -904,6 +934,12 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
904
934
|
paging?: {
|
|
905
935
|
kind: "arcgis-query";
|
|
906
936
|
orderBy: string;
|
|
937
|
+
} | {
|
|
938
|
+
kind: "socrata-resource";
|
|
939
|
+
pageSize: number;
|
|
940
|
+
} | {
|
|
941
|
+
kind: "ckan-datastore";
|
|
942
|
+
pageSize: number;
|
|
907
943
|
} | undefined;
|
|
908
944
|
} | {
|
|
909
945
|
status: "not-fetchable";
|
|
@@ -1016,6 +1052,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
1016
1052
|
factor: number;
|
|
1017
1053
|
}[];
|
|
1018
1054
|
columns?: string[] | undefined;
|
|
1055
|
+
nullValues?: string[] | undefined;
|
|
1019
1056
|
} | {
|
|
1020
1057
|
format: "geojson";
|
|
1021
1058
|
encoding: "utf-8";
|
|
@@ -1039,6 +1076,12 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
1039
1076
|
paging?: {
|
|
1040
1077
|
kind: "arcgis-query";
|
|
1041
1078
|
orderBy: string;
|
|
1079
|
+
} | {
|
|
1080
|
+
kind: "socrata-resource";
|
|
1081
|
+
pageSize: number;
|
|
1082
|
+
} | {
|
|
1083
|
+
kind: "ckan-datastore";
|
|
1084
|
+
pageSize: number;
|
|
1042
1085
|
} | undefined;
|
|
1043
1086
|
} | {
|
|
1044
1087
|
status: "not-fetchable";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as Parser, A as LineageSchema, B as Citation, C as CatalogueIndex, D as NamespaceNode, E as DatasetQuery, F as AliasSchema, G as Entry, H as ColumnNameSchema, I as Archive, J as IdSchema, K as EntrySchema, L as ArchiveMember, M as ManifestSchema, N as SemVerSchema, O as createCatalogue, P as parseManifest, Q as NamespaceSchema, R as ArchiveMemberSchema, S as Catalogue, T as Dataset, U as CrosswalkReference, V as CitationSchema, W as CrosswalkReferenceSchema, X as LicenceSchema, Y as Licence, Z as Namespace, _ as GraphEdgeSchema, a as CategoryTargets, at as Snapshot, b as GraphV1, c as getCategory, ct as SourceSchema, d as listCategories, dt as waybackUrl, et as ParserSchema, f as OrNode, g as GraphEdge, h as OrV1Schema, i as CategoryTarget, it as RelativePathSchema, j as Manifest, k as Lineage, l as getTarget, lt as WaybackTimestampSchema, m as OrV1, n as CategoryId, nt as Recipe, o as CountedKey, ot as SnapshotSchema, p as OrNodeSchema, q as HttpUrlSchema, r as CategoryIdSchema, rt as RecipeSchema, s as countTarget, st as Source, t as Category, tt as RESERVED_META_KEYS, u as kindOf, ut as parseEntry, v as GraphNode, w as CatalogueIndexSchema, x as GraphV1Schema, y as GraphNodeSchema, z as ArchiveSchema } from "./categories-
|
|
1
|
+
import { $ as Parser, A as LineageSchema, B as Citation, C as CatalogueIndex, D as NamespaceNode, E as DatasetQuery, F as AliasSchema, G as Entry, H as ColumnNameSchema, I as Archive, J as IdSchema, K as EntrySchema, L as ArchiveMember, M as ManifestSchema, N as SemVerSchema, O as createCatalogue, P as parseManifest, Q as NamespaceSchema, R as ArchiveMemberSchema, S as Catalogue, T as Dataset, U as CrosswalkReference, V as CitationSchema, W as CrosswalkReferenceSchema, X as LicenceSchema, Y as Licence, Z as Namespace, _ as GraphEdgeSchema, a as CategoryTargets, at as Snapshot, b as GraphV1, c as getCategory, ct as SourceSchema, d as listCategories, dt as waybackUrl, et as ParserSchema, f as OrNode, g as GraphEdge, h as OrV1Schema, i as CategoryTarget, it as RelativePathSchema, j as Manifest, k as Lineage, l as getTarget, lt as WaybackTimestampSchema, m as OrV1, n as CategoryId, nt as Recipe, o as CountedKey, ot as SnapshotSchema, p as OrNodeSchema, q as HttpUrlSchema, r as CategoryIdSchema, rt as RecipeSchema, s as countTarget, st as Source, t as Category, tt as RESERVED_META_KEYS, u as kindOf, ut as parseEntry, v as GraphNode, w as CatalogueIndexSchema, x as GraphV1Schema, y as GraphNodeSchema, z as ArchiveSchema } from "./categories-CJZ2LiJh.js";
|
|
2
2
|
import { Crosswalk } from "twindex";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
//#region src/errors.d.ts
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NativeParseError, A as toIssues, B as kindOf, C as RESERVED_META_KEYS, D as SourceSchema, E as SnapshotSchema, G as GraphNodeSchema, H as OrNodeSchema, I as CategoryIdSchema, J as ArchiveError, K as GraphV1Schema, L as countTarget, M as NAME_SEPARATOR, N as groupsOf, O as WaybackTimestampSchema, P as namesOf, Q as KeepsetError, R as getCategory, S as ParserSchema, T as RelativePathSchema, U as OrV1Schema, V as listCategories, W as GraphEdgeSchema, X as DatasetAmbiguousError, Y as CatalogueSyntaxError, Z as DatasetNotFoundError, _ as EntrySchema, a as CatalogueIndexSchema, b as LicenceSchema, c as ManifestSchema, d as AliasSchema, et as RecipeError, f as ArchiveMemberSchema, g as CrosswalkReferenceSchema, h as ColumnNameSchema, i as listNamespaces, j as waybackUrl, k as parseEntry, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as JsonScalarSchema, r as listDatasets, s as LineageSchema, t as getDataset, tt as UnknownCategoryError, u as parseManifest, v as HttpUrlSchema, w as RecipeSchema, x as NamespaceSchema, y as IdSchema, z as getTarget } from "./bundled-
|
|
1
|
+
import { $ as NativeParseError, A as toIssues, B as kindOf, C as RESERVED_META_KEYS, D as SourceSchema, E as SnapshotSchema, G as GraphNodeSchema, H as OrNodeSchema, I as CategoryIdSchema, J as ArchiveError, K as GraphV1Schema, L as countTarget, M as NAME_SEPARATOR, N as groupsOf, O as WaybackTimestampSchema, P as namesOf, Q as KeepsetError, R as getCategory, S as ParserSchema, T as RelativePathSchema, U as OrV1Schema, V as listCategories, W as GraphEdgeSchema, X as DatasetAmbiguousError, Y as CatalogueSyntaxError, Z as DatasetNotFoundError, _ as EntrySchema, a as CatalogueIndexSchema, b as LicenceSchema, c as ManifestSchema, d as AliasSchema, et as RecipeError, f as ArchiveMemberSchema, g as CrosswalkReferenceSchema, h as ColumnNameSchema, i as listNamespaces, j as waybackUrl, k as parseEntry, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as JsonScalarSchema, r as listDatasets, s as LineageSchema, t as getDataset, tt as UnknownCategoryError, u as parseManifest, v as HttpUrlSchema, w as RecipeSchema, x as NamespaceSchema, y as IdSchema, z as getTarget } from "./bundled-CAsUNCDb.js";
|
|
2
2
|
import { validateCrosswalk } from "twindex";
|
|
3
3
|
//#region src/serialise.ts
|
|
4
4
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as Entry, T as Dataset, at as Snapshot, i as CategoryTarget, st as Source } from "../categories-
|
|
1
|
+
import { G as Entry, T as Dataset, at as Snapshot, i as CategoryTarget, st as Source } from "../categories-CJZ2LiJh.js";
|
|
2
2
|
//#region src/node/fetch-dataset.d.ts
|
|
3
3
|
/** The network port a recipe downloads through: `downloadRaw` by default, which returns the bytes exactly as served, or a stand-in under test. */
|
|
4
4
|
export type FetchPort = (url: string, init: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-
|
|
1
|
+
import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-CAsUNCDb.js";
|
|
2
2
|
import { RoundTripMismatchError, assertRoundTrip, encode } from "twindex";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -390,8 +390,8 @@ function columnNames(cells, idColumn, line) {
|
|
|
390
390
|
return names;
|
|
391
391
|
}
|
|
392
392
|
/** The code unit of each character the scanner stops at. */
|
|
393
|
-
const QUOTE = "\"".charCodeAt(0);
|
|
394
|
-
const LINE_FEED = "\n".charCodeAt(0);
|
|
393
|
+
const QUOTE$1 = "\"".charCodeAt(0);
|
|
394
|
+
const LINE_FEED$1 = "\n".charCodeAt(0);
|
|
395
395
|
const CARRIAGE_RETURN = "\r".charCodeAt(0);
|
|
396
396
|
/** Scans a table file's text as it arrives in pieces, holding what a record in progress needs between them: the fields and field so far, whether a quoted field is open, and the line the record began on and the line the scan has reached. */
|
|
397
397
|
var RecordScanner = class {
|
|
@@ -429,7 +429,7 @@ var RecordScanner = class {
|
|
|
429
429
|
completed,
|
|
430
430
|
carry: text.slice(index)
|
|
431
431
|
};
|
|
432
|
-
if (text.charCodeAt(index + 1) === QUOTE) {
|
|
432
|
+
if (text.charCodeAt(index + 1) === QUOTE$1) {
|
|
433
433
|
this.field += "\"";
|
|
434
434
|
index += 2;
|
|
435
435
|
} else {
|
|
@@ -439,7 +439,7 @@ var RecordScanner = class {
|
|
|
439
439
|
continue;
|
|
440
440
|
}
|
|
441
441
|
const code = text.charCodeAt(index);
|
|
442
|
-
if (code === QUOTE) {
|
|
442
|
+
if (code === QUOTE$1) {
|
|
443
443
|
if (this.field !== "") throw new NativeParseError("csv-table", this.line, "a quote opens inside an unquoted field");
|
|
444
444
|
this.quoted = true;
|
|
445
445
|
index += 1;
|
|
@@ -447,19 +447,19 @@ var RecordScanner = class {
|
|
|
447
447
|
this.fields.push(this.field);
|
|
448
448
|
this.field = "";
|
|
449
449
|
index += 1;
|
|
450
|
-
} else if (code === LINE_FEED || code === CARRIAGE_RETURN) {
|
|
450
|
+
} else if (code === LINE_FEED$1 || code === CARRIAGE_RETURN) {
|
|
451
451
|
if (code === CARRIAGE_RETURN && index + 1 >= text.length && !final) return {
|
|
452
452
|
completed,
|
|
453
453
|
carry: text.slice(index)
|
|
454
454
|
};
|
|
455
|
-
index += code === CARRIAGE_RETURN && text.charCodeAt(index + 1) === LINE_FEED ? 2 : 1;
|
|
455
|
+
index += code === CARRIAGE_RETURN && text.charCodeAt(index + 1) === LINE_FEED$1 ? 2 : 1;
|
|
456
456
|
this.line += 1;
|
|
457
457
|
completed.push(this.endRecord());
|
|
458
458
|
} else {
|
|
459
459
|
let next = index + 1;
|
|
460
460
|
while (next < text.length) {
|
|
461
461
|
const following = text.charCodeAt(next);
|
|
462
|
-
if (following === QUOTE || following === this.delimiterCode || following === LINE_FEED || following === CARRIAGE_RETURN) break;
|
|
462
|
+
if (following === QUOTE$1 || following === this.delimiterCode || following === LINE_FEED$1 || following === CARRIAGE_RETURN) break;
|
|
463
463
|
next += 1;
|
|
464
464
|
}
|
|
465
465
|
this.field += text.slice(index, next);
|
|
@@ -574,7 +574,7 @@ function* tableRecords(text, options) {
|
|
|
574
574
|
for (const record of records(body, DELIMITERS[options.delimiter], options.skipLines + 1)) if (record.fields.length > 1 || record.fields[0]?.trim() !== "") yield record;
|
|
575
575
|
}
|
|
576
576
|
/**
|
|
577
|
-
* Parses a delimited table into rows: the first record after `skipLines` lines is the header, each later record is a row, and every row carries every column but the id column (or, when the recipe lists `columns`, exactly those, so a wide table is not held cell for cell), an empty cell being null. Cells are trimmed. A column the recipe declares numeric is read as a decimal number times its factor and any other column stays a string. `id` is the row's id, so a column of that name which is not the id column is named by its position. A row's id is the cell of the recipe's id column, which is then not also a column, or its 1-based row number when the recipe names none. Blank lines are skipped; a row with a different number of fields than the header, two columns with the same name, and an id cell that is empty are errors.
|
|
577
|
+
* Parses a delimited table into rows (a cell whose text is one of the recipe's `nullValues` is empty, so null): the first record after `skipLines` lines is the header, each later record is a row, and every row carries every column but the id column (or, when the recipe lists `columns`, exactly those, so a wide table is not held cell for cell), an empty cell being null. Cells are trimmed. A column the recipe declares numeric is read as a decimal number times its factor and any other column stays a string. `id` is the row's id, so a column of that name which is not the id column is named by its position. A row's id is the cell of the recipe's id column, which is then not also a column, or its 1-based row number when the recipe names none. Blank lines are skipped; a row with a different number of fields than the header, two columns with the same name, and an id cell that is empty are errors.
|
|
578
578
|
* @param text - The file text, or its pieces in order.
|
|
579
579
|
* @param options - The recipe's table parser.
|
|
580
580
|
* @returns The rows, in file order.
|
|
@@ -595,6 +595,7 @@ function parseCsvTable(text, options) {
|
|
|
595
595
|
if (idColumn !== null && wanted.has(idColumn)) throw new NativeParseError("csv-table", header.line, `the id column ${idColumn} is the row's id, so it is not listed among the columns`);
|
|
596
596
|
for (const { column } of numericColumns) if (!wanted.has(column)) throw new NativeParseError("csv-table", header.line, `the column ${column} is declared numeric but is not among the columns kept`);
|
|
597
597
|
const factors = new Map(numericColumns.map((entry) => [entry.column, entry.factor]));
|
|
598
|
+
const missing = new Set(options.nullValues ?? []);
|
|
598
599
|
const idPosition = idColumn === null ? -1 : names.indexOf(idColumn);
|
|
599
600
|
const kept = names.flatMap((name, position) => name !== idColumn && wanted.has(name) ? [{
|
|
600
601
|
name,
|
|
@@ -607,7 +608,8 @@ function parseCsvTable(text, options) {
|
|
|
607
608
|
const id = idColumn === null ? String(nodes.length + 1) : (record.fields[idPosition] ?? "").trim();
|
|
608
609
|
if (id === "") throw new NativeParseError("csv-table", record.line, `the id column ${idColumn ?? ""} is empty`);
|
|
609
610
|
const cells = kept.map(({ name, position, factor }) => {
|
|
610
|
-
const
|
|
611
|
+
const trimmed = (record.fields[position] ?? "").trim();
|
|
612
|
+
const cell = missing.has(trimmed) ? "" : trimmed;
|
|
611
613
|
return [name, factor === void 0 ? cell === "" ? null : cell : numeric$1(cell, factor, name, record.line)];
|
|
612
614
|
});
|
|
613
615
|
nodes.push({
|
|
@@ -1373,20 +1375,40 @@ const REDIRECTS = new Set(Object.values({
|
|
|
1373
1375
|
}));
|
|
1374
1376
|
/** What the downloader says it is, so a server operator can tell where the traffic comes from. */
|
|
1375
1377
|
const USER_AGENT = "keepset (+https://github.com/Mearman/keepset)";
|
|
1378
|
+
/** The failure of a connection that sent nothing for the stall time. */
|
|
1379
|
+
var StalledError = class extends KeepsetError {};
|
|
1380
|
+
/**
|
|
1381
|
+
* Whether a failure is the connection's and may pass: a stall, or a socket error such as a reset, a broken pipe or a timeout, which carries an `E` code. A bad URL, a redirect that leads nowhere and an abort are not.
|
|
1382
|
+
* @param error - The failure.
|
|
1383
|
+
* @returns True when asking again may succeed.
|
|
1384
|
+
*/
|
|
1385
|
+
function isConnectionFailure(error) {
|
|
1386
|
+
return error instanceof StalledError || typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && /^E[A-Z]+$/.test(error.code) && error.code !== "ERR_INVALID_URL";
|
|
1387
|
+
}
|
|
1376
1388
|
/**
|
|
1377
1389
|
* Sends one GET and resolves with the response head, its body still unread.
|
|
1378
1390
|
* @param url - The URL.
|
|
1379
1391
|
* @param signal - Aborts the request.
|
|
1392
|
+
* @param stall - How many milliseconds the connection may send nothing, in the head or the body that follows.
|
|
1380
1393
|
* @returns The response.
|
|
1394
|
+
* @throws KeepsetError when the connection stalls.
|
|
1381
1395
|
*/
|
|
1382
|
-
async function get(url, signal) {
|
|
1396
|
+
async function get(url, signal, stall) {
|
|
1383
1397
|
const send = url.protocol === "https:" ? request$1 : request;
|
|
1384
1398
|
return new Promise((resolve, reject) => {
|
|
1399
|
+
let answered;
|
|
1385
1400
|
const request = send(url, {
|
|
1386
1401
|
method: "GET",
|
|
1387
1402
|
headers: { "user-agent": USER_AGENT },
|
|
1388
1403
|
...signal === void 0 ? {} : { signal }
|
|
1389
|
-
},
|
|
1404
|
+
}, (response) => {
|
|
1405
|
+
answered = response;
|
|
1406
|
+
resolve(response);
|
|
1407
|
+
});
|
|
1408
|
+
request.setTimeout(stall, () => {
|
|
1409
|
+
const dead = new StalledError(`${url.href} sent nothing for ${String(stall)} milliseconds, so the connection was given up as dead`);
|
|
1410
|
+
(answered ?? request).destroy(dead);
|
|
1411
|
+
});
|
|
1390
1412
|
request.on("error", reject);
|
|
1391
1413
|
request.end();
|
|
1392
1414
|
});
|
|
@@ -1407,13 +1429,13 @@ async function readBody(response) {
|
|
|
1407
1429
|
/**
|
|
1408
1430
|
* Requests one URL, following its redirects by recursion.
|
|
1409
1431
|
* @param url - The URL to request.
|
|
1410
|
-
* @param
|
|
1432
|
+
* @param init - The request options.
|
|
1411
1433
|
* @param redirects - How many redirects led here.
|
|
1412
1434
|
* @returns The final response.
|
|
1413
1435
|
*/
|
|
1414
|
-
async function follow(url,
|
|
1436
|
+
async function follow(url, init, redirects) {
|
|
1415
1437
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new KeepsetError(`${url.href} is not an http or https URL`);
|
|
1416
|
-
const response = await get(url, signal);
|
|
1438
|
+
const response = await get(url, init.signal, init.stallMilliseconds ?? 12e4);
|
|
1417
1439
|
const { statusCode: status, statusMessage } = response;
|
|
1418
1440
|
if (status === void 0 || statusMessage === void 0) throw new KeepsetError(`${url.href} answered without a status line`);
|
|
1419
1441
|
if (!REDIRECTS.has(status)) return new Response(await readBody(response), {
|
|
@@ -1424,17 +1446,36 @@ async function follow(url, signal, redirects) {
|
|
|
1424
1446
|
const { location } = response.headers;
|
|
1425
1447
|
if (location === void 0) throw new KeepsetError(`${url.href} answered ${String(status)} without a location`);
|
|
1426
1448
|
if (redirects === MAX_REDIRECTS) throw new KeepsetError(`${url.href} redirected more than ${String(MAX_REDIRECTS)} times`);
|
|
1427
|
-
return follow(new URL(location, url),
|
|
1449
|
+
return follow(new URL(location, url), init, redirects + 1);
|
|
1428
1450
|
}
|
|
1429
1451
|
/**
|
|
1430
1452
|
* Downloads a URL's bytes exactly as the server sends them, following redirects. Unlike the platform `fetch`, it never undoes a `Content-Encoding`: servers commonly label a `.gz` file `Content-Encoding: gzip`, and a recipe that unpacks gzip needs the file as published, which is also what its SHA-256 is recorded over. No `Accept-Encoding` is sent, so a server that compresses on the fly has no reason to.
|
|
1431
1453
|
* @param url - An http or https URL.
|
|
1432
1454
|
* @param init - The request options.
|
|
1433
1455
|
* @returns A Response holding the raw bytes, with the final status.
|
|
1434
|
-
* @throws KeepsetError when a redirect has no location, leaves http and https, or passes the redirect limit.
|
|
1456
|
+
* @throws KeepsetError when a redirect has no location, leaves http and https, or passes the redirect limit; the connection's failure, after the tries, when it keeps failing or sends nothing for longer than the stall time.
|
|
1435
1457
|
*/
|
|
1436
1458
|
async function downloadRaw(url, init) {
|
|
1437
|
-
return
|
|
1459
|
+
return tryDownload(url, init, 1);
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* One try of a download, and the tries after it while the connection fails.
|
|
1463
|
+
* @param url - The URL.
|
|
1464
|
+
* @param init - The request options.
|
|
1465
|
+
* @param attempt - Which try this is, from 1.
|
|
1466
|
+
* @returns The response.
|
|
1467
|
+
* @throws The last failure when the tries are used up, or at once for a failure that is not the connection's.
|
|
1468
|
+
*/
|
|
1469
|
+
async function tryDownload(url, init, attempt) {
|
|
1470
|
+
try {
|
|
1471
|
+
return await follow(new URL(url), init, 0);
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
if (attempt >= 3 || init.signal?.aborted === true || !isConnectionFailure(error)) throw error;
|
|
1474
|
+
await new Promise((resolve) => {
|
|
1475
|
+
setTimeout(resolve, (init.retryDelayMilliseconds ?? 3e3) * attempt);
|
|
1476
|
+
});
|
|
1477
|
+
return tryDownload(url, init, attempt + 1);
|
|
1478
|
+
}
|
|
1438
1479
|
}
|
|
1439
1480
|
//#endregion
|
|
1440
1481
|
//#region src/node/paged.ts
|
|
@@ -1481,7 +1522,7 @@ const FeatureListSchema = z.array(z.unknown());
|
|
|
1481
1522
|
async function readPages(layer, held) {
|
|
1482
1523
|
if (held.length >= layer.count) return held;
|
|
1483
1524
|
const url = `${layer.query}?where=1%3D1&outFields=*&orderByFields=${encodeURIComponent(layer.orderBy)}&resultOffset=${String(held.length)}&f=geojson`;
|
|
1484
|
-
const page = await getJson(layer.port, url, layer.signal);
|
|
1525
|
+
const page = await withAttempts(async () => getJson(layer.port, url, layer.signal), layer.signal);
|
|
1485
1526
|
const features = FeatureListSchema.safeParse(typeof page === "object" && page !== null && "features" in page ? page.features : void 0);
|
|
1486
1527
|
if (!features.success || features.data.length === 0) throw new KeepsetError(`${layer.query} gave no features at offset ${String(held.length)}, before the ${String(layer.count)} it reports, so it does not page by offset`);
|
|
1487
1528
|
return readPages(layer, [...held, ...features.data]);
|
|
@@ -1511,6 +1552,136 @@ async function downloadArcgisLayer(layerUrl, orderBy, port, signal) {
|
|
|
1511
1552
|
features: pages
|
|
1512
1553
|
}));
|
|
1513
1554
|
}
|
|
1555
|
+
/** How many times a page is requested before the download fails: a connection can drop or go silent in one request of dozens and succeed in the next, and the pages are read by offset so asking again is exact. */
|
|
1556
|
+
const PAGE_ATTEMPTS = 3;
|
|
1557
|
+
/**
|
|
1558
|
+
* Runs one page read, asking again when it fails, so one bad response of a long download does not lose the rest.
|
|
1559
|
+
* @param read - The page read.
|
|
1560
|
+
* @param signal - Aborts the download, which is not asked again.
|
|
1561
|
+
* @param attempt - Which attempt this is, from 1.
|
|
1562
|
+
* @returns What the read returns.
|
|
1563
|
+
* @throws The last failure, when the attempts are used up or the download was aborted.
|
|
1564
|
+
*/
|
|
1565
|
+
async function withAttempts(read, signal, attempt = 1) {
|
|
1566
|
+
try {
|
|
1567
|
+
return await read();
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
if (attempt >= PAGE_ATTEMPTS || signal?.aborted === true) throw error;
|
|
1570
|
+
return withAttempts(read, signal, attempt + 1);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const LINE_FEED = 10;
|
|
1574
|
+
const QUOTE = 34;
|
|
1575
|
+
/**
|
|
1576
|
+
* How many CSV records a response holds: the line feeds outside a quoted field, and one more when the last record has no line feed.
|
|
1577
|
+
* @param bytes - A CSV response.
|
|
1578
|
+
* @returns The number of records, the header included.
|
|
1579
|
+
*/
|
|
1580
|
+
function recordsIn(bytes) {
|
|
1581
|
+
let records = 0;
|
|
1582
|
+
let quoted = false;
|
|
1583
|
+
for (const byte of bytes) if (byte === QUOTE) quoted = !quoted;
|
|
1584
|
+
else if (byte === LINE_FEED && !quoted) records += 1;
|
|
1585
|
+
return bytes.length > 0 && bytes[bytes.length - 1] !== LINE_FEED ? records + 1 : records;
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Reads one response of a Socrata resource endpoint as bytes.
|
|
1589
|
+
* @param port - The network port.
|
|
1590
|
+
* @param url - The request URL.
|
|
1591
|
+
* @param signal - Aborts the request.
|
|
1592
|
+
* @returns The response body.
|
|
1593
|
+
* @throws KeepsetError when the server does not answer successfully.
|
|
1594
|
+
*/
|
|
1595
|
+
async function getBytes(port, url, signal) {
|
|
1596
|
+
const response = await port(url, signal === void 0 ? {} : { signal });
|
|
1597
|
+
if (!response.ok) throw new KeepsetError(`${url} answered ${String(response.status)} ${response.statusText}`);
|
|
1598
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Reads a source's pages, each starting where the rows held so far end, and checks each holds exactly the rows it should, so a response cut short is an error and not a table with rows missing.
|
|
1602
|
+
* @param read - The source read.
|
|
1603
|
+
* @param held - The pages read so far, each after the first without its header record.
|
|
1604
|
+
* @param offset - How many rows the pages hold.
|
|
1605
|
+
* @returns Every page.
|
|
1606
|
+
* @throws KeepsetError when a page holds a different number of rows than the count says it should, or its header differs from the first page's.
|
|
1607
|
+
*/
|
|
1608
|
+
async function readResourcePages(read, held, offset) {
|
|
1609
|
+
if (offset >= read.count) return held;
|
|
1610
|
+
const expected = Math.min(read.pageSize, read.count - offset);
|
|
1611
|
+
const page = await withAttempts(async () => {
|
|
1612
|
+
const bytes = await getBytes(read.port, read.pageUrl(offset), read.signal);
|
|
1613
|
+
const rows = recordsIn(bytes) - 1;
|
|
1614
|
+
if (rows !== expected) throw new KeepsetError(`${read.resource} gave ${String(rows)} rows at offset ${String(offset)} where ${String(expected)} of its ${String(read.count)} were due, so a response was cut short or the table changed during the download`);
|
|
1615
|
+
return bytes;
|
|
1616
|
+
}, read.signal);
|
|
1617
|
+
const first = held[0];
|
|
1618
|
+
const headerEnd = page.indexOf(LINE_FEED) + 1;
|
|
1619
|
+
if (first !== void 0 && !page.subarray(0, headerEnd).every((byte, index) => byte === first[index])) throw new KeepsetError(`${read.resource} gave a different header at offset ${String(offset)} than at the start`);
|
|
1620
|
+
return readResourcePages(read, [...held, first === void 0 ? page : page.subarray(headerEnd)], offset + expected);
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Downloads a CSV source a page at a time and joins the pages under the first header.
|
|
1624
|
+
* @param read - The source read, with its count.
|
|
1625
|
+
* @returns The bytes of one CSV, which is what the parser reads and what the provenance hashes.
|
|
1626
|
+
* @throws KeepsetError when the source has no rows or a page is not what it should be.
|
|
1627
|
+
*/
|
|
1628
|
+
async function downloadPages(read) {
|
|
1629
|
+
const pages = await readResourcePages(read, [], 0);
|
|
1630
|
+
if (pages.length === 0) throw new KeepsetError(`${read.resource} has no rows`);
|
|
1631
|
+
return Buffer.concat(pages);
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Downloads a whole Socrata resource as one CSV: the row count is read first, then pages of `pageSize` rows in the order of the rows' ids, each checked to hold the rows it should, and joined under the first page's header.
|
|
1635
|
+
* @param resource - The resource's URL, such as `https://data.example.org/resource/abcd-1234.csv`, without a query.
|
|
1636
|
+
* @param pageSize - How many rows a page holds.
|
|
1637
|
+
* @param port - The network port.
|
|
1638
|
+
* @param signal - Aborts the download.
|
|
1639
|
+
* @returns The bytes of one CSV, which is what the parser reads and what the provenance hashes.
|
|
1640
|
+
* @throws KeepsetError when the resource does not report a count, or a page is not what it should be.
|
|
1641
|
+
*/
|
|
1642
|
+
async function downloadSocrataResource(resource, pageSize, port, signal) {
|
|
1643
|
+
const reported = /^"count"\r?\n"(\d+)"/.exec(new TextDecoder("utf-8").decode(await getBytes(port, `${resource}?%24select=count(*)`, signal)));
|
|
1644
|
+
if (reported?.[1] === void 0) throw new KeepsetError(`${resource} did not report a row count`);
|
|
1645
|
+
return downloadPages({
|
|
1646
|
+
resource,
|
|
1647
|
+
pageUrl: (offset) => `${resource}?%24order=%3Aid&%24limit=${String(pageSize)}&%24offset=${String(offset)}`,
|
|
1648
|
+
pageSize,
|
|
1649
|
+
count: Number(reported[1]),
|
|
1650
|
+
port,
|
|
1651
|
+
signal
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
const CountResultSchema = z.object({ result: z.object({ total: z.int().min(0) }) });
|
|
1655
|
+
/**
|
|
1656
|
+
* Downloads a whole CKAN datastore table as one CSV: the row count is read from the portal's datastore API, then pages of `pageSize` rows of the table's CSV dump, each checked to hold the rows it should, and joined under the first page's header.
|
|
1657
|
+
* @param dump - The dump's URL, such as `https://data.example.org/datastore/dump/<resource id>`, without a query; a portal that serves CKAN under a path prefix keeps it before `/datastore/dump/`.
|
|
1658
|
+
* @param pageSize - How many rows a page holds.
|
|
1659
|
+
* @param port - The network port.
|
|
1660
|
+
* @param signal - Aborts the download.
|
|
1661
|
+
* @returns The bytes of one CSV, which is what the parser reads and what the provenance hashes.
|
|
1662
|
+
* @throws KeepsetError when the URL is not a datastore dump, the table does not report a count, or a page is not what it should be.
|
|
1663
|
+
*/
|
|
1664
|
+
async function downloadCkanDatastore(dump, pageSize, port, signal) {
|
|
1665
|
+
const parsed = /^(.*)\/datastore\/dump\/([^/?#]+)$/.exec(dump);
|
|
1666
|
+
if (parsed?.[1] === void 0 || parsed[2] === void 0) throw new KeepsetError(`${dump} is not a CKAN datastore dump URL`);
|
|
1667
|
+
const url = `${parsed[1]}/api/3/action/datastore_search?resource_id=${parsed[2]}&limit=0`;
|
|
1668
|
+
let body;
|
|
1669
|
+
try {
|
|
1670
|
+
body = JSON.parse(new TextDecoder("utf-8").decode(await getBytes(port, url, signal)));
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
throw new KeepsetError(`${url} did not answer JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
1673
|
+
}
|
|
1674
|
+
const count = CountResultSchema.safeParse(body);
|
|
1675
|
+
if (!count.success) throw new KeepsetError(`${dump} did not report a row count`);
|
|
1676
|
+
return downloadPages({
|
|
1677
|
+
resource: dump,
|
|
1678
|
+
pageUrl: (offset) => `${dump}?format=csv&limit=${String(pageSize)}&offset=${String(offset)}`,
|
|
1679
|
+
pageSize,
|
|
1680
|
+
count: count.data.result.total,
|
|
1681
|
+
port,
|
|
1682
|
+
signal
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1514
1685
|
/**
|
|
1515
1686
|
* Downloads a paged source as the one file its pages make, in the way its recipe's paging says.
|
|
1516
1687
|
* @param url - The source's URL.
|
|
@@ -1521,7 +1692,8 @@ async function downloadArcgisLayer(layerUrl, orderBy, port, signal) {
|
|
|
1521
1692
|
* @throws KeepsetError when a page cannot be read or the pages do not add up to what the source reports.
|
|
1522
1693
|
*/
|
|
1523
1694
|
async function downloadPaged(url, paging, port, signal) {
|
|
1524
|
-
return downloadArcgisLayer(url, paging.orderBy, port, signal);
|
|
1695
|
+
if (paging.kind === "arcgis-query") return downloadArcgisLayer(url, paging.orderBy, port, signal);
|
|
1696
|
+
return paging.kind === "socrata-resource" ? downloadSocrataResource(url, paging.pageSize, port, signal) : downloadCkanDatastore(url, paging.pageSize, port, signal);
|
|
1525
1697
|
}
|
|
1526
1698
|
//#endregion
|
|
1527
1699
|
//#region src/node/fetch-dataset.ts
|
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.22.0";
|
|
8
8
|
const usage = `Usage: keepset <command> [options]
|
|
9
9
|
|
|
10
10
|
Commands:
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"category":{"enum":["graph/v1","or/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"xlsx","type":"string"},"sheet":{"minLength":1,"type":"string"}},"required":["kind","sheet"],"type":"object"}]},"paging":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"arcgis-query","type":"string"},"orderBy":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"}},"required":["kind","orderBy"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"delimiter":{"enum":["comma","tab","semicolon"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"csv-table","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","idColumn","numericColumns"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"minItems":1,"type":"array"},"dateColumns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"encoding":{"const":"utf-8","type":"string"},"format":{"const":"geojson","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"}},"required":["format","encoding","idColumn","numericColumns","dateColumns","columns"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.
|
|
1
|
+
{"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"category":{"enum":["graph/v1","or/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"xlsx","type":"string"},"sheet":{"minLength":1,"type":"string"}},"required":["kind","sheet"],"type":"object"}]},"paging":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"arcgis-query","type":"string"},"orderBy":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"}},"required":["kind","orderBy"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"socrata-resource","type":"string"},"pageSize":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"required":["kind","pageSize"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"ckan-datastore","type":"string"},"pageSize":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"required":["kind","pageSize"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"delimiter":{"enum":["comma","tab","semicolon"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"csv-table","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"nullValues":{"items":{"minLength":1,"type":"string"},"type":"array"},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","idColumn","numericColumns"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"minItems":1,"type":"array"},"dateColumns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"encoding":{"const":"utf-8","type":"string"},"format":{"const":"geojson","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"}},"required":["format","encoding","idColumn","numericColumns","dateColumns","columns"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.22.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.
|
|
1
|
+
{"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.22.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}
|