forecaster 0.0.2 → 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.
- checksums.yaml +4 -4
- data/bin/forecast +5 -0
- data/lib/forecaster/cli.rb +243 -0
- data/lib/forecaster/configuration.rb +2 -1
- data/lib/forecaster/forecast.rb +78 -33
- data/lib/forecaster/version.rb +4 -0
- data/lib/forecaster.rb +11 -1
- metadata +128 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f918096d5bc3c84b8b1be9ded8a0e9597a3bf700
|
4
|
+
data.tar.gz: 73a46ed9e59a3c3f05cc45c5a654d8548055fa58
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dac29049eb42eb4cee58446c28a15d090c9abdfe76426db8c732e499b2cd1dcf8f9405d7a0bfa3479496525291de9e417c54a3cbdc687b87b1d9d73aa5fbe05a
|
7
|
+
data.tar.gz: 7406bb7e8d9de0afa5ff34854d826d71a34e5412a14871f82c0766d3f9793e0f212238473c32176518f8c45ed6402081efe43d7b3beee83f13dbd2a4f5221e71
|
data/bin/forecast
ADDED
@@ -0,0 +1,243 @@
|
|
1
|
+
require "yaml/store"
|
2
|
+
require "trollop"
|
3
|
+
require "chronic"
|
4
|
+
require "timezone"
|
5
|
+
require "geocoder"
|
6
|
+
require "ruby-progressbar"
|
7
|
+
|
8
|
+
require "forecaster"
|
9
|
+
|
10
|
+
# Fetch and read data from the Global Forecast System.
|
11
|
+
module Forecaster
|
12
|
+
# Command line interface printing the forecast for a time and a location.
|
13
|
+
class CLI
|
14
|
+
include Singleton # TODO: Find how best to organize CLI class
|
15
|
+
|
16
|
+
FORECAST_FORMAT = " %-15s % 7.1f %s".freeze
|
17
|
+
|
18
|
+
def self.start(args, env)
|
19
|
+
instance.start(args, env)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@store = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def start(args, env)
|
27
|
+
opts = parse(args)
|
28
|
+
|
29
|
+
configure(opts)
|
30
|
+
|
31
|
+
cache_file = File.join(Forecaster.configuration.cache_dir, "forecast.yml")
|
32
|
+
@store = YAML::Store.new(cache_file)
|
33
|
+
|
34
|
+
puts "GFS Weather Forecast"
|
35
|
+
puts
|
36
|
+
|
37
|
+
lat, lon = get_location(opts, env)
|
38
|
+
|
39
|
+
Trollop.die("Could not parse location") if lat.nil? || lon.nil?
|
40
|
+
|
41
|
+
ENV["TZ"] = get_timezone(lat, lon, env) || env["TZ"]
|
42
|
+
time = get_time(opts)
|
43
|
+
forecast = get_forecast(time, opts)
|
44
|
+
print_forecast(forecast, lat, lon)
|
45
|
+
ENV["TZ"] = env["TZ"] # Restore TZ
|
46
|
+
end
|
47
|
+
|
48
|
+
# Parse command line options
|
49
|
+
def parse(args)
|
50
|
+
opts = Trollop.options(args) do
|
51
|
+
usage "for <time> in <location>"
|
52
|
+
version "Forecaster v#{Forecaster::VERSION}"
|
53
|
+
opt :time, "Set time in any format", :type => String
|
54
|
+
opt :location, "Set location name", :type => String
|
55
|
+
opt :latitude, "Set latitude in degree", :type => Float, :short => "a"
|
56
|
+
opt :longitude, "Set longitude in degree", :type => Float, :short => "o"
|
57
|
+
opt :cache, "Set cache directory", :default => "/tmp/forecaster"
|
58
|
+
opt :debug, "Enable debug mode"
|
59
|
+
end
|
60
|
+
|
61
|
+
cmd_opts = { :time => [], :location => [] }
|
62
|
+
key = :time
|
63
|
+
args.each do |word|
|
64
|
+
case word
|
65
|
+
when "for"
|
66
|
+
key = :time
|
67
|
+
when "in"
|
68
|
+
key = :location
|
69
|
+
else
|
70
|
+
cmd_opts[key] << word
|
71
|
+
end
|
72
|
+
end
|
73
|
+
opts[:time] ||= cmd_opts[:time].join(" ")
|
74
|
+
opts[:location] ||= cmd_opts[:location].join(" ")
|
75
|
+
|
76
|
+
opts
|
77
|
+
end
|
78
|
+
|
79
|
+
# Configure gem
|
80
|
+
def configure(opts)
|
81
|
+
Forecaster.configure do |config|
|
82
|
+
config.cache_dir = opts[:cache]
|
83
|
+
config.records = {
|
84
|
+
:prate => ":PRATE:surface:",
|
85
|
+
:pres => ":PRES:surface:",
|
86
|
+
:rh => ":RH:2 m above ground:",
|
87
|
+
:tmp => ":TMP:2 m above ground:",
|
88
|
+
:ugrd => ":UGRD:10 m above ground:",
|
89
|
+
:vgrd => ":VGRD:10 m above ground:",
|
90
|
+
:tcdc => ":TCDC:entire atmosphere:"
|
91
|
+
}
|
92
|
+
end
|
93
|
+
FileUtils.mkpath(Forecaster.configuration.cache_dir)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get location
|
97
|
+
def get_location(opts, env)
|
98
|
+
if opts[:location]
|
99
|
+
@store.transaction do
|
100
|
+
if opts[:debug]
|
101
|
+
puts format("%-15s '%s'", "Geolocalizing:", opts[:location])
|
102
|
+
end
|
103
|
+
|
104
|
+
key = "geocoder:#{opts[:location]}"
|
105
|
+
lat, lon = @store[key] ||= geolocalize(opts[:location])
|
106
|
+
|
107
|
+
if opts[:debug]
|
108
|
+
if lat && lon
|
109
|
+
puts format("%-15s %s, %s", "Found:", lat, lon)
|
110
|
+
else
|
111
|
+
puts "Not found"
|
112
|
+
end
|
113
|
+
puts
|
114
|
+
end
|
115
|
+
|
116
|
+
[lat, lon]
|
117
|
+
end
|
118
|
+
elsif opts[:latitude] && opts[:longitude]
|
119
|
+
[opts[:latitude], opts[:longitude]]
|
120
|
+
else
|
121
|
+
[env["FORECAST_LATITUDE"], env["FORECAST_LONGITUDE"]]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get timezone
|
126
|
+
def get_timezone(lat, lon, env)
|
127
|
+
tz = nil
|
128
|
+
if env["GEONAMES_USERNAME"]
|
129
|
+
Timezone::Lookup.config(:geonames) do |config|
|
130
|
+
config.username = env["GEONAMES_USERNAME"]
|
131
|
+
end
|
132
|
+
@store.transaction do
|
133
|
+
key = "timezone:#{lat}:#{lon}"
|
134
|
+
tz = @store[key] || @store[key] = Timezone.lookup(lat, lon).name
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
tz
|
139
|
+
end
|
140
|
+
|
141
|
+
# Get time
|
142
|
+
def get_time(opts)
|
143
|
+
if opts[:time]
|
144
|
+
# TODO: Look for a timestamp first
|
145
|
+
time = Chronic.parse(opts[:time])
|
146
|
+
Trollop.die(:time, "could not be parsed") if time.nil?
|
147
|
+
time.utc
|
148
|
+
else
|
149
|
+
Time.now.utc
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Get forecast
|
154
|
+
def get_forecast(time, opts)
|
155
|
+
forecast = Forecast.at(time)
|
156
|
+
|
157
|
+
if opts[:debug]
|
158
|
+
puts format("%-15s %s", "Requested time:", time.localtime)
|
159
|
+
puts format("%-15s %s", "GFS run time:", forecast.run_time.localtime)
|
160
|
+
puts format("%-15s %s", "Forecast time:", forecast.time.localtime)
|
161
|
+
puts
|
162
|
+
end
|
163
|
+
|
164
|
+
unless forecast.fetched?
|
165
|
+
if opts[:debug]
|
166
|
+
puts "Downloading: '#{forecast.url}'"
|
167
|
+
|
168
|
+
puts "Reading index file..."
|
169
|
+
ranges = forecast.fetch_index
|
170
|
+
|
171
|
+
filesize = ranges.reduce(0) do |acc, range|
|
172
|
+
first, last = range.split("-").map(&:to_i)
|
173
|
+
acc + last - first
|
174
|
+
end
|
175
|
+
filesize_in_megabytes = (filesize.to_f / (1 << 20)).round(2)
|
176
|
+
puts "Length: #{filesize} (#{filesize_in_megabytes}M)"
|
177
|
+
puts
|
178
|
+
|
179
|
+
progressbar = ProgressBar.create(
|
180
|
+
:format => "%p%% [%b>%i] %r KB/s %e",
|
181
|
+
:rate_scale => lambda { |rate| rate / 1024 }
|
182
|
+
)
|
183
|
+
|
184
|
+
progress_block = lambda do |progress, total|
|
185
|
+
progressbar.total = total
|
186
|
+
progressbar.progress = progress
|
187
|
+
end
|
188
|
+
|
189
|
+
forecast.fetch_grib2(ranges, :progress_block => progress_block)
|
190
|
+
|
191
|
+
progressbar.finish
|
192
|
+
puts
|
193
|
+
else
|
194
|
+
forecast.fetch # That's a lot easier ^^
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
forecast
|
199
|
+
end
|
200
|
+
|
201
|
+
# Print forecast
|
202
|
+
def print_forecast(forecast, lat, lon)
|
203
|
+
forecast_time = forecast.time.localtime
|
204
|
+
|
205
|
+
puts format(" %-11s % 11s", "Date:", forecast_time.strftime("%Y-%m-%d"))
|
206
|
+
puts format(" %-11s % 11s", "Time:", forecast_time.strftime("%T"))
|
207
|
+
puts format(" %-11s % 11s", "Zone:", forecast_time.strftime("%z"))
|
208
|
+
puts format(FORECAST_FORMAT, "Latitude:", lat, "°")
|
209
|
+
puts format(FORECAST_FORMAT, "Longitude:", lon, "°")
|
210
|
+
puts
|
211
|
+
|
212
|
+
pres = forecast.read(:pres, :latitude => lat, :longitude => lon).to_f
|
213
|
+
tmp = forecast.read(:tmp, :latitude => lat, :longitude => lon).to_f
|
214
|
+
ugrd = forecast.read(:ugrd, :latitude => lat, :longitude => lon).to_f
|
215
|
+
vgrd = forecast.read(:vgrd, :latitude => lat, :longitude => lon).to_f
|
216
|
+
prate = forecast.read(:prate, :latitude => lat, :longitude => lon).to_f
|
217
|
+
rh = forecast.read(:rh, :latitude => lat, :longitude => lon).to_f
|
218
|
+
tcdc = forecast.read(:tcdc, :latitude => lat, :longitude => lon).to_f
|
219
|
+
|
220
|
+
pressure = pres / 100.0
|
221
|
+
temperature = tmp - 273.15
|
222
|
+
wind_speed = Math.sqrt(ugrd**2 + vgrd**2)
|
223
|
+
wind_direction = (270 - Math.atan2(ugrd, vgrd) * 180 / Math::PI) % 360
|
224
|
+
precipitation = prate * 3600
|
225
|
+
humidity = rh
|
226
|
+
cloud_cover = tcdc
|
227
|
+
|
228
|
+
puts format(FORECAST_FORMAT, "Pressure:", pressure, "hPa")
|
229
|
+
puts format(FORECAST_FORMAT, "Temperature:", temperature, "°C")
|
230
|
+
puts format(FORECAST_FORMAT, "Wind Speed:", wind_speed, "m/s")
|
231
|
+
puts format(FORECAST_FORMAT, "Wind Direction:", wind_direction, "°")
|
232
|
+
puts format(FORECAST_FORMAT, "Precipitation:", precipitation, "mm")
|
233
|
+
puts format(FORECAST_FORMAT, "Humidity:", humidity, "%")
|
234
|
+
puts format(FORECAST_FORMAT, "Cloud Cover:", cloud_cover, "%")
|
235
|
+
end
|
236
|
+
|
237
|
+
def geolocalize(location)
|
238
|
+
Geocoder.configure(:timeout => 10)
|
239
|
+
res = Geocoder.search(location).first
|
240
|
+
[res.latitude, res.longitude] if res
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -1,11 +1,12 @@
|
|
1
|
+
# Fetch and read data from the Global Forecast System.
|
1
2
|
module Forecaster
|
3
|
+
# Configure how to fetch and read from a forecast file.
|
2
4
|
class Configuration
|
3
5
|
attr_accessor :server, :cache_dir, :curl_path, :wgrib2_path, :records
|
4
6
|
|
5
7
|
def initialize
|
6
8
|
@server = "http://www.ftp.ncep.noaa.gov/data/nccf/com/gfs/prod"
|
7
9
|
@cache_dir = "/tmp/forecaster"
|
8
|
-
@curl_path = "curl"
|
9
10
|
@wgrib2_path = "wgrib2"
|
10
11
|
|
11
12
|
# See: http://www.nco.ncep.noaa.gov/pmb/products/gfs/gfs_upgrade/gfs.t06z.pgrb2.0p25.f006.shtml
|
data/lib/forecaster/forecast.rb
CHANGED
@@ -1,33 +1,68 @@
|
|
1
1
|
require "fileutils"
|
2
|
+
require "excon"
|
2
3
|
|
4
|
+
# Fetch and read data from the Global Forecast System.
|
3
5
|
module Forecaster
|
6
|
+
# Fetch and read a specific forecast from a GFS run.
|
7
|
+
#
|
8
|
+
# See: http://www.nco.ncep.noaa.gov/pmb/products/gfs/
|
4
9
|
class Forecast
|
5
|
-
def
|
10
|
+
def self.last_run_at
|
11
|
+
# There is a new GFS run every 6 hours starting at midnight UTC, and it
|
12
|
+
# takes approximately 3 to 5 hours before a run is available online, so
|
13
|
+
# to be on the safe side we return the previous one.
|
14
|
+
now = Time.now.utc
|
15
|
+
run = Time.new(now.year, now.month, now.day, (now.hour / 6) * 6)
|
16
|
+
|
17
|
+
run - 6 * 3600
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.at(time)
|
21
|
+
# There is a forecast every 3 hours after a run for 384 hours.
|
22
|
+
t = time.utc
|
23
|
+
fct = Time.new(t.year, t.month, t.day, (t.hour / 3) * 3)
|
24
|
+
run = Time.new(t.year, t.month, t.day, (t.hour / 6) * 6)
|
25
|
+
run -= 6 * 3600 if run == fct
|
26
|
+
|
27
|
+
last_run = Forecast.last_run_at
|
28
|
+
run = last_run if run > last_run
|
29
|
+
|
30
|
+
fct_hour = (fct - run) / 3600
|
31
|
+
|
32
|
+
raise "Time too far in the future" if fct_hour > 384
|
33
|
+
|
34
|
+
Forecast.new(run.year, run.month, run.day, run.hour, fct_hour)
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(year, month, day, run_hour, fct_hour)
|
6
38
|
@year = year
|
7
39
|
@month = month
|
8
40
|
@day = day
|
9
|
-
@
|
10
|
-
@
|
41
|
+
@run_hour = run_hour
|
42
|
+
@fct_hour = fct_hour
|
43
|
+
end
|
44
|
+
|
45
|
+
def run_time
|
46
|
+
Time.utc(@year, @month, @day, @run_hour)
|
47
|
+
end
|
48
|
+
|
49
|
+
def time
|
50
|
+
run_time + @fct_hour * 3600
|
11
51
|
end
|
12
52
|
|
13
53
|
def dirname
|
14
|
-
subdir = "%04d%02d%02d%02d"
|
15
|
-
@year, @month, @day, @hour_of_run
|
16
|
-
]
|
54
|
+
subdir = format("%04d%02d%02d%02d", @year, @month, @day, @run_hour)
|
17
55
|
File.join(Forecaster.configuration.cache_dir, subdir)
|
18
56
|
end
|
19
57
|
|
20
58
|
def filename
|
21
|
-
"gfs.t%02dz.pgrb2.0p25.f%03d"
|
22
|
-
@hour_of_run, @hour_of_forecast
|
23
|
-
]
|
59
|
+
format("gfs.t%02dz.pgrb2.0p25.f%03d", @run_hour, @fct_hour)
|
24
60
|
end
|
25
61
|
|
26
62
|
def url
|
27
63
|
server = Forecaster.configuration.server
|
28
|
-
"
|
29
|
-
|
30
|
-
]
|
64
|
+
subdir = format("gfs.%04d%02d%02d%02d", @year, @month, @day, @run_hour)
|
65
|
+
format("%s/%s/%s", server, subdir, filename)
|
31
66
|
end
|
32
67
|
|
33
68
|
def fetched?
|
@@ -38,44 +73,54 @@ module Forecaster
|
|
38
73
|
# But only the parts of the file containing the fields defined in
|
39
74
|
# the configuration will be downloaded.
|
40
75
|
def fetch
|
41
|
-
return
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
FileUtils.mkpath(File.join(dirname))
|
46
|
-
|
47
|
-
path = File.join(dirname, filename)
|
76
|
+
return if fetched?
|
77
|
+
ranges = fetch_index
|
78
|
+
fetch_grib2(ranges)
|
79
|
+
end
|
48
80
|
|
49
|
-
|
50
|
-
|
51
|
-
|
81
|
+
def fetch_index
|
82
|
+
begin
|
83
|
+
res = Excon.get("#{url}.idx")
|
84
|
+
rescue Excon::Errors::Error
|
85
|
+
raise "Download of '#{url}.idx' failed"
|
86
|
+
end
|
52
87
|
|
53
|
-
lines =
|
54
|
-
|
55
|
-
ranges = lines.each_index.reduce([]) do |r, i|
|
88
|
+
lines = res.body.lines
|
89
|
+
lines.each_index.reduce([]) do |r, i|
|
56
90
|
records = Forecaster.configuration.records
|
57
91
|
if records.values.any? { |record| lines[i].include?(record) }
|
58
92
|
first = lines[i].split(":")[1].to_i
|
59
93
|
last = ""
|
60
94
|
|
61
95
|
j = i
|
62
|
-
while (j += 1) <
|
96
|
+
while (j += 1) < lines.count
|
63
97
|
last = lines[j].split(":")[1].to_i - 1
|
64
98
|
break if last != first - 1
|
65
99
|
end
|
66
100
|
|
67
|
-
r << "#{first}-#{last}" #
|
101
|
+
r << "#{first}-#{last}" # Range header syntax
|
68
102
|
else
|
69
103
|
r
|
70
104
|
end
|
71
105
|
end
|
72
|
-
|
106
|
+
end
|
73
107
|
|
74
|
-
|
75
|
-
|
76
|
-
|
108
|
+
def fetch_grib2(ranges, progress_block: nil)
|
109
|
+
FileUtils.mkpath(dirname)
|
110
|
+
path = File.join(dirname, filename)
|
77
111
|
|
78
|
-
|
112
|
+
streamer = lambda do |chunk, remaining, total|
|
113
|
+
File.open(path, "ab") { |f| f.write(chunk) }
|
114
|
+
progress_block.call(total - remaining, total) if progress_block
|
115
|
+
end
|
116
|
+
|
117
|
+
headers = { "Range" => "bytes=#{ranges.join(',')}" }
|
118
|
+
begin
|
119
|
+
Excon.get(url, :headers => headers, :response_block => streamer)
|
120
|
+
rescue Excon::Errors::Error => e
|
121
|
+
File.delete(path)
|
122
|
+
raise "Download of '#{url}' failed: #{e}"
|
123
|
+
end
|
79
124
|
end
|
80
125
|
|
81
126
|
def read(field, latitude: 0.0, longitude: 0.0)
|
@@ -83,7 +128,7 @@ module Forecaster
|
|
83
128
|
record = Forecaster.configuration.records[field]
|
84
129
|
path = File.join(dirname, filename)
|
85
130
|
|
86
|
-
raise "'#{path}' not found" unless File.
|
131
|
+
raise "'#{path}' not found" unless File.exist?(path)
|
87
132
|
|
88
133
|
coords = "#{longitude} #{latitude}"
|
89
134
|
output = `#{wgrib2} #{path} -lon #{coords} -match "#{record}"`
|
data/lib/forecaster.rb
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
require "forecaster/configuration"
|
2
2
|
require "forecaster/forecast"
|
3
|
+
require "forecaster/version"
|
4
|
+
require "forecaster/cli"
|
3
5
|
|
6
|
+
# Fetch and read data from the Global Forecast System.
|
4
7
|
module Forecaster
|
5
8
|
class << self
|
6
9
|
attr_accessor :configuration
|
@@ -12,6 +15,13 @@ module Forecaster
|
|
12
15
|
end
|
13
16
|
|
14
17
|
def self.fetch(year, month, day, hour_of_run, hour_of_forecast)
|
15
|
-
|
18
|
+
y = year
|
19
|
+
m = month
|
20
|
+
d = day
|
21
|
+
c = hour_of_run
|
22
|
+
h = hour_of_forecast
|
23
|
+
forecast = Forecaster::Forecast.new(y, m, d, c, h)
|
24
|
+
forecast.fetch
|
25
|
+
forecast
|
16
26
|
end
|
17
27
|
end
|
metadata
CHANGED
@@ -1,24 +1,148 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: forecaster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vincent Ollivier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-06-
|
12
|
-
dependencies:
|
11
|
+
date: 2016-06-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: excon
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.49'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.49.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.49'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.49.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: trollop
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '2.1'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 2.1.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '2.1'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 2.1.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: chronic
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0.10'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 0.10.0
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0.10'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 0.10.0
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: timezone
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0.99'
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.99.0
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.99'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 0.99.0
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: geocoder
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '1.3'
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 1.3.0
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.3'
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 1.3.0
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: ruby-progressbar
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '1.8'
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 1.8.0
|
123
|
+
type: :runtime
|
124
|
+
prerelease: false
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '1.8'
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: 1.8.0
|
13
133
|
description: Wrapper around curl and wgrib2 to fetch and read GFS data
|
14
134
|
email: v@vinc.cc
|
15
|
-
executables:
|
135
|
+
executables:
|
136
|
+
- forecast
|
16
137
|
extensions: []
|
17
138
|
extra_rdoc_files: []
|
18
139
|
files:
|
140
|
+
- bin/forecast
|
19
141
|
- lib/forecaster.rb
|
142
|
+
- lib/forecaster/cli.rb
|
20
143
|
- lib/forecaster/configuration.rb
|
21
144
|
- lib/forecaster/forecast.rb
|
145
|
+
- lib/forecaster/version.rb
|
22
146
|
homepage: https://github.com/vinc/forecaster
|
23
147
|
licenses:
|
24
148
|
- MIT
|