gull 0.4.0 → 1.0.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 +5 -5
- data/.github/workflows/ci.yml +23 -0
- data/.gitignore +5 -21
- data/.rubocop.yml +34 -1
- data/CHANGELOG.md +9 -1
- data/Gemfile +2 -0
- data/Guardfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +41 -25
- data/Rakefile +2 -0
- data/gull.gemspec +19 -11
- data/lib/gull/alert.rb +79 -45
- data/lib/gull/client.rb +51 -36
- data/lib/gull/error.rb +5 -0
- data/lib/gull/geocode.rb +5 -0
- data/lib/gull/polygon.rb +11 -27
- data/lib/gull/version.rb +3 -1
- data/lib/gull.rb +10 -3
- data/spec/alert_spec.rb +130 -72
- data/spec/client_spec.rb +46 -63
- data/spec/error_spec.rb +4 -2
- data/spec/fixtures/alerts.json +4881 -0
- data/spec/fixtures/empty.json +4 -0
- data/spec/fixtures/features/blizzard_warning.json +93 -0
- data/spec/fixtures/features/empty_geocode.json +34 -0
- data/spec/fixtures/features/flood_advisory.json +156 -0
- data/spec/fixtures/features/flood_warning.json +108 -0
- data/spec/fixtures/features/missing_times.json +36 -0
- data/spec/fixtures/features/multipolygon.json +69 -0
- data/spec/fixtures/features/null_geometry.json +145 -0
- data/spec/fixtures/features/polygon_no_vtec.json +165 -0
- data/spec/fixtures/features/polygon_with_vtec.json +128 -0
- data/spec/fixtures/missing_event.json +21 -0
- data/spec/polygon_spec.rb +22 -34
- data/spec/spec_helper.rb +5 -88
- metadata +42 -42
- data/.hound.yml +0 -3
- data/.ruby-version +0 -1
- data/.travis.yml +0 -7
- data/spec/fixtures/alerts.xml +0 -118
- data/spec/fixtures/bad.xml +0 -1
- data/spec/fixtures/empty.xml +0 -30
- data/spec/fixtures/missing_cap.xml +0 -46
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7924e8188cee18f8b12e1a420d6ea68d1e0951c1c7c7e53f9d87b11b528d23f0
|
|
4
|
+
data.tar.gz: 517bb2c4a28d250207ddd22c51173b2be3392e0f1059b89110cc2af11a48af65
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 79015c143ade595b2d021986b48c0e7dbe78c83f2f42b787e9b7027914263631b3a6581a7491c607cef8037240465e056d8200f4eb9770016a9e88c83b022b71
|
|
7
|
+
data.tar.gz: 61d48682d8434559a7b14347765b3537cfc32c78f7c23e5ff137dcfe7d5571e3e98bbd8a8c79be198959351e3bd8913da7e0c02f82ad8310bf7a33ad7784d974
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [master]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [master]
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
strategy:
|
|
11
|
+
matrix:
|
|
12
|
+
ruby-version: ['3.1', '3.2', '3.3', '3.4']
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
16
|
+
uses: ruby/setup-ruby@v1
|
|
17
|
+
with:
|
|
18
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
19
|
+
bundler-cache: true
|
|
20
|
+
- name: Run tests
|
|
21
|
+
run: bundle exec rspec
|
|
22
|
+
- name: Run linter
|
|
23
|
+
run: bundle exec rubocop
|
data/.gitignore
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.yardoc
|
|
1
|
+
/.bundle
|
|
2
|
+
/coverage
|
|
3
|
+
/pkg
|
|
4
|
+
/tmp
|
|
6
5
|
Gemfile.lock
|
|
7
|
-
|
|
8
|
-
_yardoc
|
|
9
|
-
coverage
|
|
10
|
-
doc/
|
|
11
|
-
lib/bundler/man
|
|
12
|
-
pkg
|
|
13
|
-
rdoc
|
|
14
|
-
spec/reports
|
|
15
|
-
test/tmp
|
|
16
|
-
test/version_tmp
|
|
17
|
-
tmp
|
|
18
|
-
*.bundle
|
|
19
|
-
*.so
|
|
20
|
-
*.o
|
|
21
|
-
*.a
|
|
22
|
-
mkmf.log
|
|
6
|
+
.ruby-version
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
2
5
|
Exclude:
|
|
3
6
|
- 'spec/spec_helper.rb'
|
|
7
|
+
- 'gull.gemspec'
|
|
8
|
+
- 'vendor/**/*'
|
|
9
|
+
Metrics/BlockLength:
|
|
10
|
+
Exclude:
|
|
11
|
+
- 'spec/**/*'
|
|
12
|
+
Metrics/MethodLength:
|
|
13
|
+
Max: 20
|
|
14
|
+
Exclude:
|
|
15
|
+
- 'spec/**/*'
|
|
16
|
+
Metrics/AbcSize:
|
|
17
|
+
Max: 35
|
|
18
|
+
Exclude:
|
|
19
|
+
- 'spec/**/*'
|
|
20
|
+
Metrics/ClassLength:
|
|
21
|
+
Enabled: false
|
|
22
|
+
Metrics/CyclomaticComplexity:
|
|
23
|
+
Enabled: false
|
|
24
|
+
Metrics/PerceivedComplexity:
|
|
25
|
+
Enabled: false
|
|
26
|
+
Layout/LineLength:
|
|
27
|
+
Max: 80
|
|
28
|
+
Exclude:
|
|
29
|
+
- 'spec/**/*'
|
|
4
30
|
Style/StringLiterals:
|
|
5
|
-
EnforcedStyle: single_quotes
|
|
31
|
+
EnforcedStyle: single_quotes
|
|
32
|
+
Style/StringConcatenation:
|
|
33
|
+
Exclude:
|
|
34
|
+
- 'spec/**/*'
|
|
35
|
+
Style/Documentation:
|
|
36
|
+
Enabled: false
|
|
37
|
+
Naming/PredicateMethod:
|
|
38
|
+
Enabled: false
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
1.0.1 (03/07/2026) - Add YARD doc comments to all public classes and methods. Expand README with polygon, error handling, and client usage examples. Handle malformed JSON responses (raise `HttpError` instead of `JSON::ParserError`). Handle missing time fields in alert data (return `nil` instead of raising `TypeError`).
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
1.0.0 (03/07/2026) - Major rewrite: migrate from defunct `alerts.weather.gov` XML feed to `api.weather.gov/alerts` JSON API. Drop all runtime dependencies (`httpclient`, `nokogiri`) in favor of Ruby stdlib (`net/http`, `json`). Require Ruby >= 3.1. Add `area` option for state-based filtering. Replace Travis CI with GitHub Actions. Add RuboCop. **Breaking:** remove `url` option (use `area` instead), remove `Polygon#image_url`, remove `strict` option.
|
|
6
|
+
|
|
7
|
+
***
|
|
8
|
+
|
|
1
9
|
0.4.0 (07/26/2016) - Merged pull request #2 from [schrockwell](https://github.com/schrockwell), which adds `to_wkt` to `Polygon` which formats the polygon points as Well-Known Text (WKT). Removed `centroid` method, use new `to_wkt` method with an external geospatial library instead.
|
|
2
10
|
|
|
3
11
|
***
|
|
@@ -43,7 +51,7 @@
|
|
|
43
51
|
***
|
|
44
52
|
|
|
45
53
|
0.2.0 (10/02/2014) - Introduced Polygon type.
|
|
46
|
-
|
|
54
|
+
|
|
47
55
|
***
|
|
48
56
|
|
|
49
57
|
0.1.1 (10/01/2014) - Refactored and simplified alert processing.
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
[](http://badge.fury.io/rb/gull)
|
|
2
|
-
[](https://coveralls.io/r/sethdeckard/gull)
|
|
4
|
-
[](https://codeclimate.com/github/sethdeckard/gull)
|
|
5
|
-
[](https://gemnasium.com/sethdeckard/gull)
|
|
6
|
-
[](https://hakiri.io/github/sethdeckard/gull/master)
|
|
2
|
+
[](https://github.com/sethdeckard/gull/actions/workflows/ci.yml)
|
|
7
3
|
# Gull
|
|
8
4
|
|
|
9
|
-
Ruby client for parsing NOAA/NWS alerts, warnings, and watches. The name comes from the type of bird featured on the NOAA logo.
|
|
5
|
+
Ruby client for parsing NOAA/NWS alerts, warnings, and watches. The name comes from the type of bird featured on the NOAA logo. Zero runtime dependencies -- uses only Ruby stdlib (`net/http`, `json`).
|
|
10
6
|
|
|
11
7
|
## Installation
|
|
12
8
|
|
|
@@ -40,7 +36,7 @@ alert.effective_at
|
|
|
40
36
|
alert.expires_at
|
|
41
37
|
alert.published_at
|
|
42
38
|
alert.area
|
|
43
|
-
alert.polygon
|
|
39
|
+
alert.polygon # Gull::Polygon or nil
|
|
44
40
|
alert.geocode.fips6
|
|
45
41
|
alert.geocode.ugc
|
|
46
42
|
alert.urgency
|
|
@@ -49,41 +45,61 @@ alert.certainty
|
|
|
49
45
|
alert.vtec
|
|
50
46
|
```
|
|
51
47
|
|
|
52
|
-
To get alerts for a single state
|
|
48
|
+
To get alerts for a single state or territory, pass the area option:
|
|
53
49
|
|
|
54
50
|
```ruby
|
|
55
|
-
|
|
56
|
-
alerts = Gull::Alert.fetch(url: oklahoma_url)
|
|
51
|
+
alerts = Gull::Alert.fetch(area: 'OK')
|
|
57
52
|
```
|
|
58
53
|
|
|
59
|
-
|
|
54
|
+
### Polygons
|
|
55
|
+
|
|
56
|
+
Alerts with geographic boundaries include a `Polygon` object:
|
|
60
57
|
|
|
61
58
|
```ruby
|
|
62
|
-
alert.polygon.
|
|
59
|
+
alert.polygon.coordinates
|
|
60
|
+
# => [[34.57, -97.56], [34.77, -97.38], ...]
|
|
61
|
+
|
|
62
|
+
alert.polygon.to_s
|
|
63
|
+
# => "34.57,-97.56 34.77,-97.38 ..."
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
alert.polygon.to_wkt
|
|
66
|
+
# => "POLYGON((-97.56 34.57, -97.38 34.77, ...))"
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
### Error Handling
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
begin
|
|
73
|
+
alerts = Gull::Alert.fetch
|
|
74
|
+
rescue Gull::TimeoutError => e
|
|
75
|
+
# request timed out
|
|
76
|
+
rescue Gull::HttpError => e
|
|
77
|
+
# non-success response or connection failure
|
|
78
|
+
e.original # wrapped exception, if any
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Advanced: Client
|
|
83
|
+
|
|
84
|
+
For direct access to unparseable features, use `Client`:
|
|
68
85
|
|
|
69
86
|
```ruby
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
client = Gull::Client.new(area: 'OK')
|
|
88
|
+
alerts = client.fetch
|
|
89
|
+
client.errors # features that could not be parsed
|
|
73
90
|
```
|
|
74
91
|
|
|
75
|
-
##Notes, Caveats
|
|
76
|
-
This library provides a simplified/flattened model of the [Common Alerting Protocol](http://docs.oasis-open.org/emergency/cap/v1.2/CAP-v1.2-os.html) based only on the elements NWS utilizes in their [public Atom feeds](http://alerts.weather.gov/). If you need a complete CAP parser I suggest looking at [RCAP](https://github.com/farrel/RCAP).
|
|
92
|
+
## Notes, Caveats
|
|
77
93
|
|
|
78
|
-
|
|
94
|
+
This library fetches active alerts from the [NWS API](https://api.weather.gov) (`api.weather.gov/alerts/active`), which returns GeoJSON. No authentication is required but the API does require a `User-Agent` header (set automatically by the gem). See the [NWS API docs](https://www.weather.gov/documentation/services-web-api) for more details.
|
|
79
95
|
|
|
80
|
-
The
|
|
96
|
+
The NWS will often cancel or update alerts before their expiration time. The API only returns currently active alerts.
|
|
81
97
|
|
|
82
98
|
### Urgency
|
|
83
99
|
|
|
84
|
-
| Symbol | Definition
|
|
100
|
+
| Symbol | Definition
|
|
85
101
|
| :------------- |:-------------
|
|
86
|
-
| :immediate | Responsive action should
|
|
102
|
+
| :immediate | Responsive action should be taken immediately
|
|
87
103
|
| :expected | Responsive action should be taken soon (within next hour)
|
|
88
104
|
| :future | Responsive action should be taken in the near future
|
|
89
105
|
| :past | Responsive action is no longer required
|
|
@@ -91,7 +107,7 @@ The public Atom feeds have not always been reliable in terms of uptime and are o
|
|
|
91
107
|
|
|
92
108
|
### Severity
|
|
93
109
|
|
|
94
|
-
| Symbol | Definition
|
|
110
|
+
| Symbol | Definition
|
|
95
111
|
| :------------- |:-------------
|
|
96
112
|
| :extreme | Extraordinary threat to life or property
|
|
97
113
|
| :severe | Significant threat to life or property
|
|
@@ -101,7 +117,7 @@ The public Atom feeds have not always been reliable in terms of uptime and are o
|
|
|
101
117
|
|
|
102
118
|
### Certainty
|
|
103
119
|
|
|
104
|
-
| Symbol | Definition
|
|
120
|
+
| Symbol | Definition
|
|
105
121
|
| :------------- |:-------------
|
|
106
122
|
| :very_likely | Highly likely (p > ~ 85%) or certain
|
|
107
123
|
| :likely | Likely (p > ~50%)
|
data/Rakefile
CHANGED
data/gull.gemspec
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
5
|
require 'gull/version'
|
|
5
6
|
|
|
@@ -9,22 +10,29 @@ Gem::Specification.new do |spec|
|
|
|
9
10
|
spec.authors = ['Seth Deckard']
|
|
10
11
|
spec.email = ['seth@deckard.me']
|
|
11
12
|
spec.summary = 'Client for parsing NOAA/NWS alerts, warnings, and watches.'
|
|
12
|
-
spec.description = '
|
|
13
|
+
spec.description = 'Fetches and parses NOAA/NWS alerts, warnings, and watches from api.weather.gov. Zero runtime dependencies.'
|
|
13
14
|
spec.homepage = 'https://github.com/sethdeckard/gull'
|
|
14
15
|
spec.license = 'MIT'
|
|
16
|
+
spec.required_ruby_version = '>= 3.1'
|
|
17
|
+
|
|
18
|
+
spec.metadata = {
|
|
19
|
+
'source_code_uri' => 'https://github.com/sethdeckard/gull',
|
|
20
|
+
'changelog_uri' => 'https://github.com/sethdeckard/gull/blob/master/CHANGELOG.md',
|
|
21
|
+
'bug_tracker_uri' => 'https://github.com/sethdeckard/gull/issues'
|
|
22
|
+
}
|
|
15
23
|
|
|
16
|
-
spec.files
|
|
24
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
f.match(%r{^(AGENTS|CLAUDE)\.md$})
|
|
26
|
+
end
|
|
17
27
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
|
-
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
|
19
28
|
spec.require_paths = ['lib']
|
|
20
29
|
|
|
21
|
-
spec.add_runtime_dependency 'httpclient'
|
|
22
|
-
spec.add_runtime_dependency 'nokogiri', '>= 1.6.8'
|
|
23
|
-
|
|
24
30
|
spec.add_development_dependency 'bundler'
|
|
31
|
+
spec.add_development_dependency 'guard-rspec'
|
|
25
32
|
spec.add_development_dependency 'rake'
|
|
26
|
-
spec.add_development_dependency 'rspec', '>=3.0'
|
|
27
|
-
spec.add_development_dependency '
|
|
28
|
-
spec.add_development_dependency '
|
|
33
|
+
spec.add_development_dependency 'rspec', '>= 3.0'
|
|
34
|
+
spec.add_development_dependency 'rubocop'
|
|
35
|
+
spec.add_development_dependency 'rubocop-rake'
|
|
36
|
+
spec.add_development_dependency 'simplecov'
|
|
29
37
|
spec.add_development_dependency 'webmock'
|
|
30
38
|
end
|
data/lib/gull/alert.rb
CHANGED
|
@@ -1,80 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
require 'nokogiri'
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module Gull
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Represents a single NWS weather alert (warning, watch, or
|
|
5
|
+
# advisory). Use +Alert.fetch+ to retrieve active alerts from the
|
|
6
|
+
# NWS API.
|
|
7
7
|
class Alert
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# @!attribute id [rw] NWS alert identifier
|
|
9
|
+
# @!attribute title [rw] Alert headline
|
|
10
|
+
# @!attribute summary [rw] Full alert description text
|
|
11
|
+
# @!attribute link [rw] Canonical URL for this alert
|
|
12
|
+
# @!attribute alert_type [rw] Event type (e.g. "Tornado Warning")
|
|
13
|
+
# @!attribute polygon [rw] Alert area as a Polygon, or nil
|
|
14
|
+
# @!attribute area [rw] Human-readable area description
|
|
15
|
+
# @!attribute effective_at [rw] Time the alert takes effect
|
|
16
|
+
# @!attribute expires_at [rw] Time the alert expires
|
|
17
|
+
# @!attribute updated_at [rw] Onset time, or sent time if absent
|
|
18
|
+
# @!attribute published_at [rw] Time the alert was sent
|
|
19
|
+
# @!attribute urgency [rw] Urgency level as a Symbol
|
|
20
|
+
# @!attribute severity [rw] Severity level as a Symbol
|
|
21
|
+
# @!attribute certainty [rw] Certainty level as a Symbol
|
|
22
|
+
# @!attribute geocode [rw] Geocode with UGC and FIPS codes
|
|
23
|
+
# @!attribute vtec [rw] VTEC string, or nil
|
|
24
|
+
attr_accessor :id, :title, :summary, :link, :alert_type, :polygon,
|
|
25
|
+
:area, :effective_at, :expires_at, :updated_at,
|
|
26
|
+
:published_at, :urgency, :severity, :certainty,
|
|
27
|
+
:geocode, :vtec
|
|
11
28
|
|
|
12
29
|
def initialize
|
|
13
30
|
self.geocode = Geocode.new
|
|
14
31
|
end
|
|
15
32
|
|
|
33
|
+
# Fetches active alerts from the NWS API.
|
|
16
34
|
def self.fetch(options = {})
|
|
17
|
-
client = Client.new
|
|
35
|
+
client = Client.new(options)
|
|
18
36
|
client.fetch
|
|
19
37
|
end
|
|
20
38
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
# Populates this alert from a GeoJSON feature hash.
|
|
40
|
+
def parse(feature)
|
|
41
|
+
props = feature['properties']
|
|
42
|
+
parse_core_attributes(feature, props)
|
|
43
|
+
parse_times(props)
|
|
44
|
+
parse_categories(props)
|
|
45
|
+
parse_polygon(feature)
|
|
46
|
+
parse_geocode(props)
|
|
47
|
+
parse_vtec(props)
|
|
29
48
|
end
|
|
30
49
|
|
|
31
50
|
private
|
|
32
51
|
|
|
33
|
-
def parse_core_attributes(
|
|
34
|
-
self.id =
|
|
35
|
-
self.title =
|
|
36
|
-
self.summary =
|
|
37
|
-
self.link =
|
|
38
|
-
self.alert_type =
|
|
39
|
-
self.area =
|
|
52
|
+
def parse_core_attributes(feature, props)
|
|
53
|
+
self.id = props['id']
|
|
54
|
+
self.title = props['headline']
|
|
55
|
+
self.summary = props['description']
|
|
56
|
+
self.link = props['@id'] || feature['id']
|
|
57
|
+
self.alert_type = props['event']
|
|
58
|
+
self.area = props['areaDesc']
|
|
40
59
|
end
|
|
41
60
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
def parse_times(props)
|
|
62
|
+
self.effective_at = parse_time(props['effective'])
|
|
63
|
+
self.expires_at = parse_time(props['expires'])
|
|
64
|
+
self.published_at = parse_time(props['sent'])
|
|
65
|
+
self.updated_at = parse_time(props['onset'] || props['sent'])
|
|
45
66
|
end
|
|
46
67
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
self.published_at = Time.parse(element.css('published').inner_text)
|
|
50
|
-
self.effective_at = Time.parse(element.xpath('cap:effective').inner_text)
|
|
51
|
-
self.expires_at = Time.parse(element.xpath('cap:expires').inner_text)
|
|
68
|
+
def parse_time(value)
|
|
69
|
+
value ? Time.parse(value) : nil
|
|
52
70
|
end
|
|
53
71
|
|
|
54
|
-
def parse_categories(
|
|
55
|
-
self.urgency = code_to_symbol
|
|
56
|
-
self.severity = code_to_symbol
|
|
57
|
-
self.certainty = code_to_symbol
|
|
72
|
+
def parse_categories(props)
|
|
73
|
+
self.urgency = code_to_symbol(props['urgency'])
|
|
74
|
+
self.severity = code_to_symbol(props['severity'])
|
|
75
|
+
self.certainty = code_to_symbol(props['certainty'])
|
|
58
76
|
end
|
|
59
77
|
|
|
60
|
-
def parse_polygon(
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
def parse_polygon(feature)
|
|
79
|
+
geometry = feature['geometry']
|
|
80
|
+
return if geometry.nil?
|
|
81
|
+
|
|
82
|
+
coords = geometry['coordinates']
|
|
83
|
+
return if coords.nil? || coords.empty?
|
|
84
|
+
|
|
85
|
+
ring = coords.first
|
|
86
|
+
ring = ring.first if geometry['type'] == 'MultiPolygon'
|
|
87
|
+
self.polygon = Polygon.new(ring)
|
|
63
88
|
end
|
|
64
89
|
|
|
65
|
-
def parse_geocode(
|
|
66
|
-
|
|
90
|
+
def parse_geocode(props)
|
|
91
|
+
geocode_data = props['geocode']
|
|
92
|
+
return if geocode_data.nil?
|
|
67
93
|
|
|
68
|
-
|
|
69
|
-
geocode.ugc =
|
|
94
|
+
ugc = geocode_data['UGC']
|
|
95
|
+
geocode.ugc = ugc&.join(' ')
|
|
96
|
+
|
|
97
|
+
fips = geocode_data['SAME']
|
|
98
|
+
geocode.fips6 = fips&.join(' ')
|
|
70
99
|
end
|
|
71
100
|
|
|
72
|
-
def parse_vtec(
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
def parse_vtec(props)
|
|
102
|
+
params = props['parameters']
|
|
103
|
+
return if params.nil?
|
|
104
|
+
|
|
105
|
+
vtec_values = params['VTEC']
|
|
106
|
+
self.vtec = vtec_values&.first
|
|
75
107
|
end
|
|
76
108
|
|
|
77
109
|
def code_to_symbol(code)
|
|
110
|
+
return :unknown if code.nil?
|
|
111
|
+
|
|
78
112
|
code.tr(' ', '_').downcase.to_sym
|
|
79
113
|
end
|
|
80
114
|
end
|
data/lib/gull/client.rb
CHANGED
|
@@ -1,65 +1,80 @@
|
|
|
1
|
-
|
|
2
|
-
require 'nokogiri'
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module Gull
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Low-level HTTP client for the NWS alerts API. Handles
|
|
5
|
+
# fetching, parsing, and error wrapping. Most callers should
|
|
6
|
+
# use +Alert.fetch+ instead.
|
|
7
7
|
class Client
|
|
8
|
+
URL = 'https://api.weather.gov/alerts/active'
|
|
9
|
+
USER_AGENT = "gull/#{VERSION} (Ruby #{RUBY_VERSION})".freeze
|
|
10
|
+
|
|
11
|
+
# Features that could not be parsed are collected here.
|
|
8
12
|
attr_accessor :errors
|
|
9
13
|
|
|
10
14
|
def initialize(options = {})
|
|
11
|
-
@options =
|
|
12
|
-
url: 'http://alerts.weather.gov/cap/us.php?x=1',
|
|
13
|
-
strict: false
|
|
14
|
-
}.merge options
|
|
15
|
+
@options = options
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
# Fetches active alerts and returns an Array of Alert objects.
|
|
17
19
|
def fetch
|
|
18
20
|
self.errors = []
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
json = response
|
|
22
|
+
data = JSON.parse(json)
|
|
23
|
+
process(data['features'] || [])
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
raise HttpError, 'Unexpected response from NWS API'
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
private
|
|
27
29
|
|
|
28
30
|
def response
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
uri = build_uri
|
|
32
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
33
|
+
http.use_ssl = uri.scheme == 'https'
|
|
34
|
+
request = Net::HTTP::Get.new(uri)
|
|
35
|
+
request['User-Agent'] = USER_AGENT
|
|
36
|
+
request['Accept'] = 'application/geo+json'
|
|
37
|
+
result = http.request(request)
|
|
38
|
+
unless result.is_a?(Net::HTTPSuccess)
|
|
39
|
+
raise HttpError, "NWS API returned #{result.code}"
|
|
37
40
|
end
|
|
41
|
+
|
|
42
|
+
result.body
|
|
43
|
+
rescue Net::OpenTimeout, Net::ReadTimeout
|
|
44
|
+
raise TimeoutError,
|
|
45
|
+
'Timeout while connecting to NWS web service'
|
|
46
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET
|
|
47
|
+
raise HttpError,
|
|
48
|
+
'Could not connect to NWS web service'
|
|
38
49
|
end
|
|
39
50
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
def build_uri
|
|
52
|
+
uri = URI(URL)
|
|
53
|
+
if @options[:area]
|
|
54
|
+
params = URI.decode_www_form(uri.query || '')
|
|
55
|
+
params << ['area', @options[:area]]
|
|
56
|
+
uri.query = URI.encode_www_form(params)
|
|
46
57
|
end
|
|
58
|
+
uri
|
|
59
|
+
end
|
|
47
60
|
|
|
61
|
+
def process(features)
|
|
62
|
+
alerts = []
|
|
63
|
+
features.each do |feature|
|
|
64
|
+
alert = create_instance(feature)
|
|
65
|
+
alerts.push(alert) unless alert.nil?
|
|
66
|
+
errors.push(feature) if alert.nil?
|
|
67
|
+
end
|
|
48
68
|
alerts
|
|
49
69
|
end
|
|
50
70
|
|
|
51
|
-
def create_instance(
|
|
52
|
-
|
|
71
|
+
def create_instance(feature)
|
|
72
|
+
properties = feature['properties']
|
|
73
|
+
return if properties.nil? || properties['event'].nil?
|
|
53
74
|
|
|
54
75
|
alert = Alert.new
|
|
55
|
-
alert.parse
|
|
76
|
+
alert.parse(feature)
|
|
56
77
|
alert
|
|
57
78
|
end
|
|
58
|
-
|
|
59
|
-
def namespaces
|
|
60
|
-
{ 'xmlns' => 'http://www.w3.org/2005/Atom',
|
|
61
|
-
'cap' => 'urn:oasis:names:tc:emergency:cap:1.1',
|
|
62
|
-
'ha' => 'http://www.alerting.net/namespace/index_1.0' }
|
|
63
|
-
end
|
|
64
79
|
end
|
|
65
80
|
end
|
data/lib/gull/error.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'English'
|
|
2
4
|
|
|
3
5
|
module Gull
|
|
6
|
+
# Raised when the NWS API returns a non-success response or the
|
|
7
|
+
# connection fails. Wraps the original exception, if any.
|
|
4
8
|
class HttpError < StandardError
|
|
5
9
|
attr_reader :original
|
|
6
10
|
|
|
@@ -10,6 +14,7 @@ module Gull
|
|
|
10
14
|
end
|
|
11
15
|
end
|
|
12
16
|
|
|
17
|
+
# Raised when the NWS API request times out.
|
|
13
18
|
class TimeoutError < HttpError
|
|
14
19
|
end
|
|
15
20
|
end
|
data/lib/gull/geocode.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Gull
|
|
4
|
+
# Holds UGC zone codes and FIPS county codes for an alert area.
|
|
2
5
|
class Geocode
|
|
6
|
+
# @!attribute fips6 [rw] Space-separated FIPS 6 codes
|
|
7
|
+
# @!attribute ugc [rw] Space-separated UGC zone codes
|
|
3
8
|
attr_accessor :fips6, :ugc
|
|
4
9
|
end
|
|
5
10
|
end
|