keepset 0.21.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.
@@ -482,7 +482,9 @@ const ParserSchema = z.discriminatedUnion("format", [
482
482
  column: ColumnNameSchema,
483
483
  factor: z.number().positive()
484
484
  }).strict()),
485
- columns: z.array(ColumnNameSchema).optional()
485
+ columns: z.array(ColumnNameSchema).optional(),
486
+ /** Cell texts, besides the empty cell, that stand for a missing value (`NA`): such a cell is null in every column, numeric or not. */
487
+ nullValues: z.array(z.string().min(1)).optional()
486
488
  }).strict().refine((parser) => new Set(parser.numericColumns.map((numeric) => numeric.column)).size === parser.numericColumns.length, {
487
489
  message: "a column may be declared numeric once",
488
490
  path: ["numericColumns"]
@@ -514,14 +516,21 @@ const ParserSchema = z.discriminatedUnion("format", [
514
516
  path: ["dateColumns"]
515
517
  })
516
518
  ]);
517
- /** Where a source is a paged query and not one file. An ArcGIS feature layer returns a limited number of features per request: the source URL is the layer's, the download reads every page in `orderBy` order and the parser reads the one GeoJSON collection they make. A Socrata resource endpoint (`/resource/<id>.csv`) is read `pageSize` rows at a time in the order of its row ids, each page a CSV with its own header, and the download joins them into the one CSV the parser reads, so no single response has to survive a long transfer. */
518
- const PagingSchema = z.discriminatedUnion("kind", [z.object({
519
- kind: z.literal("arcgis-query"),
520
- orderBy: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, { message: "must be a field name of the layer" })
521
- }).strict(), z.object({
522
- kind: z.literal("socrata-resource"),
523
- pageSize: z.int().min(1)
524
- }).strict()]);
519
+ /** Where a source is a paged query and not one file. An ArcGIS feature layer returns a limited number of features per request: the source URL is the layer's, the download reads every page in `orderBy` order and the parser reads the one GeoJSON collection they make. A Socrata resource endpoint (`/resource/<id>.csv`) is read `pageSize` rows at a time in the order of its row ids, and a CKAN datastore dump (`/datastore/dump/<id>`) `pageSize` rows at a time, each page a CSV with its own header; the download joins them into the one CSV the parser reads, so no single response has to survive a long transfer. */
520
+ const PagingSchema = z.discriminatedUnion("kind", [
521
+ z.object({
522
+ kind: z.literal("arcgis-query"),
523
+ orderBy: z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/, { message: "must be a field name of the layer" })
524
+ }).strict(),
525
+ z.object({
526
+ kind: z.literal("socrata-resource"),
527
+ pageSize: z.int().min(1)
528
+ }).strict(),
529
+ z.object({
530
+ kind: z.literal("ckan-datastore"),
531
+ pageSize: z.int().min(1)
532
+ }).strict()
533
+ ]);
525
534
  /** How to obtain the dataset: a fetchable recipe naming the source (by index into `sources`), the archive layout and the parser, or a statement that no declarative recipe exists yet and why. */
526
535
  const RecipeSchema = z.discriminatedUnion("status", [z.object({
527
536
  status: z.literal("fetchable"),
@@ -896,7 +905,7 @@ var catalogue_default = {
896
905
  "id": "keepset",
897
906
  "name": "keepset",
898
907
  "description": "The keepset catalogue: datasets with machine-readable recipes that fetch each one and normalise it into its category's target shape.",
899
- "version": "1.9.0",
908
+ "version": "1.10.0",
900
909
  "repository": "https://github.com/Mearman/keepset",
901
910
  "namespaceAliases": {
902
911
  "networkrepository": ["nr"],
@@ -1319,6 +1328,10 @@ var catalogue_default = {
1319
1328
  "recipe": {
1320
1329
  "status": "fetchable",
1321
1330
  "source": 1,
1331
+ "paging": {
1332
+ "kind": "ckan-datastore",
1333
+ "pageSize": 5e4
1334
+ },
1322
1335
  "archive": { "kind": "none" },
1323
1336
  "parser": {
1324
1337
  "format": "csv-table",
@@ -3430,7 +3443,7 @@ var catalogue_default = {
3430
3443
  "citations": [],
3431
3444
  "recipe": {
3432
3445
  "status": "not-fetchable",
3433
- "reason": "The bulk download is a zip of 355,930,383 bytes covering every product, which is larger than a recipe can read, and the debt collection subset is only reachable through a search API."
3446
+ "reason": "The bulk download is a zip of 355,930,383 bytes covering every product, and the debt collection subset is only reachable through a search API that keepset cannot page."
3434
3447
  },
3435
3448
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
3436
3449
  "expectedCounts": {},
@@ -4824,7 +4837,7 @@ var catalogue_default = {
4824
4837
  "citations": [],
4825
4838
  "recipe": {
4826
4839
  "status": "not-fetchable",
4827
- "reason": "The CSV starts with a title line and a production note before any column header, so it is not a rectangular table with the header on its first line."
4840
+ "reason": "The CSV has a title and a production note above its header and its address contains the month it was produced, so it changes every month and has no stable download URL."
4828
4841
  },
4829
4842
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
4830
4843
  "expectedCounts": {},
@@ -5469,7 +5482,7 @@ var catalogue_default = {
5469
5482
  "citations": [],
5470
5483
  "recipe": {
5471
5484
  "status": "not-fetchable",
5472
- "reason": "The survey URL is a landing page and no direct download could be verified, because the files sit on a host whose TLS connection failed."
5485
+ "reason": "The yearly download is a zip holding a ten sheet xlsx workbook with a title row above each header, durations as Excel day fractions and outlet positions only as National Grid references, so a workbook inside a zip with no coordinates in degrees has no recipe; the host the landing page names has no address, so the files are fetched through the catalogue's download API."
5473
5486
  },
5474
5487
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
5475
5488
  "expectedCounts": {},
@@ -5499,7 +5512,7 @@ var catalogue_default = {
5499
5512
  "citations": [],
5500
5513
  "recipe": {
5501
5514
  "status": "not-fetchable",
5502
- "reason": "The survey URL is a landing page; the zip it names is hosted on api.agrimetrics.co.uk, whose TLS connection failed during the survey, so no direct download or table could be verified, and the page lists the format as ODS (a spreadsheet) which keepset has no parser for yet."
5515
+ "reason": "The download is a zip holding one OpenDocument spreadsheet whose member name changes every quarter, and keepset reads xlsx and not ods."
5503
5516
  },
5504
5517
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
5505
5518
  "expectedCounts": {},
@@ -6406,7 +6419,7 @@ var catalogue_default = {
6406
6419
  "citations": [],
6407
6420
  "recipe": {
6408
6421
  "status": "not-fetchable",
6409
- "reason": "The data is served only as JSON pages by the OpenFEMA API (the survey page itself refuses automated requests), keepset has no JSON paging parser yet, and the successor v3 table holds 2725989 records, more than a recipe can read."
6422
+ "reason": "The OpenFEMA v3 API pages claims anonymously as CSV, but the table has millions of rows whose locations are rounded to a tenth of a degree, the terms of use limit it to statistical use without reidentification, and keepset has no recipe paging for this API."
6410
6423
  },
6411
6424
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
6412
6425
  "expectedCounts": {},
@@ -6657,7 +6670,7 @@ var catalogue_default = {
6657
6670
  "citations": [],
6658
6671
  "recipe": {
6659
6672
  "status": "not-fetchable",
6660
- "reason": "The download is a zip of shapefile layers, and keepset has no shapefile parser yet."
6673
+ "reason": "The download is a per-request zip of a point shapefile (the attributes are in a dbf and the positions in a separate shp, in a projected system) with no CSV or GeoJSON route, and keepset has no shapefile parser."
6661
6674
  },
6662
6675
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
6663
6676
  "expectedCounts": {},
@@ -6905,7 +6918,7 @@ var catalogue_default = {
6905
6918
  "citations": [],
6906
6919
  "recipe": {
6907
6920
  "status": "not-fetchable",
6908
- "reason": "The instances are keyword-format text files that keepset has no parser for yet, and dataset.csv only indexes the selected instances."
6921
+ "reason": "The instances are keyword format text files of matrices (a collection of many, indexed by dataset.csv), and a collection is more than one or/v1 document, so no recipe reads it."
6909
6922
  },
6910
6923
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
6911
6924
  "expectedCounts": {},
@@ -6933,7 +6946,7 @@ var catalogue_default = {
6933
6946
  "citations": [],
6934
6947
  "recipe": {
6935
6948
  "status": "not-fetchable",
6936
- "reason": "The instances are keyword-format text files (and semicolon-delimited copies with the same content) that keepset has no parser for yet."
6949
+ "reason": "The instances are keyword format text files in a collection mirrored by a student without a licence statement, and a collection is more than one or/v1 document, so no recipe reads it."
6937
6950
  },
6938
6951
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
6939
6952
  "expectedCounts": {},
@@ -6965,7 +6978,7 @@ var catalogue_default = {
6965
6978
  "citations": [],
6966
6979
  "recipe": {
6967
6980
  "status": "not-fetchable",
6968
- "reason": "The plain CSV probed holds only an index, date, dispatch time and order id with no mappable field, and the coordinate data sits in a zip that keepset cannot read."
6981
+ "reason": "The data is released under CC BY-NC 4.0 with a bar on passing it to third parties without permission, and its coordinates are shifted by the publisher, so they are not WGS84 positions."
6969
6982
  },
6970
6983
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
6971
6984
  "expectedCounts": {},
@@ -6993,7 +7006,7 @@ var catalogue_default = {
6993
7006
  "citations": [],
6994
7007
  "recipe": {
6995
7008
  "status": "not-fetchable",
6996
- "reason": "The only data file is an xls spreadsheet and keepset has no spreadsheet parser yet."
7009
+ "reason": "The only data file is an old binary xls workbook, and keepset reads only xlsx."
6997
7010
  },
6998
7011
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
6999
7012
  "expectedCounts": {},
@@ -7029,7 +7042,7 @@ var catalogue_default = {
7029
7042
  "citations": [],
7030
7043
  "recipe": {
7031
7044
  "status": "not-fetchable",
7032
- "reason": "The download is a zip of Solomon-style instance text files and tab-separated matrices, and keepset has no parser for that format yet."
7045
+ "reason": "The download is a collection of Solomon style instance files and distance matrices, and a collection is more than one or/v1 document, so no recipe reads it."
7033
7046
  },
7034
7047
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
7035
7048
  "expectedCounts": {},
@@ -7928,6 +7941,10 @@ var catalogue_default = {
7928
7941
  "url": "https://www.kaggle.com/datasets/dharun4772/doordash-eta-prediction",
7929
7942
  "retrieved": "2026-10-19",
7930
7943
  "status": "live"
7944
+ }, {
7945
+ "url": "https://www.kaggle.com/api/v1/datasets/download/dharun4772/doordash-eta-prediction",
7946
+ "retrieved": "2026-10-19",
7947
+ "status": "live"
7931
7948
  }],
7932
7949
  "licence": {
7933
7950
  "kind": "spdx",
@@ -7935,10 +7952,83 @@ var catalogue_default = {
7935
7952
  },
7936
7953
  "citations": [],
7937
7954
  "recipe": {
7938
- "status": "not-fetchable",
7939
- "reason": "The download is a zip archive holding one CSV and keepset has no archive handling yet, so the table cannot be read directly from the download."
7955
+ "status": "fetchable",
7956
+ "source": 1,
7957
+ "archive": {
7958
+ "kind": "zip",
7959
+ "member": { "path": "historical_data.csv" }
7960
+ },
7961
+ "parser": {
7962
+ "format": "csv-table",
7963
+ "encoding": "utf-8",
7964
+ "delimiter": "comma",
7965
+ "skipLines": 0,
7966
+ "idColumn": null,
7967
+ "numericColumns": [{
7968
+ "column": "estimated_order_place_duration",
7969
+ "factor": 1
7970
+ }, {
7971
+ "column": "estimated_store_to_consumer_driving_duration",
7972
+ "factor": 1
7973
+ }],
7974
+ "columns": [
7975
+ "actual_delivery_time",
7976
+ "estimated_order_place_duration",
7977
+ "created_at",
7978
+ "store_primary_category",
7979
+ "market_id",
7980
+ "estimated_store_to_consumer_driving_duration"
7981
+ ],
7982
+ "nullValues": ["NA"]
7983
+ }
7984
+ },
7985
+ "crosswalk": {
7986
+ "source": "csv-table",
7987
+ "target": "or/v1",
7988
+ "rules": [
7989
+ {
7990
+ "op": "move",
7991
+ "from": "meta",
7992
+ "to": "meta"
7993
+ },
7994
+ {
7995
+ "op": "move",
7996
+ "from": "nodes[].id",
7997
+ "to": "nodes[].id"
7998
+ },
7999
+ {
8000
+ "op": "move",
8001
+ "from": "nodes[].actual_delivery_time",
8002
+ "to": "nodes[].closed"
8003
+ },
8004
+ {
8005
+ "op": "move",
8006
+ "from": "nodes[].estimated_order_place_duration",
8007
+ "to": "nodes[].duration"
8008
+ },
8009
+ {
8010
+ "op": "move",
8011
+ "from": "nodes[].created_at",
8012
+ "to": "nodes[].opened"
8013
+ },
8014
+ {
8015
+ "op": "move",
8016
+ "from": "nodes[].store_primary_category",
8017
+ "to": "nodes[].skill"
8018
+ },
8019
+ {
8020
+ "op": "move",
8021
+ "from": "nodes[].market_id",
8022
+ "to": "nodes[].territory"
8023
+ },
8024
+ {
8025
+ "op": "move",
8026
+ "from": "nodes[].estimated_store_to_consumer_driving_duration",
8027
+ "to": "nodes[].travelTime"
8028
+ }
8029
+ ],
8030
+ "oneWay": []
7940
8031
  },
7941
- "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
7942
8032
  "expectedCounts": {},
7943
8033
  "properties": { "surveyId": "or-31" }
7944
8034
  },
@@ -7967,7 +8057,7 @@ var catalogue_default = {
7967
8057
  "citations": [],
7968
8058
  "recipe": {
7969
8059
  "status": "not-fetchable",
7970
- "reason": "The download is a zip archive holding one CSV and keepset has no archive handling yet, so the table cannot be read directly from the download."
8060
+ "reason": "The records are patient level health data under CC BY-NC-SA 4.0 with no location column."
7971
8061
  },
7972
8062
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
7973
8063
  "expectedCounts": {},
@@ -8325,7 +8415,7 @@ var catalogue_default = {
8325
8415
  "citations": [],
8326
8416
  "recipe": {
8327
8417
  "status": "not-fetchable",
8328
- "reason": "The download is a zip of instance files in a plain text format with a small header and no column names, and keepset has no parser for it yet."
8418
+ "reason": "The download is a collection of 60 text instance files of one 100 node point set under different budgets (planar coordinates and a prize, with no time quantities), and a collection is more than one or/v1 document, so no recipe reads it."
8329
8419
  },
8330
8420
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
8331
8421
  "expectedCounts": {},
@@ -8359,7 +8449,7 @@ var catalogue_default = {
8359
8449
  "citations": [],
8360
8450
  "recipe": {
8361
8451
  "status": "not-fetchable",
8362
- "reason": "The download is a zip of instance files in a Solomon style text format with no column names, and keepset has no parser for it yet."
8452
+ "reason": "The download is a collection of Solomon style time window instance files (planar coordinates, service durations, prizes and windows in abstract units, not seconds), and a collection is more than one or/v1 document, so no recipe reads it."
8363
8453
  },
8364
8454
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
8365
8455
  "expectedCounts": {},
@@ -8426,7 +8516,7 @@ var catalogue_default = {
8426
8516
  "citations": [],
8427
8517
  "recipe": {
8428
8518
  "status": "not-fetchable",
8429
- "reason": "The download is a zip of plain-text Solomon instance files and keepset has no parser for that format yet."
8519
+ "reason": "The download is a collection of Solomon instance files (planar coordinates, demands, service times and windows in abstract units, not seconds), and a collection is more than one or/v1 document, so no recipe reads it."
8430
8520
  },
8431
8521
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
8432
8522
  "expectedCounts": {},
@@ -8544,7 +8634,7 @@ var catalogue_default = {
8544
8634
  "citations": [],
8545
8635
  "recipe": {
8546
8636
  "status": "not-fetchable",
8547
- "reason": "The CSV has a title line (FPN Detail) above the real header, so it does not have its header on the first line and is not a plain table."
8637
+ "reason": "Each quarter is its own file at its own tokenised address, the later quarters are xlsx, and each CSV has a title row above its header and a summary block and blank rows below its data."
8548
8638
  },
8549
8639
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
8550
8640
  "expectedCounts": {},
@@ -9540,7 +9630,7 @@ var catalogue_default = {
9540
9630
  "citations": [],
9541
9631
  "recipe": {
9542
9632
  "status": "not-fetchable",
9543
- "reason": "The downloads are MATLAB data files and keepset has no parser for them yet."
9633
+ "reason": "The downloads are MATLAB data files and keepset has no parser for them."
9544
9634
  },
9545
9635
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
9546
9636
  "expectedCounts": {},
@@ -9579,7 +9669,7 @@ var catalogue_default = {
9579
9669
  "citations": [],
9580
9670
  "recipe": {
9581
9671
  "status": "not-fetchable",
9582
- "reason": "The download is a spreadsheet (xlsx) with task, team and distance matrix sheets and no coordinates, and keepset has no spreadsheet parser yet."
9672
+ "reason": "The download is hundreds of small Excel workbooks whose task sheets have no coordinates (places exist only as distance matrices) and whose file names repeat across folders, so no recipe can name one table."
9583
9673
  },
9584
9674
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
9585
9675
  "expectedCounts": {},
@@ -9610,7 +9700,7 @@ var catalogue_default = {
9610
9700
  "citations": [],
9611
9701
  "recipe": {
9612
9702
  "status": "not-fetchable",
9613
- "reason": "The only file is an Excel workbook of results and keepset has no spreadsheet parser yet."
9703
+ "reason": "The only file is an Excel workbook of aggregate results per solved instance, with no row for a place or a task, so there are no nodes to map."
9614
9704
  },
9615
9705
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
9616
9706
  "expectedCounts": {},
@@ -9674,7 +9764,7 @@ var catalogue_default = {
9674
9764
  "citations": [],
9675
9765
  "recipe": {
9676
9766
  "status": "not-fetchable",
9677
- "reason": "The download is a RAR archive of macro-enabled Excel workbooks and keepset has no archive or spreadsheet parser yet."
9767
+ "reason": "The download is a RAR archive of macro enabled Excel workbooks, and keepset reads neither RAR archives nor macro workbooks."
9678
9768
  },
9679
9769
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
9680
9770
  "expectedCounts": {},
@@ -9709,7 +9799,7 @@ var catalogue_default = {
9709
9799
  "citations": [],
9710
9800
  "recipe": {
9711
9801
  "status": "not-fetchable",
9712
- "reason": "The only file is an xlsx workbook of about 19.7 MB, and keepset has no spreadsheet parser yet."
9802
+ "reason": "The only file is an Excel workbook with a row per stop visit of a route that holds minute offsets and distances between stops and no location, so the only node field it could fill is a time window and a fetch would carry no place."
9713
9803
  },
9714
9804
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
9715
9805
  "expectedCounts": {},
@@ -16620,7 +16710,7 @@ var catalogue_default = {
16620
16710
  "citations": [],
16621
16711
  "recipe": {
16622
16712
  "status": "not-fetchable",
16623
- "reason": "The instances are keyword-format text files on a Wayback Machine snapshot and keepset has no parser for them yet."
16713
+ "reason": "The instances are keyword format text files on a Wayback Machine snapshot, a collection of separate files, and a collection is more than one or/v1 document, so no recipe reads it."
16624
16714
  },
16625
16715
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
16626
16716
  "expectedCounts": {},
@@ -16647,7 +16737,7 @@ var catalogue_default = {
16647
16737
  "citations": [],
16648
16738
  "recipe": {
16649
16739
  "status": "not-fetchable",
16650
- "reason": "The source is an SRU search endpoint that returns XML notices in a query language, not a table, and keepset has no parser for it."
16740
+ "reason": "The source is an SRU search endpoint that returns XML notices in a query language and not a table, and keepset has no parser for it."
16651
16741
  },
16652
16742
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
16653
16743
  "expectedCounts": {},
@@ -20723,6 +20813,10 @@ var catalogue_default = {
20723
20813
  "url": "https://www.data.qld.gov.au/dataset/unclaimed-monies",
20724
20814
  "retrieved": "2026-10-19",
20725
20815
  "status": "live"
20816
+ }, {
20817
+ "url": "https://www.data.qld.gov.au/datastore/dump/872065ae-ddfd-4b5f-ad15-e1935dadd883",
20818
+ "retrieved": "2026-10-19",
20819
+ "status": "live"
20726
20820
  }],
20727
20821
  "licence": {
20728
20822
  "kind": "spdx",
@@ -20730,10 +20824,62 @@ var catalogue_default = {
20730
20824
  },
20731
20825
  "citations": [],
20732
20826
  "recipe": {
20733
- "status": "not-fetchable",
20734
- "reason": "The CSV download answers with an AWS WAF challenge and an empty body, the CKAN datastore API is paged JSON that keepset cannot read yet, and the table has 2553652 records, more than a recipe can read."
20827
+ "status": "fetchable",
20828
+ "source": 1,
20829
+ "paging": {
20830
+ "kind": "ckan-datastore",
20831
+ "pageSize": 5e4
20832
+ },
20833
+ "archive": { "kind": "none" },
20834
+ "parser": {
20835
+ "format": "csv-table",
20836
+ "encoding": "utf-8",
20837
+ "delimiter": "comma",
20838
+ "skipLines": 0,
20839
+ "idColumn": "id",
20840
+ "numericColumns": [{
20841
+ "column": "Amount",
20842
+ "factor": 1
20843
+ }],
20844
+ "columns": [
20845
+ "DateRec",
20846
+ "Amount",
20847
+ "PCode"
20848
+ ]
20849
+ }
20850
+ },
20851
+ "crosswalk": {
20852
+ "source": "csv-table",
20853
+ "target": "or/v1",
20854
+ "rules": [
20855
+ {
20856
+ "op": "move",
20857
+ "from": "meta",
20858
+ "to": "meta"
20859
+ },
20860
+ {
20861
+ "op": "move",
20862
+ "from": "nodes[].id",
20863
+ "to": "nodes[].id"
20864
+ },
20865
+ {
20866
+ "op": "move",
20867
+ "from": "nodes[].DateRec",
20868
+ "to": "nodes[].opened"
20869
+ },
20870
+ {
20871
+ "op": "move",
20872
+ "from": "nodes[].Amount",
20873
+ "to": "nodes[].prize"
20874
+ },
20875
+ {
20876
+ "op": "move",
20877
+ "from": "nodes[].PCode",
20878
+ "to": "nodes[].territory"
20879
+ }
20880
+ ],
20881
+ "oneWay": []
20735
20882
  },
20736
- "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
20737
20883
  "expectedCounts": {},
20738
20884
  "properties": { "surveyId": "or-124" }
20739
20885
  },
@@ -21321,7 +21467,7 @@ var catalogue_default = {
21321
21467
  "citations": [],
21322
21468
  "recipe": {
21323
21469
  "status": "not-fetchable",
21324
- "reason": "The download is a set of zip archives of CSV files, one per financial year and report type with differing columns, and keepset has no archive parser yet."
21470
+ "reason": "The portal answered a bot challenge to automated requests for the table dumps when probed, and the data is a file per financial year and report type with different columns before and after 2022, so no single recipe covers the dataset."
21325
21471
  },
21326
21472
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
21327
21473
  "expectedCounts": {},
@@ -55709,7 +55855,7 @@ var catalogue_default = {
55709
55855
  "citations": [],
55710
55856
  "recipe": {
55711
55857
  "status": "not-fetchable",
55712
- "reason": "The download is a zip of TSPLIB-style .cop instance files plus a readme and an xlsx workbook, and keepset has no parser for that format yet."
55858
+ "reason": "The download is a collection of TSPLIB style instance files whose prizes belong to overlapping clusters of nodes and not to nodes, and a collection is more than one or/v1 document, so no recipe reads it."
55713
55859
  },
55714
55860
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
55715
55861
  "expectedCounts": {},
@@ -55737,7 +55883,7 @@ var catalogue_default = {
55737
55883
  "citations": [],
55738
55884
  "recipe": {
55739
55885
  "status": "not-fetchable",
55740
- "reason": "The download is a zip holding many separate coordinate files plus a readme, and keepset has no zip handling for a recipe yet."
55886
+ "reason": "The download is a collection of separate coordinate files plus a readme, and a collection is more than one or/v1 document, so no recipe reads it."
55741
55887
  },
55742
55888
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
55743
55889
  "expectedCounts": {},
@@ -55765,7 +55911,7 @@ var catalogue_default = {
55765
55911
  "citations": [],
55766
55912
  "recipe": {
55767
55913
  "status": "not-fetchable",
55768
- "reason": "The download is a zip of TSPLIB-style text files with a POP section and keepset has no parser for it yet."
55914
+ "reason": "The download is a collection of TSPLIB style instance files with a prize and probability section, half of which carry no coordinates and some of which use the degrees and minutes form, and a collection is more than one or/v1 document, so no recipe reads it."
55769
55915
  },
55770
55916
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
55771
55917
  "expectedCounts": {},
@@ -55794,7 +55940,7 @@ var catalogue_default = {
55794
55940
  "citations": [],
55795
55941
  "recipe": {
55796
55942
  "status": "not-fetchable",
55797
- "reason": "The download is a zip of instance files in the VRP-REP XML format and keepset has no parser for it yet."
55943
+ "reason": "The download is a collection of VRP-REP XML instance files holding coordinates and skills but no times, and a collection is more than one or/v1 document, so no recipe reads it."
55798
55944
  },
55799
55945
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
55800
55946
  "expectedCounts": {},
@@ -56854,7 +57000,7 @@ var catalogue_default = {
56854
57000
  "citations": [],
56855
57001
  "recipe": {
56856
57002
  "status": "not-fetchable",
56857
- "reason": "The download is a zip of plain-text arc-routing instance files (customers, network and links) described by a readme, and keepset has no parser for that format yet."
57003
+ "reason": "The download is one arc routing instance in two tab separated text files with no column names, holding only planar coordinates of customers and a road network, so there is no field of the or/v1 node beyond a position and no table the parser reads."
56858
57004
  },
56859
57005
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
56860
57006
  "expectedCounts": {},
@@ -56890,7 +57036,7 @@ var catalogue_default = {
56890
57036
  "citations": [],
56891
57037
  "recipe": {
56892
57038
  "status": "not-fetchable",
56893
- "reason": "The download is a zip of Excel (xlsx) files holding distance and time matrices, and keepset has no spreadsheet parser yet."
57039
+ "reason": "The download is a zip of Excel workbooks that hold distance and time matrices only, so there are no nodes to map, and keepset reads a sheet of a workbook, not a workbook inside a zip."
56894
57040
  },
56895
57041
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
56896
57042
  "expectedCounts": {},
@@ -56925,7 +57071,7 @@ var catalogue_default = {
56925
57071
  "citations": [],
56926
57072
  "recipe": {
56927
57073
  "status": "not-fetchable",
56928
- "reason": "Zenodo refused automated requests, so the file list and formats could not be seen and no direct download could be verified."
57074
+ "reason": "The Zenodo files are a custom sectioned text format (technicians, tasks and a travel time matrix of millions of lines) and not a table, and two of them are collections of instance files."
56929
57075
  },
56930
57076
  "crosswalk": { "file": "crosswalks/or-v1/nodes.json" },
56931
57077
  "expectedCounts": {},
@@ -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">;
@@ -179,6 +180,9 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
179
180
  }, z.core.$strict>, z.ZodObject<{
180
181
  kind: z.ZodLiteral<"socrata-resource">;
181
182
  pageSize: z.ZodInt;
183
+ }, z.core.$strict>, z.ZodObject<{
184
+ kind: z.ZodLiteral<"ckan-datastore">;
185
+ pageSize: z.ZodInt;
182
186
  }, z.core.$strict>], "kind">>;
183
187
  archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
184
188
  kind: z.ZodLiteral<"none">;
@@ -262,6 +266,7 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
262
266
  factor: z.ZodNumber;
263
267
  }, z.core.$strict>>;
264
268
  columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
269
+ nullValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
265
270
  }, z.core.$strict>, z.ZodObject<{
266
271
  format: z.ZodLiteral<"geojson">;
267
272
  encoding: z.ZodLiteral<"utf-8">;
@@ -338,6 +343,9 @@ declare const EntrySchema: z.ZodObject<{
338
343
  }, z.core.$strict>, z.ZodObject<{
339
344
  kind: z.ZodLiteral<"socrata-resource">;
340
345
  pageSize: z.ZodInt;
346
+ }, z.core.$strict>, z.ZodObject<{
347
+ kind: z.ZodLiteral<"ckan-datastore">;
348
+ pageSize: z.ZodInt;
341
349
  }, z.core.$strict>], "kind">>;
342
350
  archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
343
351
  kind: z.ZodLiteral<"none">;
@@ -421,6 +429,7 @@ declare const EntrySchema: z.ZodObject<{
421
429
  factor: z.ZodNumber;
422
430
  }, z.core.$strict>>;
423
431
  columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
432
+ nullValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
424
433
  }, z.core.$strict>, z.ZodObject<{
425
434
  format: z.ZodLiteral<"geojson">;
426
435
  encoding: z.ZodLiteral<"utf-8">;
@@ -524,6 +533,7 @@ declare const EntrySchema: z.ZodObject<{
524
533
  factor: number;
525
534
  }[];
526
535
  columns?: string[] | undefined;
536
+ nullValues?: string[] | undefined;
527
537
  } | {
528
538
  format: "geojson";
529
539
  encoding: "utf-8";
@@ -550,6 +560,9 @@ declare const EntrySchema: z.ZodObject<{
550
560
  } | {
551
561
  kind: "socrata-resource";
552
562
  pageSize: number;
563
+ } | {
564
+ kind: "ckan-datastore";
565
+ pageSize: number;
553
566
  } | undefined;
554
567
  } | {
555
568
  status: "not-fetchable";
@@ -707,6 +720,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
707
720
  }, z.core.$strict>, z.ZodObject<{
708
721
  kind: z.ZodLiteral<"socrata-resource">;
709
722
  pageSize: z.ZodInt;
723
+ }, z.core.$strict>, z.ZodObject<{
724
+ kind: z.ZodLiteral<"ckan-datastore">;
725
+ pageSize: z.ZodInt;
710
726
  }, z.core.$strict>], "kind">>;
711
727
  archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
712
728
  kind: z.ZodLiteral<"none">;
@@ -790,6 +806,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
790
806
  factor: z.ZodNumber;
791
807
  }, z.core.$strict>>;
792
808
  columns: z.ZodOptional<z.ZodArray<z.ZodString>>;
809
+ nullValues: z.ZodOptional<z.ZodArray<z.ZodString>>;
793
810
  }, z.core.$strict>, z.ZodObject<{
794
811
  format: z.ZodLiteral<"geojson">;
795
812
  encoding: z.ZodLiteral<"utf-8">;
@@ -893,6 +910,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
893
910
  factor: number;
894
911
  }[];
895
912
  columns?: string[] | undefined;
913
+ nullValues?: string[] | undefined;
896
914
  } | {
897
915
  format: "geojson";
898
916
  encoding: "utf-8";
@@ -919,6 +937,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
919
937
  } | {
920
938
  kind: "socrata-resource";
921
939
  pageSize: number;
940
+ } | {
941
+ kind: "ckan-datastore";
942
+ pageSize: number;
922
943
  } | undefined;
923
944
  } | {
924
945
  status: "not-fetchable";
@@ -1031,6 +1052,7 @@ declare const CatalogueIndexSchema: z.ZodObject<{
1031
1052
  factor: number;
1032
1053
  }[];
1033
1054
  columns?: string[] | undefined;
1055
+ nullValues?: string[] | undefined;
1034
1056
  } | {
1035
1057
  format: "geojson";
1036
1058
  encoding: "utf-8";
@@ -1057,6 +1079,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
1057
1079
  } | {
1058
1080
  kind: "socrata-resource";
1059
1081
  pageSize: number;
1082
+ } | {
1083
+ kind: "ckan-datastore";
1084
+ pageSize: number;
1060
1085
  } | undefined;
1061
1086
  } | {
1062
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-frr8n_SI.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-NjZZ73vT.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-frr8n_SI.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-NjZZ73vT.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";
@@ -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({
@@ -1596,8 +1598,8 @@ async function getBytes(port, url, signal) {
1596
1598
  return new Uint8Array(await response.arrayBuffer());
1597
1599
  }
1598
1600
  /**
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
+ * 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.
1601
1603
  * @param held - The pages read so far, each after the first without its header record.
1602
1604
  * @param offset - How many rows the pages hold.
1603
1605
  * @returns Every page.
@@ -1607,7 +1609,7 @@ async function readResourcePages(read, held, offset) {
1607
1609
  if (offset >= read.count) return held;
1608
1610
  const expected = Math.min(read.pageSize, read.count - offset);
1609
1611
  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);
1612
+ const bytes = await getBytes(read.port, read.pageUrl(offset), read.signal);
1611
1613
  const rows = recordsIn(bytes) - 1;
1612
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`);
1613
1615
  return bytes;
@@ -1618,6 +1620,17 @@ async function readResourcePages(read, held, offset) {
1618
1620
  return readResourcePages(read, [...held, first === void 0 ? page : page.subarray(headerEnd)], offset + expected);
1619
1621
  }
1620
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
+ /**
1621
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.
1622
1635
  * @param resource - The resource's URL, such as `https://data.example.org/resource/abcd-1234.csv`, without a query.
1623
1636
  * @param pageSize - How many rows a page holds.
@@ -1629,15 +1642,45 @@ async function readResourcePages(read, held, offset) {
1629
1642
  async function downloadSocrataResource(resource, pageSize, port, signal) {
1630
1643
  const reported = /^"count"\r?\n"(\d+)"/.exec(new TextDecoder("utf-8").decode(await getBytes(port, `${resource}?%24select=count(*)`, signal)));
1631
1644
  if (reported?.[1] === void 0) throw new KeepsetError(`${resource} did not report a row count`);
1632
- const pages = await readResourcePages({
1645
+ return downloadPages({
1633
1646
  resource,
1647
+ pageUrl: (offset) => `${resource}?%24order=%3Aid&%24limit=${String(pageSize)}&%24offset=${String(offset)}`,
1634
1648
  pageSize,
1635
1649
  count: Number(reported[1]),
1636
1650
  port,
1637
1651
  signal
1638
- }, [], 0);
1639
- if (pages.length === 0) throw new KeepsetError(`${resource} has no rows`);
1640
- return Buffer.concat(pages);
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
+ });
1641
1684
  }
1642
1685
  /**
1643
1686
  * Downloads a paged source as the one file its pages make, in the way its recipe's paging says.
@@ -1649,7 +1692,8 @@ async function downloadSocrataResource(resource, pageSize, port, signal) {
1649
1692
  * @throws KeepsetError when a page cannot be read or the pages do not add up to what the source reports.
1650
1693
  */
1651
1694
  async function downloadPaged(url, paging, port, signal) {
1652
- return paging.kind === "arcgis-query" ? downloadArcgisLayer(url, paging.orderBy, port, signal) : downloadSocrataResource(url, paging.pageSize, 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);
1653
1697
  }
1654
1698
  //#endregion
1655
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.21.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.21.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"},{"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
+ {"$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.21.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"}