@uk-open-data-connectors/uk-sources 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -0
- package/dist/ancientWoodland.d.ts +102 -0
- package/dist/ancientWoodland.d.ts.map +1 -0
- package/dist/ancientWoodland.js +245 -0
- package/dist/ancientWoodland.js.map +1 -0
- package/dist/bankRate.d.ts +107 -0
- package/dist/bankRate.d.ts.map +1 -0
- package/dist/bankRate.js +251 -0
- package/dist/bankRate.js.map +1 -0
- package/dist/carbonIntensity.d.ts +151 -0
- package/dist/carbonIntensity.d.ts.map +1 -0
- package/dist/carbonIntensity.js +257 -0
- package/dist/carbonIntensity.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/fixtures.d.ts +5 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +11 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/floodMonitoring.d.ts +70 -0
- package/dist/floodMonitoring.d.ts.map +1 -0
- package/dist/floodMonitoring.js +164 -0
- package/dist/floodMonitoring.js.map +1 -0
- package/dist/foodHygiene.d.ts +68 -0
- package/dist/foodHygiene.d.ts.map +1 -0
- package/dist/foodHygiene.js +103 -0
- package/dist/foodHygiene.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/onsDatasets.d.ts +67 -0
- package/dist/onsDatasets.d.ts.map +1 -0
- package/dist/onsDatasets.js +111 -0
- package/dist/onsDatasets.js.map +1 -0
- package/dist/parliamentSeats.d.ts +115 -0
- package/dist/parliamentSeats.d.ts.map +1 -0
- package/dist/parliamentSeats.js +202 -0
- package/dist/parliamentSeats.js.map +1 -0
- package/dist/planningDatasets.d.ts +65 -0
- package/dist/planningDatasets.d.ts.map +1 -0
- package/dist/planningDatasets.js +100 -0
- package/dist/planningDatasets.js.map +1 -0
- package/dist/policeCrimes.d.ts +152 -0
- package/dist/policeCrimes.d.ts.map +1 -0
- package/dist/policeCrimes.js +262 -0
- package/dist/policeCrimes.js.map +1 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +67 -0
- package/dist/registry.js.map +1 -0
- package/dist/tflBikePoints.d.ts +65 -0
- package/dist/tflBikePoints.d.ts.map +1 -0
- package/dist/tflBikePoints.js +149 -0
- package/dist/tflBikePoints.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { UkSourceApiError, UkSourceParseError } from './errors.js';
|
|
3
|
+
import { readFixtureJson } from './fixtures.js';
|
|
4
|
+
/**
|
|
5
|
+
* The state of the parties in each house of the UK Parliament, from the
|
|
6
|
+
* Members API. The API answers without a key and Parliament publishes the
|
|
7
|
+
* data under the Open Parliament Licence v3.0:
|
|
8
|
+
* <https://www.parliament.uk/site-information/copyright-parliament/open-parliament-licence/>
|
|
9
|
+
*/
|
|
10
|
+
export const PARLIAMENT_SEATS_SOURCE_ID = 'parliament-seats';
|
|
11
|
+
/** Root every call here is built from. */
|
|
12
|
+
export const PARLIAMENT_MEMBERS_API_BASE_URL = 'https://members-api.parliament.uk/api';
|
|
13
|
+
/** Path for the state of the parties in one house on one date. */
|
|
14
|
+
export const PARLIAMENT_STATE_OF_PARTIES_PATH = 'Parties/StateOfTheParties';
|
|
15
|
+
/**
|
|
16
|
+
* The house number the API uses for the Commons. The Swagger schema behind
|
|
17
|
+
* `House` carries exactly two values: 1 for the Commons, 2 for the Lords.
|
|
18
|
+
*/
|
|
19
|
+
export const PARLIAMENT_COMMONS_HOUSE = 1;
|
|
20
|
+
/** The house number the API uses for the Lords. */
|
|
21
|
+
export const PARLIAMENT_LORDS_HOUSE = 2;
|
|
22
|
+
const PARLIAMENT_PARTY_SCHEMA = z.object({
|
|
23
|
+
id: z.number().int(),
|
|
24
|
+
name: z.string().nullable(),
|
|
25
|
+
abbreviation: z.string().nullable(),
|
|
26
|
+
backgroundColour: z.string().nullable(),
|
|
27
|
+
foregroundColour: z.string().nullable(),
|
|
28
|
+
isIndependentParty: z.boolean(),
|
|
29
|
+
});
|
|
30
|
+
const PARLIAMENT_SEAT_COUNT_SCHEMA = z.object({
|
|
31
|
+
male: z.number().int().nonnegative().nullable(),
|
|
32
|
+
female: z.number().int().nonnegative().nullable(),
|
|
33
|
+
nonBinary: z.number().int().nonnegative().nullable(),
|
|
34
|
+
total: z.number().int(),
|
|
35
|
+
party: PARLIAMENT_PARTY_SCHEMA.nullable(),
|
|
36
|
+
});
|
|
37
|
+
const PARLIAMENT_RESPONSE_SCHEMA = z.object({
|
|
38
|
+
items: z.array(z.object({ value: PARLIAMENT_SEAT_COUNT_SCHEMA.nullable() })).nullable(),
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Formats a date as the `YYYY-MM-DD` the path takes, in UK local time.
|
|
42
|
+
*
|
|
43
|
+
* @param date - the instant to format
|
|
44
|
+
* @returns the date the API reads, for example `2026-10-01`
|
|
45
|
+
*/
|
|
46
|
+
export function formatParliamentQueryDate(date) {
|
|
47
|
+
return new Intl.DateTimeFormat('en-CA', {
|
|
48
|
+
year: 'numeric',
|
|
49
|
+
month: '2-digit',
|
|
50
|
+
day: '2-digit',
|
|
51
|
+
timeZone: 'Europe/London',
|
|
52
|
+
}).format(date);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds the state-of-the-parties URL for one house and one date.
|
|
56
|
+
*
|
|
57
|
+
* @param house - 1 for the Commons, 2 for the Lords
|
|
58
|
+
* @param forDate - the date to ask for, as `YYYY-MM-DD`
|
|
59
|
+
* @returns the full URL
|
|
60
|
+
*/
|
|
61
|
+
export function buildParliamentSeatsUrl(house, forDate) {
|
|
62
|
+
return `${PARLIAMENT_MEMBERS_API_BASE_URL}/${PARLIAMENT_STATE_OF_PARTIES_PATH}/${String(house)}/${forDate}`;
|
|
63
|
+
}
|
|
64
|
+
/** Turns one API row into a party with its seats, or throws naming the row. */
|
|
65
|
+
function readParliamentPartySeats(row) {
|
|
66
|
+
const party = row.party;
|
|
67
|
+
if (party === null) {
|
|
68
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, 'a party row carried no party');
|
|
69
|
+
}
|
|
70
|
+
const name = (party.name ?? '').trim();
|
|
71
|
+
if (name === '') {
|
|
72
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, 'a party row carried no name');
|
|
73
|
+
}
|
|
74
|
+
if (row.total <= 0) {
|
|
75
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, `${name} holds ${String(row.total)} seats, which cannot be right`);
|
|
76
|
+
}
|
|
77
|
+
const maleCount = row.male ?? 0;
|
|
78
|
+
const femaleCount = row.female ?? 0;
|
|
79
|
+
const nonBinaryCount = row.nonBinary ?? 0;
|
|
80
|
+
const memberCount = maleCount + femaleCount + nonBinaryCount;
|
|
81
|
+
if (memberCount > row.total) {
|
|
82
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, `${name} names ${String(memberCount)} members across ${String(row.total)} seats`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
party: {
|
|
86
|
+
id: party.id,
|
|
87
|
+
name,
|
|
88
|
+
abbreviation: (party.abbreviation ?? '').trim(),
|
|
89
|
+
backgroundColour: party.backgroundColour,
|
|
90
|
+
foregroundColour: party.foregroundColour,
|
|
91
|
+
isIndependent: party.isIndependentParty,
|
|
92
|
+
},
|
|
93
|
+
seatCount: row.total,
|
|
94
|
+
maleCount,
|
|
95
|
+
femaleCount,
|
|
96
|
+
nonBinaryCount,
|
|
97
|
+
unallocatedSeatCount: row.total - memberCount,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parses a state-of-the-parties payload into the seats each party holds.
|
|
102
|
+
*
|
|
103
|
+
* A row the API cannot explain fails the parse rather than quietly shifting
|
|
104
|
+
* the totals: a missing party, a blank name, a party with no seats, or the
|
|
105
|
+
* same party listed twice all throw.
|
|
106
|
+
*
|
|
107
|
+
* @param payload - the API response, or the committed fixture of one
|
|
108
|
+
* @returns one entry per party, in the order the API sent them
|
|
109
|
+
*/
|
|
110
|
+
export function parseParliamentPartySeats(payload) {
|
|
111
|
+
const parsed = PARLIAMENT_RESPONSE_SCHEMA.safeParse(payload);
|
|
112
|
+
if (!parsed.success) {
|
|
113
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, parsed.error.message);
|
|
114
|
+
}
|
|
115
|
+
const rows = parsed.data.items ?? [];
|
|
116
|
+
if (rows.length === 0) {
|
|
117
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, 'the house returned no parties');
|
|
118
|
+
}
|
|
119
|
+
const seenPartyIds = new Set();
|
|
120
|
+
const seats = rows.map((row) => {
|
|
121
|
+
if (row.value === null) {
|
|
122
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, 'a party row carried no seat count');
|
|
123
|
+
}
|
|
124
|
+
const entry = readParliamentPartySeats(row.value);
|
|
125
|
+
if (seenPartyIds.has(entry.party.id)) {
|
|
126
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, `${entry.party.name} is listed more than once (party ${String(entry.party.id)})`);
|
|
127
|
+
}
|
|
128
|
+
seenPartyIds.add(entry.party.id);
|
|
129
|
+
return entry;
|
|
130
|
+
});
|
|
131
|
+
return seats;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Rolls a party list into the totals a page prints.
|
|
135
|
+
*
|
|
136
|
+
* @param seats - parties as parsed from the payload
|
|
137
|
+
* @returns the seat total, the party count, the majority line, and the ranking
|
|
138
|
+
*/
|
|
139
|
+
export function summarizeParliamentPartySeats(seats) {
|
|
140
|
+
if (seats.length === 0) {
|
|
141
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, 'no parties to summarise');
|
|
142
|
+
}
|
|
143
|
+
const seatCount = seats.reduce((sum, entry) => sum + entry.seatCount, 0);
|
|
144
|
+
const ordered = [...seats].sort((left, right) => {
|
|
145
|
+
if (right.seatCount !== left.seatCount) {
|
|
146
|
+
return right.seatCount - left.seatCount;
|
|
147
|
+
}
|
|
148
|
+
return left.party.name.localeCompare(right.party.name);
|
|
149
|
+
});
|
|
150
|
+
const largestParty = ordered[0];
|
|
151
|
+
if (largestParty === undefined) {
|
|
152
|
+
throw new UkSourceParseError(PARLIAMENT_SEATS_SOURCE_ID, 'no parties to rank');
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
seatCount,
|
|
156
|
+
partyCount: ordered.length,
|
|
157
|
+
majorityThreshold: Math.floor(seatCount / 2) + 1,
|
|
158
|
+
partiesWithOneSeat: ordered.filter((entry) => entry.seatCount === 1).length,
|
|
159
|
+
seats: ordered,
|
|
160
|
+
largestParty,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Parses a state-of-the-parties payload straight into the summary a page prints.
|
|
165
|
+
*
|
|
166
|
+
* @param payload - the API response, or the committed fixture of one
|
|
167
|
+
* @returns the seat total, the party count, the majority line, and the ranking
|
|
168
|
+
*/
|
|
169
|
+
export function parseParliamentSeats(payload) {
|
|
170
|
+
return summarizeParliamentPartySeats(parseParliamentPartySeats(payload));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Reads the state of the parties in one house, the Commons by default.
|
|
174
|
+
*
|
|
175
|
+
* The API needs a date as well as a house and answers with the composition on
|
|
176
|
+
* that date, so the caller's own date is the one asked for.
|
|
177
|
+
*
|
|
178
|
+
* @param options - the house, the date, and an optional fetch implementation
|
|
179
|
+
* @returns the seat total, the party count, the majority line, and the ranking
|
|
180
|
+
*/
|
|
181
|
+
export async function fetchParliamentSeats(options = {}) {
|
|
182
|
+
const house = options.house ?? PARLIAMENT_COMMONS_HOUSE;
|
|
183
|
+
const forDate = options.forDate ?? formatParliamentQueryDate(options.now ?? new Date());
|
|
184
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
185
|
+
const url = buildParliamentSeatsUrl(house, forDate);
|
|
186
|
+
const response = await fetchImpl(url, { headers: { accept: 'application/json' } });
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new UkSourceApiError(PARLIAMENT_SEATS_SOURCE_ID, `HTTP ${response.status} from ${url}`);
|
|
189
|
+
}
|
|
190
|
+
return parseParliamentSeats(await response.json());
|
|
191
|
+
}
|
|
192
|
+
/** UK Parliament state of the parties, keyless. */
|
|
193
|
+
export const parliamentSeatsAdapter = {
|
|
194
|
+
id: PARLIAMENT_SEATS_SOURCE_ID,
|
|
195
|
+
name: 'UK Parliament state of the parties',
|
|
196
|
+
auth: 'none',
|
|
197
|
+
description: 'Seats each party holds in the House of Commons, with the members counted behind them.',
|
|
198
|
+
fetchLive: (options) => fetchParliamentSeats(options?.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
199
|
+
parse: parseParliamentSeats,
|
|
200
|
+
loadFixture: () => parseParliamentSeats(readFixtureJson('parliament-seats.json')),
|
|
201
|
+
};
|
|
202
|
+
//# sourceMappingURL=parliamentSeats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parliamentSeats.js","sourceRoot":"","sources":["../src/parliamentSeats.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { UkDataAdapter } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The Planning Data platform's dataset catalogue, published by the Ministry of
|
|
4
|
+
* Housing, Communities and Local Government. Keyless, and the same feed the
|
|
5
|
+
* platform's own datasets page reads:
|
|
6
|
+
* <https://www.planning.data.gov.uk/dataset>
|
|
7
|
+
*/
|
|
8
|
+
export declare const PLANNING_DATASETS_URL = "https://www.planning.data.gov.uk/dataset.json";
|
|
9
|
+
/** How many of the largest datasets the summary keeps by default. */
|
|
10
|
+
export declare const DEFAULT_LARGEST_DATASET_LIMIT = 10;
|
|
11
|
+
/**
|
|
12
|
+
* The realm value that marks a dataset entry. The same file also carries the
|
|
13
|
+
* platform's pipeline configuration, data specifications, and provenance
|
|
14
|
+
* tables, which are not datasets and hold no records.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PLANNING_DATASET_REALM = "dataset";
|
|
17
|
+
/** One dataset the platform lists, with the records published behind it. */
|
|
18
|
+
export interface PlanningDataset {
|
|
19
|
+
/** Dataset identifier, the value the API takes in its `dataset=` parameter. */
|
|
20
|
+
dataset: string;
|
|
21
|
+
name: string;
|
|
22
|
+
/** Records published for this dataset; 0 while it is still being built. */
|
|
23
|
+
entityCount: number;
|
|
24
|
+
themes: string[];
|
|
25
|
+
typology: string;
|
|
26
|
+
phase: string;
|
|
27
|
+
licence: string;
|
|
28
|
+
}
|
|
29
|
+
/** Rolled-up shape of the platform's dataset list. */
|
|
30
|
+
export interface PlanningDatasetSummary {
|
|
31
|
+
datasetCount: number;
|
|
32
|
+
/** Records summed across every dataset. */
|
|
33
|
+
entityCount: number;
|
|
34
|
+
/** Datasets listed with no records published yet. */
|
|
35
|
+
emptyDatasetCount: number;
|
|
36
|
+
/** The datasets holding the most records, largest first. */
|
|
37
|
+
largestDatasets: PlanningDataset[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parses a Planning Data /dataset.json payload into datasets.
|
|
41
|
+
*
|
|
42
|
+
* @param payload - the raw JSON body from the dataset catalogue
|
|
43
|
+
* @returns one entry per dataset, in the order the platform lists them
|
|
44
|
+
*/
|
|
45
|
+
export declare function parsePlanningDatasets(payload: unknown): PlanningDataset[];
|
|
46
|
+
/**
|
|
47
|
+
* Totals the catalogue and keeps the datasets holding the most records.
|
|
48
|
+
*
|
|
49
|
+
* @param datasets - datasets from {@link parsePlanningDatasets}
|
|
50
|
+
* @param largestDatasetLimit - how many to keep in `largestDatasets`
|
|
51
|
+
* @returns dataset, record, and empty-dataset counts plus the largest datasets
|
|
52
|
+
*/
|
|
53
|
+
export declare function summarizePlanningDatasets(datasets: PlanningDataset[], largestDatasetLimit?: number): PlanningDatasetSummary;
|
|
54
|
+
/**
|
|
55
|
+
* Lists every dataset on the Planning Data platform.
|
|
56
|
+
*
|
|
57
|
+
* @param options - an optional fetch implementation
|
|
58
|
+
* @returns the datasets the catalogue returns
|
|
59
|
+
*/
|
|
60
|
+
export declare function fetchPlanningDatasets(options?: {
|
|
61
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
62
|
+
}): Promise<PlanningDataset[]>;
|
|
63
|
+
/** Planning Data platform datasets, published by MHCLG and keyless. */
|
|
64
|
+
export declare const planningDatasetsAdapter: UkDataAdapter<PlanningDataset[]>;
|
|
65
|
+
//# sourceMappingURL=planningDatasets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planningDatasets.d.ts","sourceRoot":"","sources":["../src/planningDatasets.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,kDAAkD,CAAC;AAErF,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,YAAY,CAAC;AAEhD,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,sDAAsD;AACtD,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4DAA4D;IAC5D,eAAe,EAAE,eAAe,EAAE,CAAC;CACpC;AAoBD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,eAAe,EAAE,CAgBzE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,eAAe,EAAE,EAC3B,mBAAmB,GAAE,MAAsC,GAC1D,sBAAsB,CAaxB;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAAO,GACpD,OAAO,CAAC,eAAe,EAAE,CAAC,CAO5B;AAED,uEAAuE;AACvE,eAAO,MAAM,uBAAuB,EAAE,aAAa,CAAC,eAAe,EAAE,CASpE,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { UkSourceApiError, UkSourceParseError } from './errors.js';
|
|
3
|
+
import { readFixtureJson } from './fixtures.js';
|
|
4
|
+
/**
|
|
5
|
+
* The Planning Data platform's dataset catalogue, published by the Ministry of
|
|
6
|
+
* Housing, Communities and Local Government. Keyless, and the same feed the
|
|
7
|
+
* platform's own datasets page reads:
|
|
8
|
+
* <https://www.planning.data.gov.uk/dataset>
|
|
9
|
+
*/
|
|
10
|
+
export const PLANNING_DATASETS_URL = 'https://www.planning.data.gov.uk/dataset.json';
|
|
11
|
+
/** How many of the largest datasets the summary keeps by default. */
|
|
12
|
+
export const DEFAULT_LARGEST_DATASET_LIMIT = 10;
|
|
13
|
+
/**
|
|
14
|
+
* The realm value that marks a dataset entry. The same file also carries the
|
|
15
|
+
* platform's pipeline configuration, data specifications, and provenance
|
|
16
|
+
* tables, which are not datasets and hold no records.
|
|
17
|
+
*/
|
|
18
|
+
export const PLANNING_DATASET_REALM = 'dataset';
|
|
19
|
+
// Only the fields the summary reads are required. The catalogue grows fields
|
|
20
|
+
// as the platform does, and a new or missing optional field on one entry
|
|
21
|
+
// should not take the whole list down with it.
|
|
22
|
+
const PLANNING_DATASET_SCHEMA = z.object({
|
|
23
|
+
dataset: z.string(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
'entity-count': z.number(),
|
|
26
|
+
realm: z.string(),
|
|
27
|
+
themes: z.array(z.string()).optional(),
|
|
28
|
+
typology: z.string().optional(),
|
|
29
|
+
phase: z.string().optional(),
|
|
30
|
+
licence: z.string().optional(),
|
|
31
|
+
});
|
|
32
|
+
const PLANNING_DATASETS_RESPONSE_SCHEMA = z.object({
|
|
33
|
+
datasets: z.array(PLANNING_DATASET_SCHEMA),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Parses a Planning Data /dataset.json payload into datasets.
|
|
37
|
+
*
|
|
38
|
+
* @param payload - the raw JSON body from the dataset catalogue
|
|
39
|
+
* @returns one entry per dataset, in the order the platform lists them
|
|
40
|
+
*/
|
|
41
|
+
export function parsePlanningDatasets(payload) {
|
|
42
|
+
const parsed = PLANNING_DATASETS_RESPONSE_SCHEMA.safeParse(payload);
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
throw new UkSourceParseError('planning-datasets', parsed.error.message);
|
|
45
|
+
}
|
|
46
|
+
return parsed.data.datasets
|
|
47
|
+
.filter((entry) => entry.realm === PLANNING_DATASET_REALM)
|
|
48
|
+
.map((entry) => ({
|
|
49
|
+
dataset: entry.dataset,
|
|
50
|
+
name: entry.name,
|
|
51
|
+
entityCount: entry['entity-count'],
|
|
52
|
+
themes: entry.themes ?? [],
|
|
53
|
+
typology: entry.typology ?? '',
|
|
54
|
+
phase: entry.phase ?? '',
|
|
55
|
+
licence: entry.licence ?? '',
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Totals the catalogue and keeps the datasets holding the most records.
|
|
60
|
+
*
|
|
61
|
+
* @param datasets - datasets from {@link parsePlanningDatasets}
|
|
62
|
+
* @param largestDatasetLimit - how many to keep in `largestDatasets`
|
|
63
|
+
* @returns dataset, record, and empty-dataset counts plus the largest datasets
|
|
64
|
+
*/
|
|
65
|
+
export function summarizePlanningDatasets(datasets, largestDatasetLimit = DEFAULT_LARGEST_DATASET_LIMIT) {
|
|
66
|
+
const largestDatasets = [...datasets]
|
|
67
|
+
.sort((left, right) => right.entityCount - left.entityCount || left.name.localeCompare(right.name))
|
|
68
|
+
.slice(0, largestDatasetLimit);
|
|
69
|
+
return {
|
|
70
|
+
datasetCount: datasets.length,
|
|
71
|
+
entityCount: datasets.reduce((total, dataset) => total + dataset.entityCount, 0),
|
|
72
|
+
emptyDatasetCount: datasets.filter((dataset) => dataset.entityCount === 0).length,
|
|
73
|
+
largestDatasets,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Lists every dataset on the Planning Data platform.
|
|
78
|
+
*
|
|
79
|
+
* @param options - an optional fetch implementation
|
|
80
|
+
* @returns the datasets the catalogue returns
|
|
81
|
+
*/
|
|
82
|
+
export async function fetchPlanningDatasets(options = {}) {
|
|
83
|
+
const { fetchImpl = globalThis.fetch } = options;
|
|
84
|
+
const response = await fetchImpl(PLANNING_DATASETS_URL);
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new UkSourceApiError('planning-datasets', `HTTP ${response.status} listing datasets`);
|
|
87
|
+
}
|
|
88
|
+
return parsePlanningDatasets(await response.json());
|
|
89
|
+
}
|
|
90
|
+
/** Planning Data platform datasets, published by MHCLG and keyless. */
|
|
91
|
+
export const planningDatasetsAdapter = {
|
|
92
|
+
id: 'planning-datasets',
|
|
93
|
+
name: 'Ministry of Housing, Communities and Local Government planning datasets',
|
|
94
|
+
auth: 'none',
|
|
95
|
+
description: 'Every dataset the Planning Data platform lists, with the records behind it.',
|
|
96
|
+
fetchLive: (options) => fetchPlanningDatasets(options?.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
|
|
97
|
+
parse: parsePlanningDatasets,
|
|
98
|
+
loadFixture: () => parsePlanningDatasets(readFixtureJson('planning-datasets.json')),
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=planningDatasets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planningDatasets.js","sourceRoot":"","sources":["../src/planningDatasets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { UkDataAdapter } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Street-level recorded crime from the Home Office's police.uk open data API.
|
|
4
|
+
* Every police force in England, Wales and Northern Ireland publishes to it,
|
|
5
|
+
* keyless and monthly, under the Open Government Licence v3:
|
|
6
|
+
* <https://data.police.uk/docs/>
|
|
7
|
+
*/
|
|
8
|
+
export declare const POLICE_CRIMES_SOURCE_ID = "police-crimes";
|
|
9
|
+
/** The API root every call here is built from. */
|
|
10
|
+
export declare const POLICE_API_BASE_URL = "https://data.police.uk/api";
|
|
11
|
+
/** Street-level crime within a mile of a point, for one published month. */
|
|
12
|
+
export declare const POLICE_STREET_CRIMES_URL = "https://data.police.uk/api/crimes-street/all-crime";
|
|
13
|
+
/** The months the API holds, newest first, with the forces behind each. */
|
|
14
|
+
export declare const POLICE_STREET_DATES_URL = "https://data.police.uk/api/crimes-street-dates";
|
|
15
|
+
/** The crime types the API uses, with the label it gives each one. */
|
|
16
|
+
export declare const POLICE_CRIME_CATEGORIES_URL = "https://data.police.uk/api/crime-categories";
|
|
17
|
+
/**
|
|
18
|
+
* The radius the API answers for. Its street-level endpoints take a point and
|
|
19
|
+
* return everything within one mile of it; there is no way to ask for less.
|
|
20
|
+
*/
|
|
21
|
+
export declare const POLICE_STREET_CRIMES_RADIUS_MILES = 1;
|
|
22
|
+
/** How many published months the summary covers by default. */
|
|
23
|
+
export declare const DEFAULT_POLICE_CRIME_MONTH_COUNT = 12;
|
|
24
|
+
/** The point the story counts around, and the name the page gives it. */
|
|
25
|
+
export declare const DEFAULT_POLICE_CRIME_LOCATION: {
|
|
26
|
+
readonly label: "Leeds city centre";
|
|
27
|
+
readonly latitude: 53.7997;
|
|
28
|
+
readonly longitude: -1.5492;
|
|
29
|
+
};
|
|
30
|
+
/** A point the API answers for, with the name the page shows. */
|
|
31
|
+
export interface PoliceCrimeLocation {
|
|
32
|
+
label: string;
|
|
33
|
+
latitude: number;
|
|
34
|
+
longitude: number;
|
|
35
|
+
}
|
|
36
|
+
/** One crime type the API uses. */
|
|
37
|
+
export interface PoliceCrimeCategory {
|
|
38
|
+
/** The value records carry, e.g. "violent-crime". */
|
|
39
|
+
slug: string;
|
|
40
|
+
/** The label the API gives it, e.g. "Violence and sexual offences". */
|
|
41
|
+
name: string;
|
|
42
|
+
}
|
|
43
|
+
/** One crime type with the records counted against it. */
|
|
44
|
+
export interface PoliceCategoryCount {
|
|
45
|
+
slug: string;
|
|
46
|
+
name: string;
|
|
47
|
+
recordCount: number;
|
|
48
|
+
}
|
|
49
|
+
/** One outcome with the records that ended in it. */
|
|
50
|
+
export interface PoliceOutcomeCount {
|
|
51
|
+
/** The API's outcome label, or null when the record carries no outcome yet. */
|
|
52
|
+
outcome: string | null;
|
|
53
|
+
recordCount: number;
|
|
54
|
+
}
|
|
55
|
+
/** One published month with its records counted by type. */
|
|
56
|
+
export interface PoliceMonthCount {
|
|
57
|
+
/** The month as YYYY-MM. */
|
|
58
|
+
month: string;
|
|
59
|
+
recordCount: number;
|
|
60
|
+
/** Record counts for this month, biggest first. */
|
|
61
|
+
categoryCounts: PoliceCategoryCount[];
|
|
62
|
+
}
|
|
63
|
+
/** Everything the recorded crime story draws. */
|
|
64
|
+
export interface PoliceCrimeSummary {
|
|
65
|
+
/** How many published months the window covers. */
|
|
66
|
+
monthCount: number;
|
|
67
|
+
recordCount: number;
|
|
68
|
+
/** Oldest month in the window. */
|
|
69
|
+
firstMonth: string;
|
|
70
|
+
/** Newest month in the window. */
|
|
71
|
+
latestMonth: string;
|
|
72
|
+
/** Record counts across the whole window, biggest first. */
|
|
73
|
+
categoryCounts: PoliceCategoryCount[];
|
|
74
|
+
/** Outcome counts across the whole window, biggest first. */
|
|
75
|
+
outcomeCounts: PoliceOutcomeCount[];
|
|
76
|
+
/** One entry per month, oldest first. */
|
|
77
|
+
months: PoliceMonthCount[];
|
|
78
|
+
busiestMonth: PoliceMonthCount;
|
|
79
|
+
quietestMonth: PoliceMonthCount;
|
|
80
|
+
topCategory: PoliceCategoryCount;
|
|
81
|
+
topOutcome: PoliceOutcomeCount;
|
|
82
|
+
}
|
|
83
|
+
/** The raw API responses one summary is parsed from. */
|
|
84
|
+
export interface PoliceCrimeResponses {
|
|
85
|
+
/** The `/crime-categories` response. */
|
|
86
|
+
categories: unknown;
|
|
87
|
+
/** One `/crimes-street/all-crime` response per month, newest first. */
|
|
88
|
+
months: {
|
|
89
|
+
month: string;
|
|
90
|
+
records: unknown;
|
|
91
|
+
}[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parses the API responses behind one window into the counts the chart draws.
|
|
95
|
+
*
|
|
96
|
+
* The API writes one record per offence, so the counts come from the same read
|
|
97
|
+
* the page renders. Every check here is one the page would otherwise show as a
|
|
98
|
+
* wrong number: a month that came back empty, a record filed under a month
|
|
99
|
+
* other than the one it was asked for, or a crime type the category list does
|
|
100
|
+
* not carry all throw rather than quietly changing the totals.
|
|
101
|
+
*
|
|
102
|
+
* @param payload - the raw responses, or the committed fixture of them
|
|
103
|
+
* @returns the counts, with months oldest first
|
|
104
|
+
*/
|
|
105
|
+
export declare function parsePoliceCrimeSummary(payload: unknown): PoliceCrimeSummary;
|
|
106
|
+
/**
|
|
107
|
+
* Builds the street-level crime URL for one point and one month.
|
|
108
|
+
*
|
|
109
|
+
* @param options - where to count, and which month to ask for
|
|
110
|
+
* @returns the full query URL
|
|
111
|
+
*/
|
|
112
|
+
export declare function buildPoliceStreetCrimesUrl(options: {
|
|
113
|
+
latitude: number;
|
|
114
|
+
longitude: number;
|
|
115
|
+
month: string;
|
|
116
|
+
}): string;
|
|
117
|
+
/**
|
|
118
|
+
* Lists the months the API holds street-level crime for, newest first.
|
|
119
|
+
*
|
|
120
|
+
* @param options - an optional fetch implementation
|
|
121
|
+
* @returns the published months as YYYY-MM
|
|
122
|
+
*/
|
|
123
|
+
export declare function fetchPoliceCrimeMonths(options?: {
|
|
124
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
125
|
+
}): Promise<string[]>;
|
|
126
|
+
/**
|
|
127
|
+
* Reads the crime types the API uses.
|
|
128
|
+
*
|
|
129
|
+
* @param options - an optional fetch implementation
|
|
130
|
+
* @returns one entry per crime type
|
|
131
|
+
*/
|
|
132
|
+
export declare function fetchPoliceCrimeCategories(options?: {
|
|
133
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
134
|
+
}): Promise<unknown>;
|
|
135
|
+
/**
|
|
136
|
+
* Counts recorded crime within a mile of a point over the latest months.
|
|
137
|
+
*
|
|
138
|
+
* The months are read one at a time rather than in one burst, because the API
|
|
139
|
+
* is free to use and rate-limited per caller. A window of a year is 13 calls.
|
|
140
|
+
*
|
|
141
|
+
* @param options - where to count, how many months, and an optional fetch implementation
|
|
142
|
+
* @returns the counts for the window
|
|
143
|
+
*/
|
|
144
|
+
export declare function fetchPoliceCrimeSummary(options?: {
|
|
145
|
+
latitude?: number;
|
|
146
|
+
longitude?: number;
|
|
147
|
+
monthCount?: number;
|
|
148
|
+
fetchImpl?: typeof globalThis.fetch;
|
|
149
|
+
}): Promise<PoliceCrimeSummary>;
|
|
150
|
+
/** Home Office street-level recorded crime, keyless. */
|
|
151
|
+
export declare const policeCrimesAdapter: UkDataAdapter<PoliceCrimeSummary>;
|
|
152
|
+
//# sourceMappingURL=policeCrimes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policeCrimes.d.ts","sourceRoot":"","sources":["../src/policeCrimes.ts"],"names":[],"mappings":"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"}
|