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.
@@ -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-BVROO4tM.js";
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-BmqpHjOH.js";
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-BVROO4tM.js";
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-BmqpHjOH.js";
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 cell = (record.fields[position] ?? "").trim();
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
- }, resolve);
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 signal - Aborts the request.
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, signal, redirects) {
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), signal, redirects + 1);
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 follow(new URL(url), init.signal, 0);
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
@@ -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.20.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,7 +1,7 @@
1
1
  {
2
2
  "name": "keepset",
3
3
  "description": "Library, CLI and statically generated catalogue for dataset fetch-and-normalise recipes",
4
- "version": "0.20.0",
4
+ "version": "0.22.0",
5
5
  "bin": {
6
6
  "keepset": "./dist-cli/keepset.mjs"
7
7
  },
@@ -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.20.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
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.20.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}
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"}