weather-sage 0.1.0 → 0.1.1
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/lib/weather-sage/cache-entry.rb +13 -0
- data/lib/weather-sage/cache.rb +79 -0
- data/lib/weather-sage/census/geocoder.rb +48 -0
- data/lib/weather-sage/census/match.rb +19 -0
- data/lib/weather-sage/census.rb +7 -0
- data/lib/weather-sage/cli/commands/base-forecast.rb +70 -0
- data/lib/weather-sage/cli/commands/command.rb +42 -0
- data/lib/weather-sage/cli/commands/forecast.rb +28 -0
- data/lib/weather-sage/cli/commands/geocode.rb +51 -0
- data/lib/weather-sage/cli/commands/help.rb +82 -0
- data/lib/weather-sage/cli/commands/hourly.rb +28 -0
- data/lib/weather-sage/cli/commands/now.rb +87 -0
- data/lib/weather-sage/cli/commands/stations.rb +67 -0
- data/lib/weather-sage/cli/commands.rb +15 -0
- data/lib/weather-sage/cli/env/cache.rb +38 -0
- data/lib/weather-sage/cli/env/context.rb +29 -0
- data/lib/weather-sage/cli/env/env.rb +35 -0
- data/lib/weather-sage/cli/env/log.rb +41 -0
- data/lib/weather-sage/cli/env/vars.rb +20 -0
- data/lib/weather-sage/cli/env.rb +14 -0
- data/lib/weather-sage/cli/forecast.rb +70 -0
- data/lib/weather-sage/cli/help.rb +68 -0
- data/lib/weather-sage/cli.rb +29 -0
- data/lib/weather-sage/context.rb +13 -0
- data/lib/weather-sage/http/cache.rb +72 -0
- data/lib/weather-sage/http/error.rb +15 -0
- data/lib/weather-sage/http/fetcher.rb +79 -0
- data/lib/weather-sage/http/parser.rb +46 -0
- data/lib/weather-sage/http.rb +9 -0
- data/lib/weather-sage/weather/base-object.rb +36 -0
- data/lib/weather-sage/weather/forecast.rb +47 -0
- data/lib/weather-sage/weather/observation.rb +33 -0
- data/lib/weather-sage/weather/period.rb +44 -0
- data/lib/weather-sage/weather/point.rb +71 -0
- data/lib/weather-sage/weather/station.rb +43 -0
- data/lib/weather-sage/weather.rb +11 -0
- data/lib/weather-sage.rb +1 -1
- metadata +39 -3
- data/test/make-csvs.sh +0 -10
@@ -0,0 +1,29 @@
|
|
1
|
+
#
|
2
|
+
# Create Context from environment variables.
|
3
|
+
#
|
4
|
+
class WeatherSage::CLI::Env::Context < ::WeatherSage::Context
|
5
|
+
#
|
6
|
+
# Create Context from environment variables.
|
7
|
+
#
|
8
|
+
# The following environment variables are supported:
|
9
|
+
#
|
10
|
+
# - WEATHER_SAGE_LOG_LEVEL: Log level. One of "fatal", "error",
|
11
|
+
# "warning", "info", or "debug". Defaults to "warn".
|
12
|
+
#
|
13
|
+
# - WEATHER_SAGE_LOG_PATH: Path to log file. Defaults to standard
|
14
|
+
# error.
|
15
|
+
#
|
16
|
+
# - WEATHER_SAGE_CACHE_PATH: Path to HTTP cache file. Defaults to
|
17
|
+
# "~/.config/weather-sage/http-cache.pstore".
|
18
|
+
#
|
19
|
+
def initialize(env)
|
20
|
+
# create log from environment
|
21
|
+
log = ::WeatherSage::CLI::Env::Log.new(env)
|
22
|
+
|
23
|
+
# create cache from environment and log
|
24
|
+
cache = ::WeatherSage::CLI::Env::Cache.new(env, log)
|
25
|
+
|
26
|
+
# create context
|
27
|
+
super(log, cache)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#
|
2
|
+
# Environment wrapper.
|
3
|
+
#
|
4
|
+
class WeatherSage::CLI::Env::Env
|
5
|
+
#
|
6
|
+
# Create a new environment wrapper.
|
7
|
+
#
|
8
|
+
def initialize(env)
|
9
|
+
@env = env
|
10
|
+
end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Get the value of the given environment variable.
|
14
|
+
#
|
15
|
+
def get(id, default = nil)
|
16
|
+
key = expand(id)
|
17
|
+
@env.key?(key) ? @env[key] : default
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Does the given ID exist in the environment?
|
22
|
+
#
|
23
|
+
def key?(id)
|
24
|
+
@env.key?(expand(id))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
#
|
30
|
+
# Prefix ID to get full environment variable name.
|
31
|
+
#
|
32
|
+
def expand(id)
|
33
|
+
'WEATHER_SAGE_' + id.upcase
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Create Logger from environment variables.
|
5
|
+
#
|
6
|
+
class WeatherSage::CLI::Env::Log < ::Logger
|
7
|
+
#
|
8
|
+
# Default log level.
|
9
|
+
#
|
10
|
+
DEFAULT_LEVEL = 'WARN'
|
11
|
+
|
12
|
+
#
|
13
|
+
# Create Logger from environment variables.
|
14
|
+
#
|
15
|
+
# The following environment variables are supported:
|
16
|
+
#
|
17
|
+
# - WEATHER_SAGE_LOG_LEVEL: Log level. One of "fatal", "error",
|
18
|
+
# "warning", "info", or "debug". Defaults to "warn".
|
19
|
+
#
|
20
|
+
# - WEATHER_SAGE_LOG_PATH: Path to log file. Defaults to standard
|
21
|
+
# error.
|
22
|
+
#
|
23
|
+
def initialize(env)
|
24
|
+
# get log level (default to "warn" if unspecified)
|
25
|
+
level = (env.get('LOG_LEVEL', DEFAULT_LEVEL)).upcase
|
26
|
+
|
27
|
+
# create logger
|
28
|
+
super(
|
29
|
+
# log path (default to STDERR)
|
30
|
+
env.get('LOG_PATH', STDERR),
|
31
|
+
|
32
|
+
# log level (default to WARN)
|
33
|
+
level: ::Logger.const_get(level)
|
34
|
+
)
|
35
|
+
|
36
|
+
# log level
|
37
|
+
info('Env::Log#initialize') do
|
38
|
+
'level = %p' % [level]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module WeatherSage::CLI::Env
|
2
|
+
#
|
3
|
+
# Environment variables.
|
4
|
+
#
|
5
|
+
# Used by *help* command to print available environment variables.
|
6
|
+
#
|
7
|
+
VARS = [{
|
8
|
+
name: 'LOG_LEVEL',
|
9
|
+
text: "Log level (fatal, error, warning, info, debug)",
|
10
|
+
default: 'warn',
|
11
|
+
}, {
|
12
|
+
name: 'LOG_PATH',
|
13
|
+
text: "Path to log file",
|
14
|
+
default: 'standard error',
|
15
|
+
}, {
|
16
|
+
name: 'CACHE_PATH',
|
17
|
+
text: "Path to HTTP cache store",
|
18
|
+
default: "~/.config/weather-sage/http-cache.pstore",
|
19
|
+
}].freeze
|
20
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module WeatherSage
|
2
|
+
module CLI
|
3
|
+
#
|
4
|
+
# Namespace for environment-related classes.
|
5
|
+
#
|
6
|
+
module Env
|
7
|
+
autoload :VARS, File.join(__dir__, 'env', 'vars.rb')
|
8
|
+
autoload :Env, File.join(__dir__, 'env', 'env.rb')
|
9
|
+
autoload :Log, File.join(__dir__, 'env', 'log.rb')
|
10
|
+
autoload :Cache, File.join(__dir__, 'env', 'cache.rb')
|
11
|
+
autoload :Context, File.join(__dir__, 'env', 'context.rb')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#
|
2
|
+
# Namespace containing data for *forecast* and *hourly* commands.
|
3
|
+
#
|
4
|
+
module WeatherSage::CLI::Forecast
|
5
|
+
#
|
6
|
+
# List of forecast CSV columns and properties.
|
7
|
+
#
|
8
|
+
COLUMNS = [{
|
9
|
+
name: 'address',
|
10
|
+
prop: nil,
|
11
|
+
show: %i{forecast hourly_forecast},
|
12
|
+
}, {
|
13
|
+
name: 'name',
|
14
|
+
prop: 'name',
|
15
|
+
show: %i{forecast},
|
16
|
+
}, {
|
17
|
+
name: 'start_time',
|
18
|
+
prop: 'startTime',
|
19
|
+
show: %i{hourly_forecast},
|
20
|
+
}, {
|
21
|
+
name: 'end_time',
|
22
|
+
prop: 'endTime',
|
23
|
+
show: %i{hourly_forecast},
|
24
|
+
}, {
|
25
|
+
name: 'is_daytime',
|
26
|
+
prop: 'isDaytime',
|
27
|
+
show: [],
|
28
|
+
}, {
|
29
|
+
name: 'temperature',
|
30
|
+
prop: 'temperature',
|
31
|
+
show: %i{forecast hourly_forecast},
|
32
|
+
}, {
|
33
|
+
name: 'temperature_unit',
|
34
|
+
prop: 'temperatureUnit',
|
35
|
+
show: %i{forecast hourly_forecast},
|
36
|
+
}, {
|
37
|
+
name: 'temperature_trend',
|
38
|
+
prop: 'temperatureTrend',
|
39
|
+
show: [],
|
40
|
+
}, {
|
41
|
+
name: 'wind_speed',
|
42
|
+
prop: 'windSpeed',
|
43
|
+
show: %i{forecast hourly_forecast},
|
44
|
+
}, {
|
45
|
+
name: 'wind_direction',
|
46
|
+
prop: 'windDirection',
|
47
|
+
show: %i{forecast hourly_forecast},
|
48
|
+
}, {
|
49
|
+
name: 'icon',
|
50
|
+
prop: 'icon',
|
51
|
+
show: [],
|
52
|
+
}, {
|
53
|
+
name: 'short_forecast',
|
54
|
+
prop: 'shortForecast',
|
55
|
+
show: %i{forecast hourly_forecast},
|
56
|
+
}, {
|
57
|
+
name: 'detailed_forecast',
|
58
|
+
prop: 'detailedForecast',
|
59
|
+
show: [],
|
60
|
+
}].freeze
|
61
|
+
|
62
|
+
#
|
63
|
+
# Get columns for given forecast method and mode.
|
64
|
+
#
|
65
|
+
def self.columns(forecast_method, mode)
|
66
|
+
COLUMNS.select { |col|
|
67
|
+
(mode == :full) || col[:show].include?(forecast_method)
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
#
|
2
|
+
# Namespace for data for *help* command.
|
3
|
+
#
|
4
|
+
module WeatherSage::CLI::Help
|
5
|
+
#
|
6
|
+
# Help templates.
|
7
|
+
#
|
8
|
+
TEMPLATES = {
|
9
|
+
all: [
|
10
|
+
'%<app>s: Weather and geocoding utility.',
|
11
|
+
'',
|
12
|
+
'This command uses the Census bureau geocoding API to convert',
|
13
|
+
'street addresses to latitude/longitude coordinates, and the',
|
14
|
+
'National Weather Service weather API to obtain observations',
|
15
|
+
'from the nearest weather station.',
|
16
|
+
'',
|
17
|
+
'Usage:',
|
18
|
+
' %<app>s <command> [args...]',
|
19
|
+
'',
|
20
|
+
'Commands:',
|
21
|
+
'%<cmds>s',
|
22
|
+
'',
|
23
|
+
'Use "help <command>" to see detailed help for a command.',
|
24
|
+
'',
|
25
|
+
'Environment Variables:',
|
26
|
+
'%<envs>s',
|
27
|
+
].join("\n"),
|
28
|
+
|
29
|
+
one: [
|
30
|
+
'%<label>s %<line>s',
|
31
|
+
'',
|
32
|
+
'%<full>s',
|
33
|
+
].join("\n"),
|
34
|
+
|
35
|
+
cmd: ' %-10<label>s %<line>s',
|
36
|
+
|
37
|
+
env: [
|
38
|
+
'* WEATHER_SAGE_%<name>s: %<text>s.',
|
39
|
+
' Defaults to %<default>s.',
|
40
|
+
].join("\n"),
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
#
|
44
|
+
# Build map of command name to command help.
|
45
|
+
#
|
46
|
+
# Note: The keys in this hash are sorted by command name.
|
47
|
+
#
|
48
|
+
COMMANDS = WeatherSage::CLI::Commands.constants.select { |sym|
|
49
|
+
sym.to_s.match(/^\w+Command$/)
|
50
|
+
}.sort.reduce({}) do |r, sym|
|
51
|
+
# limit to commands with a HELP constant
|
52
|
+
if WeatherSage::CLI::Commands.const_get(sym).const_defined?(:HELP)
|
53
|
+
# get help data
|
54
|
+
help = WeatherSage::CLI::Commands.const_get(sym).const_get(:HELP)
|
55
|
+
|
56
|
+
# build command id
|
57
|
+
id = sym.to_s.gsub(/Command$/, '').downcase
|
58
|
+
|
59
|
+
# add to results
|
60
|
+
r[id] = help.merge({
|
61
|
+
label: id + ':',
|
62
|
+
})
|
63
|
+
end
|
64
|
+
|
65
|
+
# return results
|
66
|
+
r
|
67
|
+
end.freeze
|
68
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#
|
2
|
+
# Command-line interface for weather-sage.
|
3
|
+
#
|
4
|
+
module WeatherSage::CLI
|
5
|
+
autoload :Commands, File.join(__dir__, 'cli', 'commands.rb')
|
6
|
+
autoload :Env, File.join(__dir__, 'cli', 'env.rb')
|
7
|
+
autoload :Help, File.join(__dir__, 'cli', 'help.rb')
|
8
|
+
autoload :Forecast, File.join(__dir__, 'cli', 'forecast.rb')
|
9
|
+
|
10
|
+
#
|
11
|
+
# Entry point for command-line interface.
|
12
|
+
#
|
13
|
+
def self.run(app, args)
|
14
|
+
require 'csv'
|
15
|
+
require 'logger'
|
16
|
+
require 'fileutils'
|
17
|
+
|
18
|
+
args = ['help'] unless args.size > 0
|
19
|
+
|
20
|
+
# wrap environment and create context
|
21
|
+
env = Env::Env.new(ENV)
|
22
|
+
ctx = Env::Context.new(env)
|
23
|
+
|
24
|
+
# map first argument to command, then run it
|
25
|
+
(Commands.const_get('%sCommand' % [
|
26
|
+
args.shift.capitalize
|
27
|
+
]) || Commands::HelpCommand).run(ctx, app, args)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
#
|
2
|
+
# HTTP cache backed by file.
|
3
|
+
#
|
4
|
+
class WeatherSage::HTTP::Cache
|
5
|
+
attr :path, :timeout
|
6
|
+
|
7
|
+
#
|
8
|
+
# Create an HTTP cache backed by file at +path+ where entries are
|
9
|
+
# valid for +timeout+ seconds.
|
10
|
+
#
|
11
|
+
# +timeout+ defaults to 30 minutes if unspecified.
|
12
|
+
#
|
13
|
+
def initialize(path, log, timeout = 30 * 60)
|
14
|
+
@path, @log, @timeout = path.freeze, log, timeout
|
15
|
+
@cache = ::WeatherSage::Cache.new(@path)
|
16
|
+
@fetcher = ::WeatherSage::HTTP::Fetcher.new(@log)
|
17
|
+
@parser = ::WeatherSage::HTTP::Parser.new(@log)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Get cached URL, or request it if it is not cached.
|
22
|
+
#
|
23
|
+
def get(url, params = {})
|
24
|
+
# parse URL into URI, get key
|
25
|
+
uri = make_uri(url, params)
|
26
|
+
str = uri.to_s
|
27
|
+
|
28
|
+
@log.debug('HTTP::Cache#get') { '%s' % [str] }
|
29
|
+
|
30
|
+
unless r = @cache.get(str)
|
31
|
+
# fetch response, parse body, and cache result
|
32
|
+
r = @cache.set(str, parse(fetch(uri)), @timeout)
|
33
|
+
end
|
34
|
+
|
35
|
+
# return result
|
36
|
+
r
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Returns true if the given URL in the cache.
|
41
|
+
#
|
42
|
+
def key?(url, params = {})
|
43
|
+
@cache.key?(make_uri(url, params).to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
#
|
49
|
+
# Convert a URL and parameters to a URI.
|
50
|
+
#
|
51
|
+
def make_uri(url, params = {})
|
52
|
+
uri = URI.parse(url)
|
53
|
+
uri.query = URI.encode_www_form(params) if params.size > 0
|
54
|
+
uri
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# Fetch URI, and return response.
|
59
|
+
#
|
60
|
+
# Raises an WeatherSage::HTTP::Error on error.
|
61
|
+
#
|
62
|
+
def fetch(uri)
|
63
|
+
@fetcher.fetch(uri)
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Parse HTTP response body.
|
68
|
+
#
|
69
|
+
def parse(resp)
|
70
|
+
@parser.parse(resp)
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
#
|
2
|
+
# HTTP error wrapper.
|
3
|
+
#
|
4
|
+
class WeatherSage::HTTP::Error < ::RuntimeError
|
5
|
+
attr :url, :code, :response
|
6
|
+
|
7
|
+
#
|
8
|
+
# Create Error instance from URL, response code, and response.
|
9
|
+
#
|
10
|
+
def initialize(url, code, resp)
|
11
|
+
@url = url.freeze
|
12
|
+
@code = code.freeze
|
13
|
+
@response = resp.freeze
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
#
|
4
|
+
# HTTP fetcher.
|
5
|
+
#
|
6
|
+
class WeatherSage::HTTP::Fetcher
|
7
|
+
#
|
8
|
+
# Request headers.
|
9
|
+
#
|
10
|
+
HEADERS = {
|
11
|
+
'Accept' => 'application/json',
|
12
|
+
'User-Agent' => "weather-sage/#{WeatherSage::VERSION}"
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
#
|
16
|
+
# Create an HTTP Fetcher.
|
17
|
+
#
|
18
|
+
def initialize(log)
|
19
|
+
@log = log
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# Fetch URI, and return response.
|
24
|
+
#
|
25
|
+
# Raises an WeatherSage::HTTP::Error on error.
|
26
|
+
#
|
27
|
+
def fetch(uri, limit = 5)
|
28
|
+
# log uri
|
29
|
+
@log.info('Fetcher#fetch') { '%p' % [uri] }
|
30
|
+
|
31
|
+
# create request, set headers
|
32
|
+
req = Net::HTTP::Get.new(uri)
|
33
|
+
HEADERS.each { |k, v| req[k] = v }
|
34
|
+
use_ssl = (uri.scheme == 'https')
|
35
|
+
|
36
|
+
# connect, fetch response
|
37
|
+
resp = Net::HTTP.start(uri.host, uri.port, use_ssl: use_ssl) do |http|
|
38
|
+
http.request(req)
|
39
|
+
end
|
40
|
+
|
41
|
+
# log response
|
42
|
+
@log.debug('Fetcher#fetch') { 'response: %p' % [resp] }
|
43
|
+
|
44
|
+
# check for error
|
45
|
+
case resp
|
46
|
+
when Net::HTTPSuccess
|
47
|
+
resp
|
48
|
+
when Net::HTTPRedirection
|
49
|
+
# have we hit the redirect limit?
|
50
|
+
unless limit > 0
|
51
|
+
# redirect limit hit, raise error
|
52
|
+
raise ::WeatherSage::HTTP::Error.new(uri.to_s, resp.code, resp)
|
53
|
+
end
|
54
|
+
|
55
|
+
# get new uri
|
56
|
+
new_uri = uri.merge(resp['location'])
|
57
|
+
|
58
|
+
# log redirect
|
59
|
+
@log.debug('Fetcher#fetch') do
|
60
|
+
'redirect: %s' % [JSON.unparse({
|
61
|
+
location: resp['location'],
|
62
|
+
old_uri: uri,
|
63
|
+
new_uri: new_uri,
|
64
|
+
})]
|
65
|
+
end
|
66
|
+
|
67
|
+
# decriment limit, redirect
|
68
|
+
fetch(new_uri, limit - 1)
|
69
|
+
else
|
70
|
+
# log error
|
71
|
+
@log.debug('Fetcher#fetch') do
|
72
|
+
'HTTP request failed: url = %s, response = %p' % [uri, resp]
|
73
|
+
end
|
74
|
+
|
75
|
+
# raise error
|
76
|
+
raise ::WeatherSage::HTTP::Error.new(uri.to_s, resp.code, resp)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
#
|
4
|
+
# HTTP response body parser.
|
5
|
+
#
|
6
|
+
class WeatherSage::HTTP::Parser
|
7
|
+
#
|
8
|
+
# Regex match for known JSON content types.
|
9
|
+
#
|
10
|
+
JSON_CONTENT_TYPE_REGEX =
|
11
|
+
/^application\/json|text\/json|application\/geo\+json/
|
12
|
+
|
13
|
+
#
|
14
|
+
# Create an HTTP response body parser.
|
15
|
+
#
|
16
|
+
def initialize(log)
|
17
|
+
@log = log
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
# Parse HTTP response body.
|
22
|
+
#
|
23
|
+
def parse(resp)
|
24
|
+
# FIXME: need to extract encoding from content-type
|
25
|
+
resp.body.force_encoding('UTF-8')
|
26
|
+
|
27
|
+
r = case resp.content_type
|
28
|
+
when JSON_CONTENT_TYPE_REGEX
|
29
|
+
# parse and return json
|
30
|
+
JSON.parse(resp.body)
|
31
|
+
else
|
32
|
+
# return string
|
33
|
+
resp.body
|
34
|
+
end
|
35
|
+
|
36
|
+
@log.debug('Parser#parse') do
|
37
|
+
JSON.unparse({
|
38
|
+
type: resp.content_type,
|
39
|
+
data: r,
|
40
|
+
})
|
41
|
+
end
|
42
|
+
|
43
|
+
# return response
|
44
|
+
r
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
#
|
2
|
+
# Namespace for HTTP classes.
|
3
|
+
#
|
4
|
+
module WeatherSage::HTTP
|
5
|
+
autoload :Error, File.join(__dir__, 'http', 'error.rb')
|
6
|
+
autoload :Parser, File.join(__dir__, 'http', 'parser.rb')
|
7
|
+
autoload :Fetcher, File.join(__dir__, 'http', 'fetcher.rb')
|
8
|
+
autoload :Cache, File.join(__dir__, 'http', 'cache.rb')
|
9
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Base class for weather API objects.
|
3
|
+
#
|
4
|
+
class WeatherSage::Weather::BaseObject
|
5
|
+
attr :cache
|
6
|
+
|
7
|
+
#
|
8
|
+
# Create a new weather object.
|
9
|
+
#
|
10
|
+
def initialize(ctx)
|
11
|
+
@ctx = ctx
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
#
|
17
|
+
# URL format string for API requests.
|
18
|
+
#
|
19
|
+
API_URL = 'https://api.weather.gov/%s'
|
20
|
+
|
21
|
+
#
|
22
|
+
# Request given API endpoint, return response.
|
23
|
+
#
|
24
|
+
# FIXME: should handle errors too.
|
25
|
+
#
|
26
|
+
def get(path)
|
27
|
+
# build full URL
|
28
|
+
url = API_URL % [path]
|
29
|
+
|
30
|
+
# log full URL
|
31
|
+
@ctx.log.debug('BaseObject#get') { '%s' % [url] }
|
32
|
+
|
33
|
+
# get URL from cache
|
34
|
+
@ctx.cache.get(url)
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Numerical weather forecast.
|
5
|
+
#
|
6
|
+
class WeatherSage::Weather::Forecast
|
7
|
+
attr :data,
|
8
|
+
:updated_at,
|
9
|
+
:units,
|
10
|
+
:generator,
|
11
|
+
:generated_at,
|
12
|
+
:update_time,
|
13
|
+
:valid_times,
|
14
|
+
:elevation,
|
15
|
+
:periods
|
16
|
+
|
17
|
+
#
|
18
|
+
# Create new forecast object from given data.
|
19
|
+
#
|
20
|
+
def initialize(ctx, data)
|
21
|
+
# cache context and data, get properties
|
22
|
+
@ctx, @data = ctx, data.freeze
|
23
|
+
props = @data['properties']
|
24
|
+
|
25
|
+
# log data
|
26
|
+
@ctx.log.debug('Forecast#initialize') do
|
27
|
+
'data = %p' % [@data]
|
28
|
+
end
|
29
|
+
|
30
|
+
@updated_at = Time.parse(props['updated'])
|
31
|
+
@units = props['units']
|
32
|
+
@generator = props['generator']
|
33
|
+
@generated_at = Time.parse(props['generatedAt'])
|
34
|
+
@update_time = Time.parse(props['updateTime'])
|
35
|
+
@valid_times = props['validTimes']
|
36
|
+
@elevation = props['elevation']['value']
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Return an array of periods for this forecast.
|
41
|
+
#
|
42
|
+
def periods
|
43
|
+
@periods ||= @data['properties']['periods'].map { |row|
|
44
|
+
::WeatherSage::Weather::Period.new(@ctx, row)
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#
|
2
|
+
# Weather station observation metadata.
|
3
|
+
#
|
4
|
+
module WeatherSage::Weather::Observation
|
5
|
+
#
|
6
|
+
# Observation property types.
|
7
|
+
#
|
8
|
+
PROPERTIES = {
|
9
|
+
timestamp: :time,
|
10
|
+
textDescription: :text,
|
11
|
+
rawMessage: :text,
|
12
|
+
icon: :url,
|
13
|
+
|
14
|
+
temperature: :value,
|
15
|
+
dewpoint: :value,
|
16
|
+
windDirection: :value,
|
17
|
+
windSpeed: :value,
|
18
|
+
windGust: :value,
|
19
|
+
barometricPressure: :value,
|
20
|
+
seaLevelPressure: :value,
|
21
|
+
visibility: :value,
|
22
|
+
maxTemperatureLast24Hours: :value,
|
23
|
+
minTemperatureLast24Hours: :value,
|
24
|
+
precipitationLastHour: :value,
|
25
|
+
precipitationLast3Hours: :value,
|
26
|
+
precipitationLast6Hours: :value,
|
27
|
+
relativeHumidity: :value,
|
28
|
+
windChill: :value,
|
29
|
+
heatIndex: :value,
|
30
|
+
|
31
|
+
cloudLayers: :cloud,
|
32
|
+
}.freeze
|
33
|
+
end
|