wasserstand 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in wasserstand.gemspec
4
4
  gemspec
5
+
6
+ # Put dalli into its own group so not everyone has to require it
7
+ gem 'dalli', :group => :dalli
data/TODO CHANGED
@@ -1,3 +1,2 @@
1
- * Tests for the command line client
2
1
  * CSV output for the command line client
3
2
  * JSON output for the command line client
@@ -6,12 +6,27 @@ Bundler.require
6
6
  require 'wasserstand'
7
7
  require 'optparse'
8
8
 
9
- OptionParser.new do |opts|
9
+ options = {}
10
+
11
+ params = OptionParser.new do |opts|
10
12
  opts.banner = <<HERE
11
- Wasserstand provides water level information as provided by PegelOnline.
13
+ Shows water level information as provided by PegelOnline.
12
14
 
13
15
  Usage:
14
- #{File.basename($0)} [options] [WATERWAY | LEVEL]
16
+ #{File.basename($0)} [options] [WATERWAY | LEVEL | WATERWAY LEVEL]
17
+
18
+ Examples:
19
+ # all waterways
20
+ #{File.basename($0)}
21
+
22
+ # single waterway
23
+ #{File.basename($0)} Rhein
24
+
25
+ # single level
26
+ #{File.basename($0)} Koblenz
27
+
28
+ # single level that happens to have the same name as a waterway
29
+ #{File.basename($0)} Ilm Ilmenau
15
30
 
16
31
  Author:
17
32
  Nicolas E. Rabenau nerab@gmx.at
@@ -33,34 +48,25 @@ HERE
33
48
  end
34
49
 
35
50
  opts.on("-u", "--url URL", "Use URL to fetch levels") do |url|
36
- Wasserstand.logger.info("Using URL #{url}")
37
- Wasserstand.provider = Wasserstand::Provider::PegelOnline.new(url)
51
+ options[:url] = url
38
52
  end
39
- end.parse!
40
53
 
41
- query = ARGV.shift
42
-
43
- if query.nil?
44
- Wasserstand::Waterway.all.each{|ww| puts "#{ww.name} (#{ww.levels.size} levels)"}
45
- else
46
- begin
47
- waterway = Wasserstand::Waterway[query]
48
-
49
- if waterway
50
- puts "#{waterway.name} has #{waterway.levels.size} levels:"
51
- waterway.levels.sort_by{|name, level| level.km}.each{|name, level| puts "#{name} (km #{level.km}): #{level.measurements.last}"}
52
- else
53
- level = Wasserstand::Level[query]
54
-
55
- if level.nil?
56
- STDERR.puts "#{File.basename($0)}: No waterway nor level found that matches '#{query}."
57
- else
58
- puts "Level #{level.name} (#{level.waterway}, km #{level.km}):"
59
- level.measurements.each{|measurement| puts measurement}
60
- end
61
- end
62
- rescue
63
- STDERR.puts $!
64
- $!.backtrace.each{|msg| Wasserstand.logger.debug(msg)}
54
+ opts.on("-c", "--cache [URL]", "Use memcached at URL instead of heap. URL defaults to 127.0.0.1:11211.") do |url|
55
+ options[:cache] = url || '127.0.0.1:11211'
65
56
  end
57
+ end.parse!
58
+
59
+ Wasserstand.provider = Wasserstand::Provider::PegelOnline.new(options[:url])
60
+
61
+ if options[:cache]
62
+ Bundler.require(:default, :dalli)
63
+ Wasserstand.logger.info("Using memcached at #{options[:cache]}")
64
+ Wasserstand.provider.cache = Dalli::Client.new(options[:cache], :expires_in => 60 * 60)
65
+ end
66
+
67
+ begin
68
+ Wasserstand::Commandline.get(params)
69
+ rescue
70
+ STDERR.puts $!
71
+ $!.backtrace.each{|msg| Wasserstand.logger.debug(msg)}
66
72
  end
@@ -13,10 +13,16 @@ module Wasserstand
13
13
  AmbigousNameError = Class.new(StandardError)
14
14
 
15
15
  class << self
16
- attr_writer :provider
17
-
18
16
  def provider
19
- @provider ||= Provider::PegelOnline.new
17
+ if @provider.nil?
18
+ self.provider = Provider::PegelOnline.new # go through attribute writer in order to log
19
+ end
20
+ @provider
21
+ end
22
+
23
+ def provider=(p)
24
+ Wasserstand.logger.info "Using provider #{p}"
25
+ @provider = p
20
26
  end
21
27
 
22
28
  def logger
@@ -0,0 +1,76 @@
1
+ module Wasserstand
2
+ module Commandline
3
+ def self.get(params = [])
4
+ case params.size
5
+ when 0
6
+ present_waterway(Wasserstand::Waterway.all)
7
+ when 1 # "Elbe" or "Dresden"
8
+ waterway = Wasserstand::Waterway[params.first]
9
+
10
+ if waterway
11
+ present_waterway(waterway)
12
+ else
13
+ # not a waterway, treat it as level
14
+ present_level(Wasserstand::Level[params.first])
15
+ end
16
+ when 2 # "Elbe Dresden" or "Ilm Ilmenau"
17
+ if waterway = Wasserstand::Waterway[params.first]
18
+ present_level(waterway.levels[params.last])
19
+ else
20
+ STDERR.puts "No matching waterway found."
21
+ end
22
+ else
23
+ raise "Unexpected number of parameters."
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def self.present_waterway(waterway, dig = true)
30
+ if waterway.nil?
31
+ STDERR.puts "No matching waterway found."
32
+ else
33
+ if waterway.respond_to?(:each)
34
+ STDERR.puts "The following waterways are available:"
35
+ waterway.each{|w| present_waterway(w, false)}
36
+ else
37
+ if !dig
38
+ puts waterway.name
39
+ else
40
+ STDERR.puts "#{waterway.name} has #{waterway.levels.size} levels:"
41
+ present_level(waterway.levels.values)
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.present_level(level, dig = true)
48
+ if level.nil?
49
+ STDERR.puts "No match found."
50
+ else
51
+ if level.respond_to?(:each)
52
+ level.each{|l| present_level(l, false)}
53
+ else
54
+ if !dig
55
+ puts level.name
56
+ else
57
+ STDERR.puts "#{level.name} (#{level.waterway}, km #{level.km}):"
58
+ present_measurement(level.measurements)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def self.present_measurement(measurement)
65
+ if measurement.nil?
66
+ STDERR.puts "No measurement found."
67
+ else
68
+ if measurement.respond_to?(:each)
69
+ measurement.each{|l| present_measurement(l)}
70
+ else
71
+ puts measurement
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -10,7 +10,7 @@ module Wasserstand
10
10
  when 1
11
11
  results.first
12
12
  else
13
- raise AmbigousNameError "Name '#{name}' is not unique. Found #{results.size} results."
13
+ raise AmbigousNameError.new "Name '#{name}' is not unique. Found #{results.size} results."
14
14
  end
15
15
  end
16
16
  end
@@ -1,4 +1,7 @@
1
1
  module Wasserstand
2
+ #
3
+ # Cache objects in memory
4
+ #
2
5
  class HeapCache
3
6
  def initialize
4
7
  @backend = {}
@@ -11,7 +11,7 @@ module Wasserstand
11
11
  end
12
12
 
13
13
  def to_s
14
- "#{@date}: #{@value} cm, Trend #{@trend}"
14
+ "#{@date.getlocal}: #{@value} cm, Trend #{@trend}"
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,7 @@
1
+ module Wasserstand
2
+ # Does nothing
3
+ class NullCache
4
+ def get(name);end
5
+ def set(name, value);end
6
+ end
7
+ end
@@ -1,24 +1,22 @@
1
1
  module Wasserstand
2
2
  module Provider
3
3
  class PegelOnline
4
- attr_writer :cache
5
-
6
4
  include Enumerable
7
5
  extend Forwardable
8
6
  def_delegator :all, :each
9
7
 
10
- def initialize(url = 'http://www.pegelonline.wsv.de/svgz/pegelstaende_neu.xml')
11
- @url = url
12
- @names = []
8
+ def initialize(url = nil)
9
+ @url = url || 'http://www.pegelonline.wsv.de/svgz/pegelstaende_neu.xml'
13
10
  end
14
11
 
15
- # Cache entries may expire, and simply iterating over the list of current cache entries would render an incomplete picture. Therefore we maintain the knowledge of what waterways exist in a single list, which is the authoritative source of what waterways exist.
12
+ # Cache entries may expire, and simply iterating over the list of current cache entries would render an incomplete picture. Therefore we maintain the knowledge of what waterways exist in a single list, which is the authoritative source. If that one is gone, it is gone as a whole and everything will need to be refreshed.
16
13
  def waterways
17
- replenish if @names.empty?
18
- @names.map do |name|
14
+ names = cache.get(KEY_NAMES) || replenish
15
+
16
+ names.map do |name|
19
17
  ww = cache.get(name) # no fetch with block in Dalli, so we cannot use it here either ...
20
18
 
21
- # Not finding the Waterway for a name means it was removed from the cache, but it may or may not exist in the backend. Therefore we need to replenish our cache including our knowledge about the name.
19
+ # Not finding the Waterway for a name means it was removed from the cache, but it may or may not exist in the backend. Therefore we need to replenish our cache including our knowledge about the names.
22
20
  if ww.nil?
23
21
  replenish
24
22
  ww = cache.get(name)
@@ -34,23 +32,40 @@ module Wasserstand
34
32
  end
35
33
 
36
34
  def cache
37
- @cache ||= HeapCache.new
35
+ if @cache.nil?
36
+ self.cache = HeapCache.new
37
+ end
38
+ @cache
39
+ end
40
+
41
+ def cache=(c)
42
+ Wasserstand.logger.info "Using cache #{c}"
43
+ @cache = c
44
+ end
45
+
46
+ def to_s
47
+ "#<#{self.class.name}:#{@url}>"
38
48
  end
39
49
 
40
50
  private
41
51
 
52
+ KEY_NAMES = "#{self.name}#names"
53
+
42
54
  #
43
55
  # Replenish the cache and names index
44
56
  #
45
57
  def replenish
46
- @names.clear
47
58
  Wasserstand.logger.info "Fetching #{@url}"
48
59
  doc = Nokogiri::HTML(open(@url).read, nil, 'ISO-8859-1')
60
+
61
+ names = []
49
62
  doc.xpath("//data/table/gewaesser").each do |node|
50
63
  ww = WaterwayMapper.map(node)
51
- @names << ww.name
52
64
  cache.set(ww.name, ww)
65
+ names << ww.name
53
66
  end
67
+ cache.set(KEY_NAMES, names)
68
+ names
54
69
  end
55
70
  end
56
71
  end
@@ -1,3 +1,3 @@
1
1
  module Wasserstand
2
- VERSION = "0.0.10"
2
+ VERSION = "0.0.11"
3
3
  end
@@ -0,0 +1,76 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'helper'
3
+ require 'open3'
4
+
5
+ #
6
+ # End-to-end test
7
+ #
8
+ class TestApp < MiniTest::Unit::TestCase
9
+ APP_SCRIPT = 'ruby bin/wasserstand'
10
+ DEFAULT_OPTIONS = {
11
+ 'url' => File.join(File.dirname(__FILE__), '..', 'fixtures', 'pegelstaende_neu.xml')
12
+ }
13
+
14
+ def test_all_waterways
15
+ stdout, stderr = execute
16
+ assert_equal(77, stdout.size)
17
+ assert_equal(["ILM\n", "ILMENAU\n", "ITTER ZUR DIEMEL\n", "ITTER ZUR EDER\n"], stdout.select{|name| name =~ /^I/i})
18
+ end
19
+
20
+ def test_waterway
21
+ stdout, stderr = execute('Diemel')
22
+ assert_equal(3, stdout.size)
23
+ assert_equal(["DIEMELTALSPERRE\n", "HELMINGHAUSEN\n", "WILHELMSBRÜCKE\n"], stdout)
24
+ assert_equal(["DIEMEL has 3 levels:\n"], stderr)
25
+ end
26
+
27
+ def test_unknown_waterway
28
+ stdout, stderr = execute("Donaukanal")
29
+ assert_equal(0, stdout.size)
30
+ assert_equal(["No match found.\n"], stderr)
31
+ end
32
+
33
+ def test_level
34
+ stdout, stderr = execute(['Cochem'])
35
+ assert_equal(["2012-09-13 18:15:00 +0000: 221.0 cm, Trend fallend\n"], stdout)
36
+ assert_equal(["COCHEM (MOSEL, km 51.6):\n"], stderr)
37
+ end
38
+
39
+ def test_level_with_waterway
40
+ stdout, stderr = execute(['Ems', 'Knock'])
41
+ assert_equal(["2012-09-13 20:31:00 +0000: 626.6 cm, Trend fallend\n"], stdout)
42
+ assert_equal(["KNOCK (EMS, km 50.848):\n"], stderr)
43
+ end
44
+
45
+ def test_unknown_level
46
+ stdout, stderr = execute("Altstadt")
47
+ assert_equal(0, stdout.size)
48
+ assert_equal(["No match found.\n"], stderr)
49
+ end
50
+
51
+ private
52
+
53
+ def execute(params = [], options = {})
54
+ line = []
55
+ line << "TZ='UTC'"
56
+ line << APP_SCRIPT
57
+ line.concat(Array(params))
58
+ line << DEFAULT_OPTIONS.merge(options).map do |k,v|
59
+ if v.nil? || v.to_s.empty?
60
+ "#{k}"
61
+ else
62
+ "--#{k}=#{v}"
63
+ end
64
+ end
65
+
66
+ out = ''
67
+ err = ''
68
+
69
+ Open3.popen3(line.join(' ')) do |stdin, stdout, stderr|
70
+ out << stdout.read
71
+ err << stderr.read
72
+ end
73
+
74
+ return [out.lines.to_a, err.lines.to_a]
75
+ end
76
+ end
@@ -21,6 +21,12 @@ class TestLevel < WasserstandTestCase
21
21
  assert(Level['GENTHIN'])
22
22
  end
23
23
 
24
+ def test_ambigous_name
25
+ assert_raises(Wasserstand::AmbigousNameError) do
26
+ Level['Nienburg']
27
+ end
28
+ end
29
+
24
30
  private
25
31
 
26
32
  def assert_level(values, level)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wasserstand
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-20 00:00:00.000000000 Z
12
+ date: 2012-09-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: require_all
@@ -190,11 +190,13 @@ files:
190
190
  - TODO
191
191
  - bin/wasserstand
192
192
  - lib/wasserstand.rb
193
+ - lib/wasserstand/commandline.rb
193
194
  - lib/wasserstand/finders.rb
194
195
  - lib/wasserstand/hash_clod.rb
195
196
  - lib/wasserstand/heap_cache.rb
196
197
  - lib/wasserstand/level.rb
197
198
  - lib/wasserstand/measurement.rb
199
+ - lib/wasserstand/null_cache.rb
198
200
  - lib/wasserstand/provider/level_mapper.rb
199
201
  - lib/wasserstand/provider/pegel_online.rb
200
202
  - lib/wasserstand/provider/waterway_mapper.rb
@@ -203,6 +205,7 @@ files:
203
205
  - lib/wasserstand/waterway.rb
204
206
  - test/fixtures/pegelstaende_neu.xml
205
207
  - test/helper.rb
208
+ - test/unit/test_app.rb
206
209
  - test/unit/test_level.rb
207
210
  - test/unit/test_measurement.rb
208
211
  - test/unit/test_trend.rb
@@ -235,6 +238,7 @@ summary: Wasserstand auf deutschen Flüssen
235
238
  test_files:
236
239
  - test/fixtures/pegelstaende_neu.xml
237
240
  - test/helper.rb
241
+ - test/unit/test_app.rb
238
242
  - test/unit/test_level.rb
239
243
  - test/unit/test_measurement.rb
240
244
  - test/unit/test_trend.rb