swiss-netex 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +77 -0
- data/LICENSE +21 -0
- data/README.md +237 -0
- data/exe/swiss-netex +6 -0
- data/lib/swiss_netex/catalog.rb +41 -0
- data/lib/swiss_netex/cli.rb +546 -0
- data/lib/swiss_netex/dataset_page.rb +90 -0
- data/lib/swiss_netex/download.rb +239 -0
- data/lib/swiss_netex/error.rb +5 -0
- data/lib/swiss_netex/extract.rb +56 -0
- data/lib/swiss_netex/filter/common_frame.rb +194 -0
- data/lib/swiss_netex/filter/frame_worker.rb +188 -0
- data/lib/swiss_netex/filter/frames.rb +126 -0
- data/lib/swiss_netex/filter/refs.rb +106 -0
- data/lib/swiss_netex/filter/service_lines.rb +139 -0
- data/lib/swiss_netex/filter/timetable.rb +387 -0
- data/lib/swiss_netex/filter/timetable_batch.rb +330 -0
- data/lib/swiss_netex/filter/xml_sieve.rb +189 -0
- data/lib/swiss_netex/filter.rb +346 -0
- data/lib/swiss_netex/http.rb +130 -0
- data/lib/swiss_netex/lines.rb +161 -0
- data/lib/swiss_netex/operators.rb +254 -0
- data/lib/swiss_netex/package.rb +145 -0
- data/lib/swiss_netex/package_source.rb +30 -0
- data/lib/swiss_netex/resource_frame.rb +73 -0
- data/lib/swiss_netex/tsv.rb +53 -0
- data/lib/swiss_netex/version.rb +5 -0
- data/lib/swiss_netex.rb +20 -0
- data/swiss-netex.gemspec +40 -0
- metadata +104 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 47374355daf2b7133d4cdd64c44bc6a2574b3a1f77fc7ede6ed3dec582c85e67
|
|
4
|
+
data.tar.gz: 6e8dc725d90ae6c5e19a8e1cb7dad5113c20603994f648712d9d169b73a0760e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4a5c0a1aca546bac12a3196cc0b52d4822caeb5665bde3586b65678eb5a635cdbb2896cb6c4c7eead48e082ca8ec14a37b5f0762e43f47f039b05be171cf3f87
|
|
7
|
+
data.tar.gz: 6c51f8a53e717ef9f0be266b7859d9665afae4d1c099bf79af9a17015bb1b4370d78bb59cb046bb6e1b3ca5e34c6b6117695d3425898691103bfae645bee1f3b
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] — unreleased
|
|
11
|
+
|
|
12
|
+
First public release. Download, inspect, and filter Swiss national NeTEx
|
|
13
|
+
timetable packages from [opentransportdata.swiss](https://opentransportdata.swiss).
|
|
14
|
+
|
|
15
|
+
Marked **1.0.0** (not 0.x): the CLI surface is complete for the intended
|
|
16
|
+
scope and treated as a stable contract. Tag/publish waits on team review.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- CLI binary `swiss-netex` with `help`, `version`, `download`, `operators`,
|
|
21
|
+
`lines`, `filter`, and `extract`
|
|
22
|
+
- `download` — resolve the latest (or year-pinned) national NeTEx zip via the
|
|
23
|
+
public catalog + dataset page HTML, stream to disk, cache under
|
|
24
|
+
`~/.cache/swiss-netex` (override with `--cache-dir` / `--force`)
|
|
25
|
+
- `operators` / `lines` — discovery tables from RESOURCE / SERVICE only
|
|
26
|
+
(TSV when piped; aligned columns on a TTY). Optional `--operator` /
|
|
27
|
+
`--line` filters on `lines`. Omit `--from` to download/cache first
|
|
28
|
+
(`-y` / `-d` / `-c` / `-f`, same rules as `extract`)
|
|
29
|
+
- `filter` — operator/line filter for a local zip or directory package:
|
|
30
|
+
streaming SERVICE + TIMETABLE passes, transitive RESOURCE / SERVICE /
|
|
31
|
+
SITE / SERVICECALENDAR / COMMON rewrite, drop empty timetable shards,
|
|
32
|
+
`README.txt` in the output zip
|
|
33
|
+
- `extract` — download (when `--from` omitted) + filter in one command
|
|
34
|
+
- Operator aliases: GO number, `ch:1:Operator:*`, SBOID, ShortName
|
|
35
|
+
(repeatable `--operator` = union). Line aliases: PublicCode, id, SLNID,
|
|
36
|
+
ShortName, Name. With explicit `--line`, journeys must carry a matching
|
|
37
|
+
`LineRef` (operator-only fallback only when no line filter is set)
|
|
38
|
+
- COMMON connection entities (`JourneyMeeting`, `InterchangeRule`) kept only
|
|
39
|
+
when every tracked journey/stop/line ref still resolves in the package
|
|
40
|
+
- Bare `-o` stem without extension becomes `stem.zip` for filter/extract;
|
|
41
|
+
existing directories / trailing `/` still get the default `*_op-….zip`
|
|
42
|
+
name inside
|
|
43
|
+
- National-scale streaming: short-lived fork workers for TIMETABLE shards
|
|
44
|
+
and support frames, parent byte pre-scan to skip empty shards, streamed
|
|
45
|
+
kept-journey bodies, non-DOM COMMON scanner
|
|
46
|
+
- Env knobs: `SWISS_NETEX_TT_WORKERS` (default CPU count; `1` = serial),
|
|
47
|
+
`SWISS_NETEX_TT_BATCH` (default 1 shard/child), `SWISS_NETEX_NO_FORK=1`
|
|
48
|
+
- `SwissNetex::Package`, `ResourceFrame`, `Operators`, `Lines`, `Filter`,
|
|
49
|
+
`Download`, `Extract` library entry points
|
|
50
|
+
- Minitest suite (fixture packages only; no national zip in CI), GitHub
|
|
51
|
+
Actions CI + tag publish gated on test/lint, minimal RuboCop, RubyGems
|
|
52
|
+
trusted-publish on version tags
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- Filtered XML no longer keeps blank “holes” where dropped entities used to sit
|
|
57
|
+
(streaming whitespace collapse in SERVICE/SITE/RESOURCE/SERVICECALENDAR,
|
|
58
|
+
TIMETABLE, and COMMON writers — no full DOM pretty-print)
|
|
59
|
+
|
|
60
|
+
### Notes
|
|
61
|
+
|
|
62
|
+
- Tool is MIT; timetable data remains under OpenTransportData terms
|
|
63
|
+
- Cache key is the remote filename (export timestamp embedded); a positive-size
|
|
64
|
+
file must also open as a zip before it counts as a hit. Use `--force` to
|
|
65
|
+
re-download
|
|
66
|
+
- Downloads request `Accept-Encoding: identity` so size checks compare
|
|
67
|
+
wire `Content-Length` to the bytes written on disk
|
|
68
|
+
- Filter/extract write the output zip via a same-directory temp file + rename
|
|
69
|
+
- TIMETABLE `TrainNumber` defs are kept whether they appear before or after
|
|
70
|
+
`vehicleJourneys` (defs-first inputs may emit `trainNumbers` after journeys)
|
|
71
|
+
- Filtered-package `README.txt` and public README document Swiss-profile
|
|
72
|
+
expectations: `ValidDayBits` calendars, closed COMMON connections, sparse
|
|
73
|
+
TIMETABLE shard indices
|
|
74
|
+
- Peak system RSS scales roughly with worker count × largest kept shard;
|
|
75
|
+
prefer a machine with several GB free RAM for full national filters
|
|
76
|
+
- `extract` / `operators` / `lines`: `--from` cannot be combined with
|
|
77
|
+
`--dataset` / `--year` / `--force` / `--cache-dir` (download-only flags)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GateMedia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# swiss-netex
|
|
2
|
+
|
|
3
|
+
CLI for Swiss national NeTEx timetable packages
|
|
4
|
+
([opentransportdata.swiss](https://opentransportdata.swiss)): download, inspect,
|
|
5
|
+
and filter by operator/line.
|
|
6
|
+
|
|
7
|
+
This is **not** a full NeTEx model library. It prepares smaller operator-scoped
|
|
8
|
+
packages from the national SKI NeTEx feed.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Ruby ≥ 4.0
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
gem install swiss-netex
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or from source:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/gatemedia/swiss-netex.git
|
|
24
|
+
cd swiss-netex
|
|
25
|
+
mise run bootstrap # or: bundle install
|
|
26
|
+
bundle exec exe/swiss-netex version
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
Download (or reuse cache) and filter one operator in a single step:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
swiss-netex extract -y 2026 -O 151 -o tl.zip
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Stdout prints the output path. Progress goes to stderr.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
swiss-netex help
|
|
41
|
+
swiss-netex help extract # full flags for any command
|
|
42
|
+
swiss-netex version
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
### `download`
|
|
48
|
+
|
|
49
|
+
Resolve and stream the latest national NeTEx zip. Resource downloads are public
|
|
50
|
+
(no API token on the happy path). Prints the file path on stdout.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
swiss-netex download
|
|
54
|
+
swiss-netex download --year 2026
|
|
55
|
+
swiss-netex download -y 2026 -o ./cache
|
|
56
|
+
swiss-netex download --dataset timetablenetex_2026 --force
|
|
57
|
+
swiss-netex download --cache-dir ./cache
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Default dataset: highest published `timetablenetex_YYYY` on the public catalog
|
|
61
|
+
- Override with `--year YYYY` or `--dataset` (slug or full page URL)
|
|
62
|
+
- Default cache: `~/.cache/swiss-netex`
|
|
63
|
+
- Cache key is the remote filename (export timestamp embedded). A positive-size
|
|
64
|
+
local file with that name that also opens as a zip is a cache hit — OTD signed
|
|
65
|
+
URLs reject HEAD, so remote size is not re-checked. Use `--force` to refresh.
|
|
66
|
+
|
|
67
|
+
### `operators`
|
|
68
|
+
|
|
69
|
+
List operators from the RESOURCE frame. Scans RESOURCE only — not TIMETABLE
|
|
70
|
+
shards — so listing stays cheap even on a full national package. Omit `--from`
|
|
71
|
+
to download (or reuse cache) first — same flags as `download` / `extract`.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
swiss-netex operators -y 2026
|
|
75
|
+
swiss-netex operators --from ./cache/PROD_NETEX_….zip
|
|
76
|
+
swiss-netex operators -y 2026 | cut -f1,4
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Columns: `go`, `operator_id`, `sboid`, `short_name`, `name`.
|
|
80
|
+
|
|
81
|
+
### `lines`
|
|
82
|
+
|
|
83
|
+
List lines from the SERVICE frame (SERVICE only — not TIMETABLE). Optional
|
|
84
|
+
`--operator` / `--line` narrow the list (same alias rules as `filter`). Omit
|
|
85
|
+
`--from` to download (or reuse cache) first.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
swiss-netex lines -y 2026
|
|
89
|
+
swiss-netex lines -y 2026 -O 151
|
|
90
|
+
swiss-netex lines --from ./cache/PROD_NETEX_….zip -O 151 -l m1
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Columns: `line_id`, `public_code`, `slnid`, `operator_ref`, `name`.
|
|
94
|
+
|
|
95
|
+
### `filter`
|
|
96
|
+
|
|
97
|
+
Filter a local package (zip or extracted directory) by operator and optional
|
|
98
|
+
line. Writes a smaller zip with transitive frame contents and a `README.txt`.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
swiss-netex filter --from ./cache/PROD_NETEX_….zip -O 151 -o tl.zip
|
|
102
|
+
swiss-netex filter --from ./cache/PROD_NETEX_….zip \
|
|
103
|
+
-O 151 -O 55 -l 1 -o filtered.zip --verbose
|
|
104
|
+
swiss-netex filter --from ./package-dir/ -O 151 -o ./out/
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- `--operator` / `-O` is **repeatable** (union). Aliases: GO number,
|
|
108
|
+
`ch:1:Operator:*`, SBOID, ShortName
|
|
109
|
+
- Optional `--line` / `-l` (repeatable): PublicCode, id, SLNID, ShortName, Name
|
|
110
|
+
- Keeps matching SERVICE lines and TIMETABLE `ServiceJourney`s, then the
|
|
111
|
+
transitive closure in RESOURCE / SERVICE / SITE / SERVICECALENDAR / COMMON
|
|
112
|
+
- Empty timetable shards are dropped (original national shard indices kept)
|
|
113
|
+
- Without `--allow-empty`, exits non-zero when no lines or journeys match
|
|
114
|
+
- `--verbose` prints per-shard and per-frame keep/drop counters on stderr
|
|
115
|
+
- Output zip includes `README.txt` (filters, counts, Swiss-profile notes)
|
|
116
|
+
|
|
117
|
+
### `extract`
|
|
118
|
+
|
|
119
|
+
Download (when `--from` is omitted) then filter. The common one-shot workflow.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
swiss-netex extract -O 151 -o tl.zip
|
|
123
|
+
swiss-netex extract -y 2026 -O 151 -o tl.zip
|
|
124
|
+
swiss-netex extract --from ./cache/PROD_NETEX_….zip -O 151 -o tl.zip
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Same filter flags as `filter`, plus download flags when no `--from` is given.
|
|
128
|
+
`operators` and `lines` share the same package-source rules: omit `--from` to
|
|
129
|
+
download/cache; with `--from`, `--dataset` / `--year` / `--force` /
|
|
130
|
+
`--cache-dir` are rejected.
|
|
131
|
+
|
|
132
|
+
## Output and conventions
|
|
133
|
+
|
|
134
|
+
| Stream | Content |
|
|
135
|
+
|--------|---------|
|
|
136
|
+
| stdout | Machine-usable result (file path, or TSV/table for discovery) |
|
|
137
|
+
| stderr | Progress, status, verbose counters, errors |
|
|
138
|
+
|
|
139
|
+
Discovery commands (`operators`, `lines`): raw TSV when piped / non-TTY;
|
|
140
|
+
aligned columns on an interactive terminal (same cells, two-space padding).
|
|
141
|
+
|
|
142
|
+
**`-o` path rules** (`filter` / `extract`):
|
|
143
|
+
|
|
144
|
+
| `-o` value | Result |
|
|
145
|
+
|------------|--------|
|
|
146
|
+
| `tl.zip` | as-is |
|
|
147
|
+
| `tl` (no extension) | `tl.zip` |
|
|
148
|
+
| `tl.tar` | as-is (explicit non-zip extension left alone) |
|
|
149
|
+
| existing directory or trailing `/` | `dir/<source>_op-….zip` |
|
|
150
|
+
|
|
151
|
+
Exit codes: `0` ok, `1` runtime/domain error, `64` usage/parse error.
|
|
152
|
+
|
|
153
|
+
## Memory and speed
|
|
154
|
+
|
|
155
|
+
National packages are large (~hundreds of MB compressed, tens of GB
|
|
156
|
+
uncompressed). `filter` / `extract` stream frames and never load the full
|
|
157
|
+
national set into a DOM:
|
|
158
|
+
|
|
159
|
+
- short-lived worker processes on platforms with `fork`
|
|
160
|
+
- parent byte pre-scan skips empty TIMETABLE shards
|
|
161
|
+
- process pool for matching shards and support frames
|
|
162
|
+
- non-DOM COMMON scanner
|
|
163
|
+
|
|
164
|
+
| Env | Effect |
|
|
165
|
+
|-----|--------|
|
|
166
|
+
| `SWISS_NETEX_TT_WORKERS=N` | Pool size (default: CPU count; `1` = serial) |
|
|
167
|
+
| `SWISS_NETEX_TT_BATCH` | Shards per child (default: `1`) |
|
|
168
|
+
| `SWISS_NETEX_NO_FORK=1` | Force in-process mode |
|
|
169
|
+
|
|
170
|
+
Prefer a machine with **≥4 GB free RAM**. Peak system RSS scales roughly with
|
|
171
|
+
worker count × largest kept shard.
|
|
172
|
+
|
|
173
|
+
## Swiss profile notes (filtered packages)
|
|
174
|
+
|
|
175
|
+
These are normal for SKI national exports and operator-scoped subsets — not
|
|
176
|
+
filter bugs. The same points are written into each output `README.txt`.
|
|
177
|
+
|
|
178
|
+
**Calendar / `DayType`**
|
|
179
|
+
|
|
180
|
+
Journeys typically reference `AvailabilityCondition`s that carry
|
|
181
|
+
`FromDate` / `ToDate` / `ValidDayBits`. Many selections collect **zero**
|
|
182
|
+
`DayType` / `DayTypeRef` entities. Importers should read validity from the
|
|
183
|
+
kept ACs in SERVICECALENDAR, not assume a DayType model.
|
|
184
|
+
|
|
185
|
+
**COMMON connections**
|
|
186
|
+
|
|
187
|
+
`JourneyMeeting` and `InterchangeRule` are kept only when **every** tracked
|
|
188
|
+
journey, stop, and line ref still resolves inside the package (no dangling
|
|
189
|
+
partner `LineRef`). Cross-operator interchanges drop unless you include both
|
|
190
|
+
operators (e.g. `-O 151 -O 55`). An empty connection section is valid.
|
|
191
|
+
|
|
192
|
+
**TIMETABLE shard names**
|
|
193
|
+
|
|
194
|
+
Only shards with kept journeys are written. Filenames keep the national
|
|
195
|
+
`TIMETABLE_{i}_{n}_…` indices, so numbering can have gaps (e.g. 45–69 plus
|
|
196
|
+
113). Consumers should glob timetable members, not assume `1…N` contiguous.
|
|
197
|
+
|
|
198
|
+
## Data license
|
|
199
|
+
|
|
200
|
+
The **tool** is MIT. **Timetable data** remains under
|
|
201
|
+
[OpenTransportData terms](https://opentransportdata.swiss). Do not commit
|
|
202
|
+
national dumps into this repository.
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
This project uses [mise](https://mise.jdx.dev/) for the local toolchain.
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
mise trust # once, if prompted
|
|
210
|
+
mise install # Ruby from mise.toml / .ruby-version
|
|
211
|
+
mise run bootstrap # bundle install
|
|
212
|
+
mise run test # bundle exec rake test
|
|
213
|
+
mise run lint # bundle exec rake rubocop
|
|
214
|
+
mise run ci # tests + rubocop
|
|
215
|
+
mise run swiss-netex help
|
|
216
|
+
mise run swiss-netex version
|
|
217
|
+
mise run build # gem package into pkg/
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Without mise:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
bundle install
|
|
224
|
+
bundle exec rake # test + rubocop
|
|
225
|
+
bundle exec rake test
|
|
226
|
+
bundle exec rake rubocop
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Release
|
|
230
|
+
|
|
231
|
+
1. Bump `SwissNetex::VERSION` and update `CHANGELOG.md`
|
|
232
|
+
2. Tag `vX.Y.Z` and push the tag
|
|
233
|
+
3. GitHub Actions builds and publishes to [RubyGems](https://rubygems.org/gems/swiss-netex)
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT © GateMedia — see [LICENSE](LICENSE).
|
data/exe/swiss-netex
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwissNetex
|
|
4
|
+
# Parses an opentransportdata.swiss catalog search HTML page and picks the
|
|
5
|
+
# highest Fahrplanjahr among national NeTEx dataset slugs
|
|
6
|
+
# (`timetablenetex_YYYY`).
|
|
7
|
+
#
|
|
8
|
+
# CKAN JSON APIs require a token on this host; the public HTML catalog does not.
|
|
9
|
+
class Catalog
|
|
10
|
+
DATASET_SLUG_RE = %r{
|
|
11
|
+
/(?:[a-z]{2}/)?dataset/(timetablenetex_(\d{4}))
|
|
12
|
+
(?:["'\s?#]|$)
|
|
13
|
+
}ix
|
|
14
|
+
|
|
15
|
+
def self.latest_slug(html)
|
|
16
|
+
new(html).latest_slug
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(html)
|
|
20
|
+
@html = html.to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def slugs
|
|
24
|
+
seen = {}
|
|
25
|
+
@html.scan(DATASET_SLUG_RE).filter_map do |slug, year|
|
|
26
|
+
key = slug.downcase
|
|
27
|
+
next if seen[key]
|
|
28
|
+
|
|
29
|
+
seen[key] = true
|
|
30
|
+
{ slug: key, year: Integer(year) }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def latest_slug
|
|
35
|
+
list = slugs
|
|
36
|
+
raise Error, "No timetablenetex_YYYY dataset found on catalog search page" if list.empty?
|
|
37
|
+
|
|
38
|
+
list.max_by { |entry| entry[:year] }[:slug]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|