Skip to content

Commit b85e9fa

Browse files
authored
Merge pull request #1 from mgm702/develop
Instant Values, Sites and Statistics added to gem from USGS API
2 parents 469aee0 + 5a4c54f commit b85e9fa

15 files changed

Lines changed: 477 additions & 91 deletions

bin/console

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# frozen_string_literal: true
33

44
require "bundler/setup"
5-
require "usgs/ruby"
5+
require "usgs"
66

77
# You can add fixtures and/or initialization code here to make experimenting
88
# with your gem easier. You can also use a different console, if you like.

lib/usgs/client.rb

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,65 @@
11
# frozen_string_literal: true
2+
require 'pry'
23

34
module Usgs
4-
# Main client class for interacting with USGS Water Services API
5-
#
6-
# @example Basic usage
7-
# Usgs.client.get_sites(state_cd: "CO")
8-
#
9-
# @example With custom timeout
10-
# client = Usgs::Client.new(timeout: 60)
115
class Client
12-
include HTTParty
13-
base_uri Usgs.config.base_url
14-
156
include Site
167
include InstantaneousValues
17-
include DailyValues
8+
include Statistics
189

1910
attr_reader :timeout, :user_agent
2011

21-
# Initialize a new USGS client
22-
#
23-
# @param timeout [Integer] Request timeout in seconds (default: 30)
24-
# @param user_agent [String] Custom User-Agent header
25-
def initialize(timeout: 30, user_agent: "usgs-ruby/#{Usgs::VERSION}")
26-
@timeout = timeout
27-
@user_agent = user_agent
12+
def initialize(timeout: 30, user_agent: "usgs-ruby/#{Usgs::VERSION}", debug: false)
13+
@timeout = timeout
14+
@user_agent = user_agent
15+
@debug = debug
16+
end
17+
18+
# Base URL for USGS Water Services
19+
def base_url
20+
"https://waterservices.usgs.gov/nwis"
2821
end
2922

30-
# Perform a GET request and parse JSON response
31-
#
32-
# @param path [String] API endpoint path
33-
# @param query [Hash] Query parameters
34-
# @return [Hash] Parsed JSON response
35-
def get_json(path, query = {})
23+
# Public: Perform GET and return parsed JSON
24+
def api_get(path, query = {})
3625
query = query.compact
3726
url = "#{base_url}#{path}"
3827

39-
response = fetch_url(url, query: query, timeout: timeout, user_agent: user_agent)
40-
JSON.parse(response.body)
28+
# binding.pry
29+
fetch_url(url, query: query, timeout: timeout, user_agent: user_agent)
4130
end
4231

4332
private
4433

45-
# Low-level fetch with debug support
46-
#
47-
# @private
4834
def fetch_url(url, query: {}, timeout: 30, user_agent: nil)
49-
uri = build_uri(url, query)
35+
uri = URI(url)
36+
uri.query = URI.encode_www_form(query) unless query.empty?
5037

51-
if Usgs.instance_variable_defined?(:@debug) && Usgs.instance_variable_get(:@debug)
52-
puts "\n=== USGS API Request ==="
53-
puts "URL: #{uri}"
54-
puts "=======================\n"
55-
end
38+
puts "\n=== USGS Request ===\n#{uri}\n====================\n" if $DEBUG || ENV["USGS_DEBUG"]
5639

5740
http_get(uri, timeout: timeout, user_agent: user_agent)
5841
end
42+
43+
def http_get(uri, timeout: 30, user_agent: nil)
44+
Net::HTTP.start(
45+
uri.host,
46+
uri.port,
47+
use_ssl: true,
48+
open_timeout: timeout,
49+
read_timeout: timeout
50+
) do |http|
51+
request = Net::HTTP::Get.new(uri)
52+
request["User-Agent"] = user_agent if user_agent
53+
request["Accept"] = "application/json"
54+
55+
response = http.request(request)
56+
57+
if response.is_a?(Net::HTTPSuccess)
58+
response
59+
else
60+
raise "USGS API Error #{response.code}: #{response.message}\n#{response.body}"
61+
end
62+
end
63+
end
5964
end
6065
end

lib/usgs/instantaneous_values.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
module Usgs
4+
module InstantaneousValues
5+
# Fetch instantaneous values (IV) from USGS NWIS
6+
#
7+
# @param sites [String, Array<String>] One or more USGS site IDs
8+
# @param parameter_cd [Symbol, String, Array] e.g. :discharge, "00060", or [:discharge, :gage_height]
9+
# @param start_date [DateTime, Date, Time, String, nil] Start time
10+
# @param end_date [DateTime, Date, Time, String, nil] End time (default: now)
11+
#
12+
# @return [Array<Usgs::Models::Reading>]
13+
#
14+
# @example
15+
# Usgs.client.get_iv(sites: "06754000", parameter_cd: :discharge, start_date: 1.day.ago)
16+
#
17+
def get_iv(sites:, parameter_cd: nil, start_date: nil, end_date: nil)
18+
site_list = Array(sites).join(",")
19+
param_list = resolve_parameter_codes(parameter_cd)
20+
21+
query = {
22+
format: "json",
23+
sites: site_list,
24+
parameterCd: param_list,
25+
# Default to the the last 24hrs if not filled out
26+
startDT: format_datetime(start_date || (Time.now.utc - (24 * 60 * 60))),
27+
endDT: format_datetime(end_date || Time.now.utc)
28+
}.compact
29+
30+
binding.pry
31+
response = api_get("/iv/", query)
32+
Parser.parse_instantaneous_values(JSON.parse(response.body))
33+
end
34+
35+
private
36+
37+
# Convert symbols to official USGS parameter codes
38+
def resolve_parameter_codes(codes)
39+
return nil if codes.nil?
40+
41+
mapping = {
42+
discharge: "00060",
43+
gage_height: "00065",
44+
temperature: "00010",
45+
precipitation: "00045",
46+
do: "00300",
47+
conductivity: "00095",
48+
ph: "00400"
49+
}
50+
51+
Array(codes).map { |c| mapping[c.to_sym] || c.to_s }.join(",")
52+
end
53+
54+
def format_datetime(dt)
55+
return nil unless dt
56+
Time.parse(dt.to_s).utc.strftime("%Y-%m-%dT%H:%M")
57+
end
58+
end
59+
end

lib/usgs/models/reading.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module Usgs
4+
module Models
5+
class Reading
6+
ATTRIBUTES = %i[
7+
site_no
8+
parameter_cd
9+
datetime
10+
value
11+
qualifiers
12+
unit
13+
metadata
14+
].freeze
15+
16+
attr_accessor(*ATTRIBUTES)
17+
18+
def initialize(data = {})
19+
data[:metadata] ||= {}
20+
attrs = data.is_a?(Hash) ? data : {}
21+
22+
ATTRIBUTES.each do |attr|
23+
instance_variable_set(:"@#{attr}", attrs[attr]) if attrs.key?(attr)
24+
end
25+
end
26+
end
27+
end
28+
end

lib/usgs/models/site.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
module Usgs
4+
module Models
5+
class Site
6+
ATTRIBUTES = %i[
7+
agency_cd
8+
site_no
9+
station_nm
10+
site_tp_cd
11+
dec_lat_va
12+
dec_long_va
13+
coord_acy_cd
14+
dec_coord_datum_cd
15+
alt_va
16+
alt_acy_va
17+
alt_datum_cd
18+
huc_cd
19+
metadata
20+
].freeze
21+
22+
attr_accessor(*ATTRIBUTES)
23+
24+
def initialize(attrs = {})
25+
# Accept either a single hash OR keyword arguments
26+
attrs = attrs.is_a?(Hash) ? attrs : {}
27+
attrs[:metadata] ||= {}
28+
29+
ATTRIBUTES.each do |attr|
30+
instance_variable_set(:"@#{attr}", attrs[attr]) if attrs.key?(attr)
31+
end
32+
end
33+
end
34+
end
35+
end

lib/usgs/models/statistic.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# frozen_string_literal: true
2+
3+
module Usgs
4+
module Models
5+
class Statistic
6+
ATTRIBUTES = %i[
7+
site_no
8+
parameter_cd
9+
month_nu
10+
day_nu
11+
begin_yr
12+
end_yr
13+
count_nu
14+
mean_va
15+
max_va
16+
max_va_yr
17+
min_va
18+
min_va_yr
19+
p05_va
20+
p10_va
21+
p20_va
22+
p25_va
23+
p50_va
24+
p75_va
25+
p80_va
26+
p90_va
27+
p95_va
28+
metadata
29+
].freeze
30+
31+
attr_accessor(*ATTRIBUTES)
32+
33+
def initialize(data = {})
34+
data[:metadata] ||= {}
35+
attrs = data.is_a?(Hash) ? data : {}
36+
37+
ATTRIBUTES.each do |attr|
38+
value = attrs[attr]
39+
40+
# Convert all the _va fields (values) from string to float
41+
if value.is_a?(String) && attr.to_s.end_with?("_va", "_yr", "_nu")
42+
stripped = value.strip
43+
value = stripped.empty? || stripped == "." ? nil : stripped.to_f
44+
end
45+
46+
instance_variable_set(:"@#{attr}", value) if attrs.key?(attr)
47+
end
48+
end
49+
end
50+
end
51+
end

lib/usgs/parser.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@ module Usgs
44
class Parser
55
class << self
66
def parse_sites(response)
7-
Parsers::SiteParser.parse_sites(response)
7+
Parsers::RdbParser.parse(response)
88
end
99

1010
def parse_time_series(response, timescale: :instantaneous)
1111
# Parsers::TimeSeriesParser.parse(response, timescale: timescale)
1212
end
13+
14+
def parse_instantaneous_values(response)
15+
Parsers::InstantaneousValuesParser.parse(response)
16+
end
17+
18+
def parse_statistics(response)
19+
Parsers::StatisticsParser.parse(response)
20+
end
1321
end
1422
end
1523
end

lib/usgs/parsers.rb

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
module Usgs
4+
module Parsers
5+
module InstantaneousValuesParser
6+
class << self
7+
def parse(response)
8+
series_list = response.dig("value", "timeSeries") || []
9+
series_list.flat_map { |series| parse_series(series) }
10+
end
11+
12+
private
13+
14+
def parse_series(series)
15+
info = series["sourceInfo"]
16+
variable = series["variable"]
17+
values = series["values"]&.first&.dig("value") || []
18+
19+
site_no = info.dig("siteCode", 0, "value")
20+
parameter_cd = variable.dig("variableCode", 0, "value")
21+
unit = variable.dig("unit", "unitCode")
22+
23+
values.map do |v|
24+
raw_value = v["value"]
25+
26+
value = case raw_value
27+
when nil, "", "-999999", "Ice", "Eqp", "Fld"
28+
nil
29+
else
30+
raw_value.to_f
31+
end
32+
33+
{
34+
site_no: site_no,
35+
parameter_cd: parameter_cd,
36+
datetime: v["dateTime"],
37+
value: value,
38+
qualifiers: v["qualifiers"],
39+
unit: unit,
40+
metadata: {}
41+
}
42+
end
43+
end
44+
end
45+
end
46+
end
47+
end

0 commit comments

Comments
 (0)