keepset 0.20.0 → 0.21.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-NjZZ73vT.js} +665 -46
- package/dist/{categories-BVROO4tM.d.ts → categories-frr8n_SI.d.ts} +18 -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 +146 -18
- 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
|
@@ -176,6 +176,9 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
176
176
|
paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
177
177
|
kind: z.ZodLiteral<"arcgis-query">;
|
|
178
178
|
orderBy: z.ZodString;
|
|
179
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
180
|
+
kind: z.ZodLiteral<"socrata-resource">;
|
|
181
|
+
pageSize: z.ZodInt;
|
|
179
182
|
}, z.core.$strict>], "kind">>;
|
|
180
183
|
archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
181
184
|
kind: z.ZodLiteral<"none">;
|
|
@@ -332,6 +335,9 @@ declare const EntrySchema: z.ZodObject<{
|
|
|
332
335
|
paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
333
336
|
kind: z.ZodLiteral<"arcgis-query">;
|
|
334
337
|
orderBy: z.ZodString;
|
|
338
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
339
|
+
kind: z.ZodLiteral<"socrata-resource">;
|
|
340
|
+
pageSize: z.ZodInt;
|
|
335
341
|
}, z.core.$strict>], "kind">>;
|
|
336
342
|
archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
337
343
|
kind: z.ZodLiteral<"none">;
|
|
@@ -541,6 +547,9 @@ declare const EntrySchema: z.ZodObject<{
|
|
|
541
547
|
paging?: {
|
|
542
548
|
kind: "arcgis-query";
|
|
543
549
|
orderBy: string;
|
|
550
|
+
} | {
|
|
551
|
+
kind: "socrata-resource";
|
|
552
|
+
pageSize: number;
|
|
544
553
|
} | undefined;
|
|
545
554
|
} | {
|
|
546
555
|
status: "not-fetchable";
|
|
@@ -695,6 +704,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
695
704
|
paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
696
705
|
kind: z.ZodLiteral<"arcgis-query">;
|
|
697
706
|
orderBy: z.ZodString;
|
|
707
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
708
|
+
kind: z.ZodLiteral<"socrata-resource">;
|
|
709
|
+
pageSize: z.ZodInt;
|
|
698
710
|
}, z.core.$strict>], "kind">>;
|
|
699
711
|
archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
700
712
|
kind: z.ZodLiteral<"none">;
|
|
@@ -904,6 +916,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
904
916
|
paging?: {
|
|
905
917
|
kind: "arcgis-query";
|
|
906
918
|
orderBy: string;
|
|
919
|
+
} | {
|
|
920
|
+
kind: "socrata-resource";
|
|
921
|
+
pageSize: number;
|
|
907
922
|
} | undefined;
|
|
908
923
|
} | {
|
|
909
924
|
status: "not-fetchable";
|
|
@@ -1039,6 +1054,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
|
|
|
1039
1054
|
paging?: {
|
|
1040
1055
|
kind: "arcgis-query";
|
|
1041
1056
|
orderBy: string;
|
|
1057
|
+
} | {
|
|
1058
|
+
kind: "socrata-resource";
|
|
1059
|
+
pageSize: number;
|
|
1042
1060
|
} | undefined;
|
|
1043
1061
|
} | {
|
|
1044
1062
|
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-frr8n_SI.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-NjZZ73vT.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-frr8n_SI.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-NjZZ73vT.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);
|
|
@@ -1373,20 +1373,40 @@ const REDIRECTS = new Set(Object.values({
|
|
|
1373
1373
|
}));
|
|
1374
1374
|
/** What the downloader says it is, so a server operator can tell where the traffic comes from. */
|
|
1375
1375
|
const USER_AGENT = "keepset (+https://github.com/Mearman/keepset)";
|
|
1376
|
+
/** The failure of a connection that sent nothing for the stall time. */
|
|
1377
|
+
var StalledError = class extends KeepsetError {};
|
|
1378
|
+
/**
|
|
1379
|
+
* 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.
|
|
1380
|
+
* @param error - The failure.
|
|
1381
|
+
* @returns True when asking again may succeed.
|
|
1382
|
+
*/
|
|
1383
|
+
function isConnectionFailure(error) {
|
|
1384
|
+
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";
|
|
1385
|
+
}
|
|
1376
1386
|
/**
|
|
1377
1387
|
* Sends one GET and resolves with the response head, its body still unread.
|
|
1378
1388
|
* @param url - The URL.
|
|
1379
1389
|
* @param signal - Aborts the request.
|
|
1390
|
+
* @param stall - How many milliseconds the connection may send nothing, in the head or the body that follows.
|
|
1380
1391
|
* @returns The response.
|
|
1392
|
+
* @throws KeepsetError when the connection stalls.
|
|
1381
1393
|
*/
|
|
1382
|
-
async function get(url, signal) {
|
|
1394
|
+
async function get(url, signal, stall) {
|
|
1383
1395
|
const send = url.protocol === "https:" ? request$1 : request;
|
|
1384
1396
|
return new Promise((resolve, reject) => {
|
|
1397
|
+
let answered;
|
|
1385
1398
|
const request = send(url, {
|
|
1386
1399
|
method: "GET",
|
|
1387
1400
|
headers: { "user-agent": USER_AGENT },
|
|
1388
1401
|
...signal === void 0 ? {} : { signal }
|
|
1389
|
-
},
|
|
1402
|
+
}, (response) => {
|
|
1403
|
+
answered = response;
|
|
1404
|
+
resolve(response);
|
|
1405
|
+
});
|
|
1406
|
+
request.setTimeout(stall, () => {
|
|
1407
|
+
const dead = new StalledError(`${url.href} sent nothing for ${String(stall)} milliseconds, so the connection was given up as dead`);
|
|
1408
|
+
(answered ?? request).destroy(dead);
|
|
1409
|
+
});
|
|
1390
1410
|
request.on("error", reject);
|
|
1391
1411
|
request.end();
|
|
1392
1412
|
});
|
|
@@ -1407,13 +1427,13 @@ async function readBody(response) {
|
|
|
1407
1427
|
/**
|
|
1408
1428
|
* Requests one URL, following its redirects by recursion.
|
|
1409
1429
|
* @param url - The URL to request.
|
|
1410
|
-
* @param
|
|
1430
|
+
* @param init - The request options.
|
|
1411
1431
|
* @param redirects - How many redirects led here.
|
|
1412
1432
|
* @returns The final response.
|
|
1413
1433
|
*/
|
|
1414
|
-
async function follow(url,
|
|
1434
|
+
async function follow(url, init, redirects) {
|
|
1415
1435
|
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);
|
|
1436
|
+
const response = await get(url, init.signal, init.stallMilliseconds ?? 12e4);
|
|
1417
1437
|
const { statusCode: status, statusMessage } = response;
|
|
1418
1438
|
if (status === void 0 || statusMessage === void 0) throw new KeepsetError(`${url.href} answered without a status line`);
|
|
1419
1439
|
if (!REDIRECTS.has(status)) return new Response(await readBody(response), {
|
|
@@ -1424,17 +1444,36 @@ async function follow(url, signal, redirects) {
|
|
|
1424
1444
|
const { location } = response.headers;
|
|
1425
1445
|
if (location === void 0) throw new KeepsetError(`${url.href} answered ${String(status)} without a location`);
|
|
1426
1446
|
if (redirects === MAX_REDIRECTS) throw new KeepsetError(`${url.href} redirected more than ${String(MAX_REDIRECTS)} times`);
|
|
1427
|
-
return follow(new URL(location, url),
|
|
1447
|
+
return follow(new URL(location, url), init, redirects + 1);
|
|
1428
1448
|
}
|
|
1429
1449
|
/**
|
|
1430
1450
|
* 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
1451
|
* @param url - An http or https URL.
|
|
1432
1452
|
* @param init - The request options.
|
|
1433
1453
|
* @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.
|
|
1454
|
+
* @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
1455
|
*/
|
|
1436
1456
|
async function downloadRaw(url, init) {
|
|
1437
|
-
return
|
|
1457
|
+
return tryDownload(url, init, 1);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* One try of a download, and the tries after it while the connection fails.
|
|
1461
|
+
* @param url - The URL.
|
|
1462
|
+
* @param init - The request options.
|
|
1463
|
+
* @param attempt - Which try this is, from 1.
|
|
1464
|
+
* @returns The response.
|
|
1465
|
+
* @throws The last failure when the tries are used up, or at once for a failure that is not the connection's.
|
|
1466
|
+
*/
|
|
1467
|
+
async function tryDownload(url, init, attempt) {
|
|
1468
|
+
try {
|
|
1469
|
+
return await follow(new URL(url), init, 0);
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (attempt >= 3 || init.signal?.aborted === true || !isConnectionFailure(error)) throw error;
|
|
1472
|
+
await new Promise((resolve) => {
|
|
1473
|
+
setTimeout(resolve, (init.retryDelayMilliseconds ?? 3e3) * attempt);
|
|
1474
|
+
});
|
|
1475
|
+
return tryDownload(url, init, attempt + 1);
|
|
1476
|
+
}
|
|
1438
1477
|
}
|
|
1439
1478
|
//#endregion
|
|
1440
1479
|
//#region src/node/paged.ts
|
|
@@ -1481,7 +1520,7 @@ const FeatureListSchema = z.array(z.unknown());
|
|
|
1481
1520
|
async function readPages(layer, held) {
|
|
1482
1521
|
if (held.length >= layer.count) return held;
|
|
1483
1522
|
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);
|
|
1523
|
+
const page = await withAttempts(async () => getJson(layer.port, url, layer.signal), layer.signal);
|
|
1485
1524
|
const features = FeatureListSchema.safeParse(typeof page === "object" && page !== null && "features" in page ? page.features : void 0);
|
|
1486
1525
|
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
1526
|
return readPages(layer, [...held, ...features.data]);
|
|
@@ -1511,6 +1550,95 @@ async function downloadArcgisLayer(layerUrl, orderBy, port, signal) {
|
|
|
1511
1550
|
features: pages
|
|
1512
1551
|
}));
|
|
1513
1552
|
}
|
|
1553
|
+
/** 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. */
|
|
1554
|
+
const PAGE_ATTEMPTS = 3;
|
|
1555
|
+
/**
|
|
1556
|
+
* Runs one page read, asking again when it fails, so one bad response of a long download does not lose the rest.
|
|
1557
|
+
* @param read - The page read.
|
|
1558
|
+
* @param signal - Aborts the download, which is not asked again.
|
|
1559
|
+
* @param attempt - Which attempt this is, from 1.
|
|
1560
|
+
* @returns What the read returns.
|
|
1561
|
+
* @throws The last failure, when the attempts are used up or the download was aborted.
|
|
1562
|
+
*/
|
|
1563
|
+
async function withAttempts(read, signal, attempt = 1) {
|
|
1564
|
+
try {
|
|
1565
|
+
return await read();
|
|
1566
|
+
} catch (error) {
|
|
1567
|
+
if (attempt >= PAGE_ATTEMPTS || signal?.aborted === true) throw error;
|
|
1568
|
+
return withAttempts(read, signal, attempt + 1);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
const LINE_FEED = 10;
|
|
1572
|
+
const QUOTE = 34;
|
|
1573
|
+
/**
|
|
1574
|
+
* 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.
|
|
1575
|
+
* @param bytes - A CSV response.
|
|
1576
|
+
* @returns The number of records, the header included.
|
|
1577
|
+
*/
|
|
1578
|
+
function recordsIn(bytes) {
|
|
1579
|
+
let records = 0;
|
|
1580
|
+
let quoted = false;
|
|
1581
|
+
for (const byte of bytes) if (byte === QUOTE) quoted = !quoted;
|
|
1582
|
+
else if (byte === LINE_FEED && !quoted) records += 1;
|
|
1583
|
+
return bytes.length > 0 && bytes[bytes.length - 1] !== LINE_FEED ? records + 1 : records;
|
|
1584
|
+
}
|
|
1585
|
+
/**
|
|
1586
|
+
* Reads one response of a Socrata resource endpoint as bytes.
|
|
1587
|
+
* @param port - The network port.
|
|
1588
|
+
* @param url - The request URL.
|
|
1589
|
+
* @param signal - Aborts the request.
|
|
1590
|
+
* @returns The response body.
|
|
1591
|
+
* @throws KeepsetError when the server does not answer successfully.
|
|
1592
|
+
*/
|
|
1593
|
+
async function getBytes(port, url, signal) {
|
|
1594
|
+
const response = await port(url, signal === void 0 ? {} : { signal });
|
|
1595
|
+
if (!response.ok) throw new KeepsetError(`${url} answered ${String(response.status)} ${response.statusText}`);
|
|
1596
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Reads a resource'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.
|
|
1600
|
+
* @param read - The resource read.
|
|
1601
|
+
* @param held - The pages read so far, each after the first without its header record.
|
|
1602
|
+
* @param offset - How many rows the pages hold.
|
|
1603
|
+
* @returns Every page.
|
|
1604
|
+
* @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.
|
|
1605
|
+
*/
|
|
1606
|
+
async function readResourcePages(read, held, offset) {
|
|
1607
|
+
if (offset >= read.count) return held;
|
|
1608
|
+
const expected = Math.min(read.pageSize, read.count - offset);
|
|
1609
|
+
const page = await withAttempts(async () => {
|
|
1610
|
+
const bytes = await getBytes(read.port, `${read.resource}?%24order=%3Aid&%24limit=${String(read.pageSize)}&%24offset=${String(offset)}`, read.signal);
|
|
1611
|
+
const rows = recordsIn(bytes) - 1;
|
|
1612
|
+
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`);
|
|
1613
|
+
return bytes;
|
|
1614
|
+
}, read.signal);
|
|
1615
|
+
const first = held[0];
|
|
1616
|
+
const headerEnd = page.indexOf(LINE_FEED) + 1;
|
|
1617
|
+
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`);
|
|
1618
|
+
return readResourcePages(read, [...held, first === void 0 ? page : page.subarray(headerEnd)], offset + expected);
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* 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.
|
|
1622
|
+
* @param resource - The resource's URL, such as `https://data.example.org/resource/abcd-1234.csv`, without a query.
|
|
1623
|
+
* @param pageSize - How many rows a page holds.
|
|
1624
|
+
* @param port - The network port.
|
|
1625
|
+
* @param signal - Aborts the download.
|
|
1626
|
+
* @returns The bytes of one CSV, which is what the parser reads and what the provenance hashes.
|
|
1627
|
+
* @throws KeepsetError when the resource does not report a count, or a page is not what it should be.
|
|
1628
|
+
*/
|
|
1629
|
+
async function downloadSocrataResource(resource, pageSize, port, signal) {
|
|
1630
|
+
const reported = /^"count"\r?\n"(\d+)"/.exec(new TextDecoder("utf-8").decode(await getBytes(port, `${resource}?%24select=count(*)`, signal)));
|
|
1631
|
+
if (reported?.[1] === void 0) throw new KeepsetError(`${resource} did not report a row count`);
|
|
1632
|
+
const pages = await readResourcePages({
|
|
1633
|
+
resource,
|
|
1634
|
+
pageSize,
|
|
1635
|
+
count: Number(reported[1]),
|
|
1636
|
+
port,
|
|
1637
|
+
signal
|
|
1638
|
+
}, [], 0);
|
|
1639
|
+
if (pages.length === 0) throw new KeepsetError(`${resource} has no rows`);
|
|
1640
|
+
return Buffer.concat(pages);
|
|
1641
|
+
}
|
|
1514
1642
|
/**
|
|
1515
1643
|
* Downloads a paged source as the one file its pages make, in the way its recipe's paging says.
|
|
1516
1644
|
* @param url - The source's URL.
|
|
@@ -1521,7 +1649,7 @@ async function downloadArcgisLayer(layerUrl, orderBy, port, signal) {
|
|
|
1521
1649
|
* @throws KeepsetError when a page cannot be read or the pages do not add up to what the source reports.
|
|
1522
1650
|
*/
|
|
1523
1651
|
async function downloadPaged(url, paging, port, signal) {
|
|
1524
|
-
return downloadArcgisLayer(url, paging.orderBy, port, signal);
|
|
1652
|
+
return paging.kind === "arcgis-query" ? downloadArcgisLayer(url, paging.orderBy, port, signal) : downloadSocrataResource(url, paging.pageSize, port, signal);
|
|
1525
1653
|
}
|
|
1526
1654
|
//#endregion
|
|
1527
1655
|
//#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.21.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"}]},"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.21.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.21.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}
|