gtfs_df 0.11.1 → 0.13.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 +4 -4
- data/CHANGELOG.md +29 -0
- data/README.md +12 -0
- data/devenv.lock +43 -39
- data/devenv.nix +1 -0
- data/devenv.yaml +5 -0
- data/lib/gtfs_df/base_gtfs_table.rb +7 -1
- data/lib/gtfs_df/feed.rb +20 -11
- data/lib/gtfs_df/graph.rb +10 -2
- data/lib/gtfs_df/reader.rb +36 -15
- data/lib/gtfs_df/version.rb +1 -1
- data/lib/gtfs_df/writer.rb +15 -7
- metadata +1 -2
- data/.solargraph.yml +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 35a4116956b65a7dfdfd539af912bf67f590a2b99b35b3ea0bb62eafb2e60690
|
|
4
|
+
data.tar.gz: 14e43ea9a946392ac3044306a166544ae0183868b7305811be0c168fc6b9b153
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c26d8662f454706e3ab4d641cb2f7f4f076acbd80fa9ce3689c2746bbec30d5c4added43129a7f984f6137bc1eee3a3d4597c1ea0d68b08d35caf79e3abe2c9
|
|
7
|
+
data.tar.gz: 824f21c6e8cabec24b31222ce9cb45ae71e0455edf5346ca56294e9490b5223915a782f0501f5800fc7f3aacd13ae1f5c6120012ffa7a9828ed6e55b9de93485
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## [0.13.0] - 2026-10-07
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- Add custom file processing to reader
|
|
6
|
+
- Add custom file processing to feed and graph
|
|
7
|
+
- Add custom file processing to writer
|
|
8
|
+
|
|
9
|
+
### 💼 Other
|
|
10
|
+
|
|
11
|
+
- Devenv 2
|
|
12
|
+
## [0.12.0] - 2026-06-12
|
|
13
|
+
|
|
14
|
+
### 🚀 Features
|
|
15
|
+
|
|
16
|
+
- Allow loading a subset of tables
|
|
17
|
+
|
|
18
|
+
### 🐛 Bug Fixes
|
|
19
|
+
|
|
20
|
+
- Truncate ragged lines
|
|
21
|
+
- Consider BOM when looking for empty files
|
|
22
|
+
|
|
23
|
+
### ⚙️ Miscellaneous Tasks
|
|
24
|
+
|
|
25
|
+
- Bump version to 0.12.0
|
|
1
26
|
## [0.11.1] - 2026-04-28
|
|
2
27
|
|
|
3
28
|
### 🐛 Bug Fixes
|
|
@@ -8,6 +33,10 @@
|
|
|
8
33
|
|
|
9
34
|
- Validate filtering from feed with empty calendar
|
|
10
35
|
- Validate unreferenced calendar dates
|
|
36
|
+
|
|
37
|
+
### ⚙️ Miscellaneous Tasks
|
|
38
|
+
|
|
39
|
+
- Bump version to 0.11.1
|
|
11
40
|
## [0.11.0] - 2026-03-30
|
|
12
41
|
|
|
13
42
|
### ⚙️ Miscellaneous Tasks
|
data/README.md
CHANGED
|
@@ -35,6 +35,18 @@ feed = GtfsDf::Reader.load_from_zip('path/to/gtfs.zip')
|
|
|
35
35
|
# Or, load from a directory
|
|
36
36
|
feed = GtfsDf::Reader.load_from_dir('path/to/gtfs_dir')
|
|
37
37
|
|
|
38
|
+
# Load methods accept an 'extra' parameter that specifies custom files and how to build them
|
|
39
|
+
extra = {
|
|
40
|
+
files: %w[directions],
|
|
41
|
+
classes: {
|
|
42
|
+
directions: DirectionsClass,
|
|
43
|
+
},
|
|
44
|
+
graph_edges: [
|
|
45
|
+
["routes", "directions", {dependencies: [{"directions" => "route_id", "routes" => "route_id"}], optional: true}]
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
GtfsDf::Reader.load_from_zip('path/to/gtfs.zip', extra:)
|
|
49
|
+
|
|
38
50
|
# Parse times as seconds since midnight instead of string
|
|
39
51
|
feed = GtfsDf::Reader.load_from_dir('path/to/gtfs_dir', parse_times: true)
|
|
40
52
|
|
data/devenv.lock
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
"devenv": {
|
|
4
4
|
"locked": {
|
|
5
5
|
"dir": "src/modules",
|
|
6
|
-
"lastModified":
|
|
6
|
+
"lastModified": 1783109842,
|
|
7
|
+
"narHash": "sha256-Fi7lW+sg1ChduOOQRBCK5JeA1cw4x5wSjIJcppRGGqQ=",
|
|
7
8
|
"owner": "cachix",
|
|
8
9
|
"repo": "devenv",
|
|
9
|
-
"rev": "
|
|
10
|
+
"rev": "e30eb49258bee68353bd9c619823f635f4afa86c",
|
|
10
11
|
"type": "github"
|
|
11
12
|
},
|
|
12
13
|
"original": {
|
|
@@ -19,14 +20,15 @@
|
|
|
19
20
|
"flake-compat": {
|
|
20
21
|
"flake": false,
|
|
21
22
|
"locked": {
|
|
22
|
-
"lastModified":
|
|
23
|
-
"
|
|
23
|
+
"lastModified": 1767039857,
|
|
24
|
+
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
|
25
|
+
"owner": "NixOS",
|
|
24
26
|
"repo": "flake-compat",
|
|
25
|
-
"rev": "
|
|
27
|
+
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
26
28
|
"type": "github"
|
|
27
29
|
},
|
|
28
30
|
"original": {
|
|
29
|
-
"owner": "
|
|
31
|
+
"owner": "NixOS",
|
|
30
32
|
"repo": "flake-compat",
|
|
31
33
|
"type": "github"
|
|
32
34
|
}
|
|
@@ -34,10 +36,11 @@
|
|
|
34
36
|
"flake-compat_2": {
|
|
35
37
|
"flake": false,
|
|
36
38
|
"locked": {
|
|
37
|
-
"lastModified":
|
|
39
|
+
"lastModified": 1767039857,
|
|
40
|
+
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
|
38
41
|
"owner": "edolstra",
|
|
39
42
|
"repo": "flake-compat",
|
|
40
|
-
"rev": "
|
|
43
|
+
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
|
41
44
|
"type": "github"
|
|
42
45
|
},
|
|
43
46
|
"original": {
|
|
@@ -52,6 +55,7 @@
|
|
|
52
55
|
},
|
|
53
56
|
"locked": {
|
|
54
57
|
"lastModified": 1731533236,
|
|
58
|
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
55
59
|
"owner": "numtide",
|
|
56
60
|
"repo": "flake-utils",
|
|
57
61
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
@@ -66,16 +70,16 @@
|
|
|
66
70
|
"git-hooks": {
|
|
67
71
|
"inputs": {
|
|
68
72
|
"flake-compat": "flake-compat",
|
|
69
|
-
"gitignore": "gitignore",
|
|
70
73
|
"nixpkgs": [
|
|
71
74
|
"nixpkgs"
|
|
72
75
|
]
|
|
73
76
|
},
|
|
74
77
|
"locked": {
|
|
75
|
-
"lastModified":
|
|
78
|
+
"lastModified": 1783008725,
|
|
79
|
+
"narHash": "sha256-jGiy6+sxjNWXSjp25uoJuNfyH9zBK1PEDY0lVoL4ibQ=",
|
|
76
80
|
"owner": "cachix",
|
|
77
81
|
"repo": "git-hooks.nix",
|
|
78
|
-
"rev": "
|
|
82
|
+
"rev": "bca82caa46d5ec0f5d422c61fb1e30bc51313cbe",
|
|
79
83
|
"type": "github"
|
|
80
84
|
},
|
|
81
85
|
"original": {
|
|
@@ -84,32 +88,16 @@
|
|
|
84
88
|
"type": "github"
|
|
85
89
|
}
|
|
86
90
|
},
|
|
87
|
-
"
|
|
91
|
+
"nixpkgs": {
|
|
88
92
|
"inputs": {
|
|
89
|
-
"nixpkgs":
|
|
90
|
-
"git-hooks",
|
|
91
|
-
"nixpkgs"
|
|
92
|
-
]
|
|
93
|
+
"nixpkgs-src": "nixpkgs-src"
|
|
93
94
|
},
|
|
94
95
|
"locked": {
|
|
95
|
-
"lastModified":
|
|
96
|
-
"
|
|
97
|
-
"repo": "gitignore.nix",
|
|
98
|
-
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
|
|
99
|
-
"type": "github"
|
|
100
|
-
},
|
|
101
|
-
"original": {
|
|
102
|
-
"owner": "hercules-ci",
|
|
103
|
-
"repo": "gitignore.nix",
|
|
104
|
-
"type": "github"
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
"nixpkgs": {
|
|
108
|
-
"locked": {
|
|
109
|
-
"lastModified": 1764580874,
|
|
96
|
+
"lastModified": 1782924808,
|
|
97
|
+
"narHash": "sha256-tn2ahNv3ZNXyVHdPx/uw+N0xa7YSMtXUr6OEvu+s8ng=",
|
|
110
98
|
"owner": "cachix",
|
|
111
99
|
"repo": "devenv-nixpkgs",
|
|
112
|
-
"rev": "
|
|
100
|
+
"rev": "e2c881cb8d5f1cac6cef9d71f0d450a55691b999",
|
|
113
101
|
"type": "github"
|
|
114
102
|
},
|
|
115
103
|
"original": {
|
|
@@ -128,10 +116,11 @@
|
|
|
128
116
|
]
|
|
129
117
|
},
|
|
130
118
|
"locked": {
|
|
131
|
-
"lastModified":
|
|
119
|
+
"lastModified": 1782898183,
|
|
120
|
+
"narHash": "sha256-2wRgS4D9TnY68y14I69FfYVLzcWXMnystgfGLkBuAMI=",
|
|
132
121
|
"owner": "bobvanderlinden",
|
|
133
122
|
"repo": "nixpkgs-ruby",
|
|
134
|
-
"rev": "
|
|
123
|
+
"rev": "edd32327ce414b547db75ca35eece1bd5329073c",
|
|
135
124
|
"type": "github"
|
|
136
125
|
},
|
|
137
126
|
"original": {
|
|
@@ -140,20 +129,35 @@
|
|
|
140
129
|
"type": "github"
|
|
141
130
|
}
|
|
142
131
|
},
|
|
132
|
+
"nixpkgs-src": {
|
|
133
|
+
"flake": false,
|
|
134
|
+
"locked": {
|
|
135
|
+
"lastModified": 1782636521,
|
|
136
|
+
"narHash": "sha256-OG8laCOGtkxlB1JH3XqOnXxnkGJme4mQdXo5hkhCQIY=",
|
|
137
|
+
"owner": "NixOS",
|
|
138
|
+
"repo": "nixpkgs",
|
|
139
|
+
"rev": "e1c1b84752fb0897897380a3cae9dc7fcab91ca3",
|
|
140
|
+
"type": "github"
|
|
141
|
+
},
|
|
142
|
+
"original": {
|
|
143
|
+
"owner": "NixOS",
|
|
144
|
+
"ref": "nixpkgs-unstable",
|
|
145
|
+
"repo": "nixpkgs",
|
|
146
|
+
"type": "github"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
143
149
|
"root": {
|
|
144
150
|
"inputs": {
|
|
145
151
|
"devenv": "devenv",
|
|
146
152
|
"git-hooks": "git-hooks",
|
|
147
153
|
"nixpkgs": "nixpkgs",
|
|
148
|
-
"nixpkgs-ruby": "nixpkgs-ruby"
|
|
149
|
-
"pre-commit-hooks": [
|
|
150
|
-
"git-hooks"
|
|
151
|
-
]
|
|
154
|
+
"nixpkgs-ruby": "nixpkgs-ruby"
|
|
152
155
|
}
|
|
153
156
|
},
|
|
154
157
|
"systems": {
|
|
155
158
|
"locked": {
|
|
156
159
|
"lastModified": 1681028828,
|
|
160
|
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
157
161
|
"owner": "nix-systems",
|
|
158
162
|
"repo": "default",
|
|
159
163
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
@@ -168,4 +172,4 @@
|
|
|
168
172
|
},
|
|
169
173
|
"root": "root",
|
|
170
174
|
"version": 7
|
|
171
|
-
}
|
|
175
|
+
}
|
data/devenv.nix
CHANGED
data/devenv.yaml
CHANGED
|
@@ -12,7 +12,13 @@ module GtfsDf
|
|
|
12
12
|
elsif input.is_a?(String)
|
|
13
13
|
# TODO: use `infer_schema: false` instead of `infer_schema_length` after polars release:
|
|
14
14
|
# https://github.com/ankane/ruby-polars/blob/master/CHANGELOG.md#100-unreleased
|
|
15
|
-
df = Polars
|
|
15
|
+
df = Polars
|
|
16
|
+
.read_csv(
|
|
17
|
+
input,
|
|
18
|
+
infer_schema_length: 0,
|
|
19
|
+
encoding: "utf8-lossy",
|
|
20
|
+
truncate_ragged_lines: true
|
|
21
|
+
)
|
|
16
22
|
.rename(->(col) { col.strip })
|
|
17
23
|
|
|
18
24
|
# Strip out empty lines. Unfortunately read_csv does not support the drop_empty_rows
|
data/lib/gtfs_df/feed.rb
CHANGED
|
@@ -38,15 +38,17 @@ module GtfsDf
|
|
|
38
38
|
|
|
39
39
|
attr_accessor(*GTFS_FILES)
|
|
40
40
|
attr_accessor(:parse_times)
|
|
41
|
-
attr_reader(:graph)
|
|
41
|
+
attr_reader(:graph, :extra)
|
|
42
42
|
|
|
43
43
|
# Initialize with a hash of DataFrames
|
|
44
44
|
REQUIRED_GTFS_FILES = %w[agency stops routes trips stop_times].freeze
|
|
45
45
|
|
|
46
46
|
# @param data [Hash] Hash of DataFrames for each GTFS file
|
|
47
47
|
# @param parse_times [Boolean] Whether to parse time fields to seconds since midnight (default: false)
|
|
48
|
-
|
|
48
|
+
# @param extra [Hash] Extra file metadata to include in the feed; see README
|
|
49
|
+
def initialize(data = {}, parse_times: false, extra: {})
|
|
49
50
|
@parse_times = parse_times
|
|
51
|
+
@extra = extra
|
|
50
52
|
|
|
51
53
|
missing = REQUIRED_GTFS_FILES.reject { |file| data[file].is_a?(Polars::DataFrame) }
|
|
52
54
|
# At least one of calendar or calendar_dates must be present
|
|
@@ -59,12 +61,16 @@ module GtfsDf
|
|
|
59
61
|
end.join(", ")}"
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
extra_files = @extra[:files] || []
|
|
65
|
+
extra_edges = @extra[:graph_edges] || []
|
|
66
|
+
extra_classes = @extra[:classes] || {}
|
|
67
|
+
@graph = GtfsDf::Graph.build(extra_edges:, extra_files:)
|
|
68
|
+
singleton_class.attr_accessor(*extra_files)
|
|
63
69
|
|
|
64
|
-
GTFS_FILES.each do |file|
|
|
70
|
+
(GTFS_FILES + extra_files).each do |file|
|
|
65
71
|
df = data[file]
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
schema_class = extra_classes[file.to_sym] || begin
|
|
73
|
+
schema_class_name = file.split("_").map(&:capitalize).join
|
|
68
74
|
GtfsDf::Schema.const_get(schema_class_name)
|
|
69
75
|
rescue
|
|
70
76
|
nil
|
|
@@ -106,7 +112,7 @@ module GtfsDf
|
|
|
106
112
|
def filter(view, filter_only_children: false)
|
|
107
113
|
filtered = {}
|
|
108
114
|
|
|
109
|
-
GTFS_FILES.each do |file|
|
|
115
|
+
(GTFS_FILES + (@extra[:files] || [])).each do |file|
|
|
110
116
|
df = send(file)
|
|
111
117
|
next unless df
|
|
112
118
|
|
|
@@ -145,7 +151,7 @@ module GtfsDf
|
|
|
145
151
|
|
|
146
152
|
(!df || df.height == 0) && !is_required_file
|
|
147
153
|
end
|
|
148
|
-
self.class.new(filtered, parse_times: @parse_times)
|
|
154
|
+
self.class.new(filtered, parse_times: @parse_times, extra: @extra)
|
|
149
155
|
end
|
|
150
156
|
|
|
151
157
|
# Utility method that returns a hash of dataframes by file name
|
|
@@ -374,7 +380,10 @@ module GtfsDf
|
|
|
374
380
|
# not dropped when another edge is processed first.
|
|
375
381
|
def prune!(root, filtered, filter_only_children: false)
|
|
376
382
|
seen_edges = Set.new
|
|
377
|
-
|
|
383
|
+
extra_files = @extra[:files] || []
|
|
384
|
+
extra_edges = @extra[:graph_edges] || []
|
|
385
|
+
all_nodes = Graph.all_nodes(extra_files:)
|
|
386
|
+
rerooted_graph = Graph.build(bidirectional: !filter_only_children, extra_edges:, extra_files:)
|
|
378
387
|
accumulated_service_ids = Polars::Series.new("service_id", dtype: Polars::String)
|
|
379
388
|
trips_base_df = nil
|
|
380
389
|
|
|
@@ -388,8 +397,8 @@ module GtfsDf
|
|
|
388
397
|
next if seen_edges.include?(edge)
|
|
389
398
|
seen_edges.add(edge)
|
|
390
399
|
|
|
391
|
-
parent_node =
|
|
392
|
-
child_node =
|
|
400
|
+
parent_node = all_nodes[parent_node_id]
|
|
401
|
+
child_node = all_nodes[child_node_id]
|
|
393
402
|
parent_df = filtered[parent_node.fetch(:file)]
|
|
394
403
|
next unless parent_df
|
|
395
404
|
|
data/lib/gtfs_df/graph.rb
CHANGED
|
@@ -40,10 +40,17 @@ module GtfsDf
|
|
|
40
40
|
|
|
41
41
|
NODES = STANDARD_FILE_NODES.merge(STOP_NODES).freeze
|
|
42
42
|
|
|
43
|
+
def self.all_nodes(extra_files: [])
|
|
44
|
+
extra_nodes = extra_files.map do |file|
|
|
45
|
+
[file, {id: file, file: file, filter: nil}]
|
|
46
|
+
end.to_h
|
|
47
|
+
NODES.merge(extra_nodes)
|
|
48
|
+
end
|
|
49
|
+
|
|
43
50
|
# Returns a directed graph of GTFS file dependencies
|
|
44
|
-
def self.build(bidirectional: false)
|
|
51
|
+
def self.build(bidirectional: false, extra_edges: [], extra_files: [])
|
|
45
52
|
g = NetworkX::DiGraph.new
|
|
46
|
-
|
|
53
|
+
all_nodes(extra_files:).keys.each { |node| g.add_node(node) }
|
|
47
54
|
|
|
48
55
|
# Edges should be parent, child
|
|
49
56
|
# TODO: Add fare_rules -> stops + test
|
|
@@ -160,6 +167,7 @@ module GtfsDf
|
|
|
160
167
|
{"booking_rules" => "booking_rule_id", "stop_times" => "drop_off_booking_rule_id"}
|
|
161
168
|
]}]
|
|
162
169
|
]
|
|
170
|
+
edges += extra_edges
|
|
163
171
|
|
|
164
172
|
edges.each do |from, to, attrs|
|
|
165
173
|
g.add_edge(from, to, **attrs)
|
data/lib/gtfs_df/reader.rb
CHANGED
|
@@ -6,13 +6,16 @@ module GtfsDf
|
|
|
6
6
|
#
|
|
7
7
|
# @param zip_path [String] Path to the GTFS zip file
|
|
8
8
|
# @param parse_times [Boolean] Whether to parse time fields to seconds since midnight (default: false)
|
|
9
|
+
# @param relevant_files [Array<String>] A list of file names, useful to avoid loading tables you don't care about.
|
|
9
10
|
# @return [Feed] The loaded GTFS feed
|
|
10
|
-
def self.load_from_zip(zip_path, parse_times: false)
|
|
11
|
+
def self.load_from_zip(zip_path, parse_times: false, relevant_files: nil, extra: nil)
|
|
11
12
|
data = nil
|
|
12
13
|
|
|
13
|
-
relevant_files
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
relevant_files ||= GtfsDf::Feed::GTFS_FILES.map { |name| "#{name}.txt" }
|
|
15
|
+
relevant_files = relevant_files.to_set
|
|
16
|
+
extra ||= {}
|
|
17
|
+
extra_files = extra[:files] || []
|
|
18
|
+
extra_classes = extra[:classes] || {}
|
|
16
19
|
|
|
17
20
|
seen = {}
|
|
18
21
|
|
|
@@ -27,9 +30,11 @@ module GtfsDf
|
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
# We're skipping:
|
|
30
|
-
# -
|
|
33
|
+
# - files neither relevant nor supported by extra
|
|
31
34
|
# - empty feed files
|
|
32
|
-
|
|
35
|
+
file_name_truncated = file_name.split(".").first # without the .txt extension
|
|
36
|
+
relevant_or_extra = relevant_files.include?(file_name) || (extra_files.include?(file_name_truncated) && extra_classes.key?(file_name_truncated.to_sym))
|
|
37
|
+
next unless relevant_or_extra && has_header?(entry)
|
|
33
38
|
|
|
34
39
|
seen[file_name] = entry.name
|
|
35
40
|
|
|
@@ -37,7 +42,7 @@ module GtfsDf
|
|
|
37
42
|
end
|
|
38
43
|
end
|
|
39
44
|
|
|
40
|
-
data = load_from_dir(tmpdir, parse_times:
|
|
45
|
+
data = load_from_dir(tmpdir, parse_times:, relevant_files:, extra:)
|
|
41
46
|
end
|
|
42
47
|
|
|
43
48
|
data
|
|
@@ -47,26 +52,42 @@ module GtfsDf
|
|
|
47
52
|
#
|
|
48
53
|
# @param dir_path [String] Path to the GTFS directory
|
|
49
54
|
# @param parse_times [Boolean] Whether to parse time fields to seconds since midnight (default: false)
|
|
55
|
+
# @param relevant_files [Array<String>] A list of file names, useful to avoid loading tables you don't care about.
|
|
50
56
|
# @return [Feed] The loaded GTFS feed
|
|
51
|
-
def self.load_from_dir(dir_path, parse_times: false)
|
|
57
|
+
def self.load_from_dir(dir_path, parse_times: false, relevant_files: nil, extra: nil)
|
|
58
|
+
relevant_files ||= GtfsDf::Feed::GTFS_FILES.map { |name| "#{name}.txt" }
|
|
59
|
+
relevant_files = relevant_files.to_set
|
|
60
|
+
extra ||= {}
|
|
61
|
+
extra_files = extra[:files] || []
|
|
62
|
+
extra_classes = extra[:classes] || {}
|
|
63
|
+
|
|
52
64
|
data = {}
|
|
53
|
-
GtfsDf::Feed::GTFS_FILES.each do |gtfs_file|
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
(GtfsDf::Feed::GTFS_FILES + extra_files).each do |gtfs_file|
|
|
66
|
+
basename = "#{gtfs_file}.txt"
|
|
67
|
+
path = File.join(dir_path, basename)
|
|
68
|
+
relevant_or_extra = relevant_files.include?(basename) || (extra_files.include?(gtfs_file) && extra_classes.key?(gtfs_file.to_sym))
|
|
69
|
+
next unless relevant_or_extra && File.exist?(path)
|
|
56
70
|
|
|
57
|
-
data[gtfs_file] = data_frame(gtfs_file, path)
|
|
71
|
+
data[gtfs_file] = data_frame(gtfs_file, path, extra_classes)
|
|
58
72
|
end
|
|
59
73
|
|
|
60
|
-
GtfsDf::Feed.new(data, parse_times: parse_times)
|
|
74
|
+
GtfsDf::Feed.new(data, parse_times: parse_times, extra: extra)
|
|
61
75
|
end
|
|
62
76
|
|
|
63
|
-
private_class_method def self.data_frame(gtfs_file, path)
|
|
77
|
+
private_class_method def self.data_frame(gtfs_file, path, extra_classes)
|
|
78
|
+
custom_class = extra_classes[gtfs_file.to_sym]
|
|
79
|
+
return custom_class.new(path).df if custom_class
|
|
80
|
+
|
|
64
81
|
schema_class_name = gtfs_file.split("_").map(&:capitalize).join
|
|
65
82
|
GtfsDf::Schema.const_get(schema_class_name).new(path).df
|
|
66
83
|
end
|
|
67
84
|
|
|
68
85
|
private_class_method def self.has_header?(zip_entry)
|
|
69
|
-
zip_entry
|
|
86
|
+
zip_entry
|
|
87
|
+
.get_input_stream
|
|
88
|
+
.readline
|
|
89
|
+
.delete_prefix("\xEF\xBB\xBF".b) # BOM
|
|
90
|
+
.strip != ""
|
|
70
91
|
rescue
|
|
71
92
|
false
|
|
72
93
|
end
|
data/lib/gtfs_df/version.rb
CHANGED
data/lib/gtfs_df/writer.rb
CHANGED
|
@@ -11,13 +11,13 @@ module GtfsDf
|
|
|
11
11
|
require "zlib"
|
|
12
12
|
FileUtils.mkdir_p(File.dirname(zip_path))
|
|
13
13
|
Zip::File.open(zip_path, create: true) do |zipfile|
|
|
14
|
-
|
|
14
|
+
output_files(feed).each do |file|
|
|
15
15
|
df = feed.send(file)
|
|
16
16
|
next unless df.is_a?(Polars::DataFrame)
|
|
17
17
|
|
|
18
18
|
# Convert time fields back to strings if parse_times was enabled
|
|
19
19
|
if feed.parse_times
|
|
20
|
-
df = format_time_fields(file, df)
|
|
20
|
+
df = format_time_fields(file, df, feed)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Write CSV to StringIO
|
|
@@ -37,26 +37,34 @@ module GtfsDf
|
|
|
37
37
|
# @param dir_path [String] The path where the directory will be created
|
|
38
38
|
def self.write_to_dir(feed, dir_path)
|
|
39
39
|
FileUtils.mkdir_p(dir_path)
|
|
40
|
-
|
|
40
|
+
output_files(feed).each do |file|
|
|
41
41
|
df = feed.send(file)
|
|
42
42
|
next unless df.is_a?(Polars::DataFrame)
|
|
43
43
|
|
|
44
44
|
# Convert time fields back to strings if parse_times was enabled
|
|
45
|
-
df = format_time_fields(file, df) if feed.parse_times
|
|
45
|
+
df = format_time_fields(file, df, feed) if feed.parse_times
|
|
46
46
|
|
|
47
47
|
# Write CSV directly to file
|
|
48
48
|
df.write_csv(File.join(dir_path, "#{file}.txt"))
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def self.output_files(feed)
|
|
53
|
+
extra_files = feed.extra&.[](:files) || []
|
|
54
|
+
GtfsDf::Feed::GTFS_FILES + extra_files
|
|
55
|
+
end
|
|
56
|
+
private_class_method :output_files
|
|
57
|
+
|
|
52
58
|
# Formats time fields back to HH:MM:SS strings for a given GTFS file
|
|
53
59
|
#
|
|
54
60
|
# @param file [String] The GTFS file name (e.g., "stop_times")
|
|
55
61
|
# @param df [Polars::DataFrame] The DataFrame to format
|
|
56
62
|
# @return [Polars::DataFrame] DataFrame with time fields formatted as strings
|
|
57
|
-
def self.format_time_fields(file, df)
|
|
58
|
-
|
|
59
|
-
schema_class =
|
|
63
|
+
def self.format_time_fields(file, df, feed = nil)
|
|
64
|
+
extra_classes = feed&.extra&.[](:classes) || {}
|
|
65
|
+
schema_class = extra_classes[file.to_sym]
|
|
66
|
+
schema_class ||= begin
|
|
67
|
+
schema_class_name = file.split("_").map(&:capitalize).join
|
|
60
68
|
GtfsDf::Schema.const_get(schema_class_name)
|
|
61
69
|
rescue
|
|
62
70
|
nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gtfs_df
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Mejorado
|
|
@@ -74,7 +74,6 @@ files:
|
|
|
74
74
|
- ".rspec"
|
|
75
75
|
- ".rubocop.yml"
|
|
76
76
|
- ".ruby-version"
|
|
77
|
-
- ".solargraph.yml"
|
|
78
77
|
- ".standard.yml"
|
|
79
78
|
- CHANGELOG.md
|
|
80
79
|
- LICENSE.txt
|
data/.solargraph.yml
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
include:
|
|
3
|
-
- Rakefile
|
|
4
|
-
- Gemfile
|
|
5
|
-
- "*.gemspec"
|
|
6
|
-
- "**/*.rb"
|
|
7
|
-
exclude:
|
|
8
|
-
- spec/**/*
|
|
9
|
-
- test/**/*
|
|
10
|
-
- vendor/**/*
|
|
11
|
-
- ".bundle/**/*"
|
|
12
|
-
require: []
|
|
13
|
-
domains: []
|
|
14
|
-
reporters:
|
|
15
|
-
- rubocop
|
|
16
|
-
- require_not_found
|
|
17
|
-
formatter:
|
|
18
|
-
rubocop:
|
|
19
|
-
cops: safe
|
|
20
|
-
except: []
|
|
21
|
-
only: []
|
|
22
|
-
extra_args: []
|
|
23
|
-
lint: true
|
|
24
|
-
require_paths: []
|
|
25
|
-
plugins: []
|
|
26
|
-
max_files: 5000
|