restfully 0.2.3 → 0.3.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.
data/LICENSE CHANGED
@@ -1,4 +1,5 @@
1
1
  Copyright (c) 2009 Cyril Rohr
2
+ Copyright (c) 2009 INRIA Rennes - Bretagne Atlantique
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining
4
5
  a copy of this software and associated documentation files (the
@@ -9,7 +9,8 @@ Alpha work.
9
9
 
10
10
  == Usage
11
11
  === Command line
12
- $ resfully base_uri [root_path] [-u username] [-p password]
12
+ $ export RUBYOPT="-rubygems"
13
+ $ restfully base_uri [root_path] [-u username] [-p password]
13
14
 
14
15
  e.g., for the Grid5000 API:
15
16
  $ restfully https://api.grid5000.fr/sid /grid5000 -u username -p password
@@ -17,37 +18,74 @@ e.g., for the Grid5000 API:
17
18
  If the connection was successful, you should get a prompt. Call the +root+ function to see what are the available resources. <tt>@property</tt> means that you can call the +property+ method on the object. Other properties are available via the <tt>[]</tt> function.
18
19
  e.g.:
19
20
  irb(main):005:0> root
20
- => #<Restfully::Resource:0x90bdd4
21
- ------------ META ------------
22
- @uri: "/grid5000"
23
- @uid: "grid5000"
24
- @type: "grid"
25
- @environments: Restfully::Collection
26
- @sites: Restfully::Collection
27
- @version: Restfully::Resource
28
- @versions: Restfully::Collection
29
- ------------ PROPERTIES ------------
30
- "version" => "ee1f8111c8835496a9e4a6f7c9491c0238ee9827">
21
+ => #<Restfully::Resource:0x8848de
22
+ ------------ META ------------
23
+ @uri: #<URI::Generic:0x11091a8 URL:/grid5000>
24
+ @uid: "grid5000"
25
+ @type: "grid"
26
+ @environments: Restfully::Collection
27
+ @sites: Restfully::Collection
28
+ @version: Restfully::Resource
29
+ @versions: Restfully::Collection
30
+ ------------ PROPERTIES ------------
31
+ "version" => "4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8">
31
32
 
32
33
  irb(main):006:0> root.uri
33
- => "/grid5000"
34
+ => #<URI::Generic:0x11091a8 URL:/grid5000>
34
35
 
35
36
  irb(main):007:0> root.sites
36
- => #<Restfully::Collection:0x8faaf2
37
- ------------ META ------------
38
- @uri: "/grid5000/sites"
39
- ------------ PROPERTIES ------------
40
- "lille" => Restfully::Resource
41
- "grenoble" => Restfully::Resource
42
- "toulouse" => Restfully::Resource
43
- "sophia" => Restfully::Resource
44
- "rennes" => Restfully::Resource
45
- "lyon" => Restfully::Resource
46
- "nancy" => Restfully::Resource
47
- "bordeaux" => Restfully::Resource
48
- "orsay" => Restfully::Resource>
49
- irb(main):008:0> root['version']
50
- => "ee1f8111c8835496a9e4a6f7c9491c0238ee9827"
37
+ => #<Restfully::Collection:0x881f6c
38
+ ------------ META ------------
39
+ @uri: "/grid5000/sites"
40
+ @offset: 0
41
+ @total: 9
42
+ @version: Restfully::Resource
43
+ @versions: Restfully::Collection
44
+ ------------ PROPERTIES ------------
45
+ "version" => "4fe96b25d2cbfee16abe5a4fb999c82dbafc2ee8"
46
+ ------------ ITEMS ------------
47
+ Restfully::Resource
48
+ Restfully::Resource
49
+ Restfully::Resource
50
+ Restfully::Resource
51
+ Restfully::Resource
52
+ Restfully::Resource
53
+ Restfully::Resource
54
+ Restfully::Resource
55
+ Restfully::Resource>
56
+
57
+ irb(main):008:0> root.sites.by_uid('rennes').clusters.by_uid('paradent').nodes.by_uid('paradent-1').metrics.by_uid('mem_free', 'cpu_idle', 'bytes_in')
58
+ => [#<Restfully::Resource:0x4259a
59
+ ------------ META ------------
60
+ @uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/mem_free"
61
+ @uid: "mem_free"
62
+ @type: "metric"
63
+ @parent: Restfully::Resource
64
+ @timeseries: Restfully::Resource
65
+ ------------ PROPERTIES ------------
66
+ "last_update" => -901639018
67
+ "step" => 15
68
+ "timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>, #<Restfully::Resource:0x12444
69
+ ------------ META ------------
70
+ @uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/cpu_idle"
71
+ @uid: "cpu_idle"
72
+ @type: "metric"
73
+ @parent: Restfully::Resource
74
+ @timeseries: Restfully::Resource
75
+ ------------ PROPERTIES ------------
76
+ "last_update" => -901639018
77
+ "step" => 15
78
+ "timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>, #<Restfully::Resource:0xe4c870
79
+ ------------ META ------------
80
+ @uri: "/grid5000/sites/rennes/clusters/paradent/nodes/paradent-1/metrics/bytes_in"
81
+ @uid: "bytes_in"
82
+ @type: "metric"
83
+ @parent: Restfully::Resource
84
+ @timeseries: Restfully::Resource
85
+ ------------ PROPERTIES ------------
86
+ "last_update" => -901639018
87
+ "step" => 15
88
+ "timeseries" => [{"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>1}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>24}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>168}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>244, "pdp_per_row"=>672}, {"xff"=>0.5, "cf"=>"AVERAGE", "rows"=>374, "pdp_per_row"=>5760}]>]
51
89
 
52
90
  irb(main):009:0> root.version
53
91
  => #<Restfully::Resource:0x8facf0
@@ -90,4 +128,4 @@ See the +examples+ directory for examples.
90
128
 
91
129
  == Copyright
92
130
 
93
- Copyright (c) 2009 Cyril Rohr. See LICENSE for details.
131
+ Copyright (c) 2009 Cyril Rohr, INRIA Rennes - Bretagne Atlantique. See LICENSE for details.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.3.0
@@ -9,26 +9,31 @@ require 'logger'
9
9
  require 'pp'
10
10
  require 'yaml'
11
11
 
12
- @options = {}
12
+
13
+ logger = Logger.new(STDOUT)
14
+ logger.level = Logger::WARN
15
+ @options = {:logger => logger}
13
16
  option_parser = OptionParser.new do |opts|
14
17
  opts.banner = "Usage: restfully base_uri [root_path] [options]"
15
18
 
16
19
  opts.on("-u=", "--username=", "Sets the username") do |u|
17
- @options['username'] = u
20
+ @options[:username] = u
18
21
  end
19
22
  opts.on("-p=", "--password=", "Sets the user password") do |p|
20
- @options['password'] = p
23
+ @options[:password] = p
21
24
  end
22
25
  opts.on("-c=", "--config=", "Sets the various options based on a custom YAML configuration file") do |v|
23
- @options['configuration_file'] = v
24
- end
25
- opts.on("-v", "--verbose", "Run verbosely") do |v|
26
- @options['verbose'] = v
26
+ @options[:configuration_file] = v
27
27
  end
28
28
  opts.on("--log=", "Outputs log messages to the given file. Defaults to stdout") do |v|
29
- @options['log'] = v
29
+ original_logger_level = logger.level
30
+ logger = Logger.new(File.expand_path(v))
31
+ logger.level = original_logger_level
32
+ @options[:logger] = logger
33
+ end
34
+ opts.on("-v", "--verbose", "Run verbosely") do |v|
35
+ @options[:logger].level = Logger::DEBUG
30
36
  end
31
-
32
37
  opts.on_tail("-h", "--help", "Show this message") do
33
38
  puts opts
34
39
  exit
@@ -38,44 +43,61 @@ end
38
43
 
39
44
  option_parser.parse!
40
45
 
41
- @base_uri = ARGV.shift
42
- @root_path = ARGV.shift || "/"
46
+ @options[:base_uri] = ARGV.shift
47
+ @options[:root_path] = ARGV.shift || "/"
43
48
 
44
- if (config_filename = @options.delete('configuration_file')) && File.exists?(File.expand_path(config_filename))
45
- config = YAML.load_file(File.expand_path(config_filename))
46
- @base_uri = config.delete('base_uri') || @base_uri
47
- @root_path = config.delete('root_path') || @root_path
48
- @options.merge!(config)
49
+ def session
50
+ @session ||= Restfully::Session.new(@options)
49
51
  end
50
52
 
51
- unless @base_uri
52
- $stderr.puts option_parser.help
53
- exit(-1)
54
- else
55
- if (log_file=@options.delete('log'))
56
- @logger = Logger.new(File.expand_path(log_file))
57
- else
58
- @logger = Logger.new(STDOUT)
59
- end
60
- if @options.delete('verbose')
61
- @logger.level = Logger::DEBUG
62
- else
63
- @logger.level = Logger::WARN
64
- end
65
-
66
- def session
67
- @session ||= Restfully::Session.new(@base_uri, @options.merge('root_path' => @root_path, 'logger' => @logger))
68
- end
69
- def root
70
- @root ||= Restfully::Resource.new(session.root_path, session).load
71
- end
72
-
73
- root # preloads
74
-
75
- require 'irb'
76
- require 'irb/completion'
77
-
78
- ARGV.clear
79
- IRB.start
80
- exit!
53
+ def root
54
+ @root ||= session.root.load
55
+ rescue Restfully::HTTP::Error => e
56
+ puts "#{e.class.name}: #{e.message}"
81
57
  end
58
+
59
+ require 'irb'
60
+ require 'irb/completion'
61
+ ARGV.clear
62
+ IRB.start
63
+ exit!
64
+
65
+
66
+ # if (config_filename = @options.delete('configuration_file')) && File.exists?(File.expand_path(config_filename))
67
+ # config = YAML.load_file(File.expand_path(config_filename))
68
+ # @base_uri = config.delete('base_uri') || @base_uri
69
+ # @root_path = config.delete('root_path') || @root_path
70
+ # @options.merge!(config)
71
+ # end
72
+ #
73
+ # unless @base_uri
74
+ # $stderr.puts option_parser.help
75
+ # exit(-1)
76
+ # else
77
+ # if (log_file=@options.delete('log'))
78
+ # @logger = Logger.new(File.expand_path(log_file))
79
+ # else
80
+ # @logger = Logger.new(STDOUT)
81
+ # end
82
+ # if @options.delete('verbose')
83
+ # @logger.level = Logger::DEBUG
84
+ # else
85
+ # @logger.level = Logger::WARN
86
+ # end
87
+ #
88
+ # def session
89
+ # @session ||= Restfully::Session.new(@base_uri, @options.merge('root_path' => @root_path, 'logger' => @logger))
90
+ # end
91
+ # def root
92
+ # @root ||= Restfully::Resource.new(session.root_path, session).load
93
+ # end
94
+ #
95
+ # root # preloads
96
+ #
97
+ # require 'irb'
98
+ # require 'irb/completion'
99
+ #
100
+ # ARGV.clear
101
+ # IRB.start
102
+ # exit!
103
+ # end
@@ -10,19 +10,19 @@ logger.level = Logger::INFO
10
10
  # Restfully.adapter = Restfully::HTTP::RestClientAdapter
11
11
  # Restfully.adapter = Patron::Session
12
12
  RestClient.log = 'stdout'
13
- Restfully::Session.new('https://localhost:3443/sid', 'root_path' => '/grid5000', 'logger' => logger) do |grid, session|
13
+ Restfully::Session.new(:base_uri => 'http://api.local/sid', :root_path => '/grid5000', :logger => logger) do |grid, session|
14
14
  grid_stats = {'hardware' => {}, 'system' => {}}
15
- grid.sites.each do |site_uid, site|
16
- site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, (node_uid, node_status)|
15
+ grid.sites.each do |site|
16
+ site_stats = site.status.inject({'hardware' => {}, 'system' => {}}) {|accu, node_status|
17
17
  accu['hardware'][node_status['hardware_state']] = (accu['hardware'][node_status['hardware_state']] || 0) + 1
18
18
  accu['system'][node_status['system_state']] = (accu['system'][node_status['system_state']] || 0) + 1
19
19
  accu
20
- }
20
+ } rescue {'hardware' => {}, 'system' => {}}
21
21
  grid_stats['hardware'].merge!(site_stats['hardware']) { |key,oldval,newval| oldval+newval }
22
22
  grid_stats['system'].merge!(site_stats['system']) { |key,oldval,newval| oldval+newval }
23
- p [site_uid, site_stats]
23
+ p [site.uid, site_stats]
24
24
  end
25
25
  p [:total, grid_stats]
26
26
  puts "Getting status of a few nodes in rennes:"
27
- pp grid.sites['rennes'].status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
27
+ pp grid.sites.by_uid('rennes').status(:query => {:only => ['paradent-1', 'paradent-10', 'paramount-3']})
28
28
  end
@@ -11,7 +11,8 @@ require 'restfully/resource'
11
11
  require 'restfully/collection'
12
12
 
13
13
 
14
- module Restfully
14
+ module Restfully
15
+ VERSION = "0.3.0"
15
16
  class << self
16
17
  attr_accessor :adapter
17
18
  end
@@ -1,41 +1,87 @@
1
1
  require 'delegate'
2
2
 
3
3
  module Restfully
4
- class Collection < DelegateClass(Hash)
4
+ # Resource and Collection classes should clearly include a common module to deal with links, attributes, associations and loading
5
+ class Collection < DelegateClass(Array)
5
6
 
6
- attr_reader :state, :raw, :uri, :session, :title
7
+ attr_reader :state, :raw, :uri, :session, :title, :offset, :total
7
8
 
8
9
  def initialize(uri, session, options = {})
9
10
  options = options.symbolize_keys
10
11
  @uri = uri
11
12
  @title = options[:title]
12
13
  @session = session
13
- @raw = options[:raw]
14
14
  @state = :unloaded
15
- @items = {}
15
+ @items = []
16
+ @indexes = {}
17
+ @associations = {}
18
+ @attributes = {}
16
19
  super(@items)
17
20
  end
18
21
 
19
22
  def loaded?; @state == :loaded; end
20
23
 
24
+ def method_missing(method, *args)
25
+ load
26
+ if association = @associations[method.to_s]
27
+ session.logger.debug "Loading association #{method}, args=#{args.inspect}"
28
+ association.load(*args)
29
+ elsif method.to_s =~ /^by_(.+)$/
30
+ key = $1
31
+ @indexes[method.to_s] ||= @items.inject({}) { |accu, item|
32
+ accu[item.has_key?(key) ? item[key] : item.send(key.to_sym)] = item
33
+ accu
34
+ }
35
+ if args.empty?
36
+ @indexes[method.to_s]
37
+ elsif args.length == 1
38
+ @indexes[method.to_s][args.first]
39
+ else
40
+ @indexes[method.to_s].values_at(*args)
41
+ end
42
+ else
43
+ super(method, *args)
44
+ end
45
+ end
46
+
21
47
  def load(options = {})
22
48
  options = options.symbolize_keys
23
49
  force_reload = !!options.delete(:reload) || options.has_key?(:query)
24
- if loaded? && !force_reload
50
+ if loaded? && !force_reload && options[:raw].nil?
25
51
  self
26
- else
52
+ else
53
+ @raw = options[:raw]
54
+ @associations.clear
55
+ @attributes.clear
27
56
  @items.clear
28
57
  if raw.nil? || force_reload
29
58
  response = session.get(uri, options)
30
59
  @raw = response.body
31
60
  end
32
61
  raw.each do |key, value|
33
- next if key == 'links'
34
- self_link = (value['links'] || []).map{|link| Link.new(link)}.detect{|link| link.self?}
35
- if self_link && self_link.valid?
36
- @items.store(key, Resource.new(self_link.href, session, :raw => value).load)
62
+ case key
63
+ when "total", "offset"
64
+ instance_variable_set("@#{key}".to_sym, value)
65
+ when "links"
66
+ value.each{|link| define_link(Link.new(link))}
67
+ when "items"
68
+ value.each do |item|
69
+ self_link = (item['links'] || []).map{|link| Link.new(link)}.detect{|link| link.self?}
70
+ if self_link && self_link.valid?
71
+ @items << Resource.new(self_link.href, session).load(:raw => item)
72
+ else
73
+ session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
74
+ end
75
+ end
37
76
  else
38
- session.logger.warn "Resource #{key} does not have a 'self' link. skipped."
77
+ case value
78
+ when Hash
79
+ @attributes.store(key, SpecialHash.new.replace(value)) unless @associations.has_key?(key)
80
+ when Array
81
+ @attributes.store(key, SpecialArray.new(value))
82
+ else
83
+ @attributes.store(key, value)
84
+ end
39
85
  end
40
86
  end
41
87
  @state = :loaded
@@ -43,21 +89,58 @@ module Restfully
43
89
  end
44
90
  end
45
91
 
92
+ def respond_to?(method, *args)
93
+ @associations.has_key?(method.to_s) || super(method, *args)
94
+ end
95
+
46
96
  def inspect(options = {:space => "\t"})
47
97
  output = "#<#{self.class}:0x#{self.object_id.to_s(16)}"
48
98
  if loaded?
49
99
  output += "\n#{options[:space]}------------ META ------------"
50
100
  output += "\n#{options[:space]}@uri: #{uri.inspect}"
51
- unless @items.empty?
101
+ output += "\n#{options[:space]}@offset: #{offset.inspect}"
102
+ output += "\n#{options[:space]}@total: #{total.inspect}"
103
+ @associations.each do |title, assoc|
104
+ output += "\n#{options[:space]}@#{title}: #{assoc.class.name}"
105
+ end
106
+ unless @attributes.empty?
52
107
  output += "\n#{options[:space]}------------ PROPERTIES ------------"
53
- @items.each do |key, value|
54
- output += "\n#{options[:space]}#{key.inspect} => #{value.class.name}"
108
+ @attributes.each do |key, value|
109
+ output += "\n#{options[:space]}#{key.inspect} => #{value.inspect}"
110
+ end
111
+ end
112
+ unless @items.empty?
113
+ output += "\n#{options[:space]}------------ ITEMS ------------"
114
+ @items.each do |value|
115
+ output += "\n#{options[:space]}#{value.class.name}"
55
116
  end
56
117
  end
57
118
  end
58
119
  output += ">"
59
120
  end
60
121
 
61
-
122
+ protected
123
+ def define_link(link)
124
+ if link.valid?
125
+ case link.rel
126
+ when 'parent'
127
+ @associations['parent'] = Resource.new(link.href, session)
128
+ when 'collection'
129
+ raw_included = link.resolved? ? raw[link.title] : nil
130
+ @associations[link.title] = Collection.new(link.href, session,
131
+ :raw => raw_included,
132
+ :title => link.title)
133
+ when 'member'
134
+ raw_included = link.resolved? ? raw[link.title] : nil
135
+ @associations[link.title] = Resource.new(link.href, session,
136
+ :title => link.title,
137
+ :raw => raw_included)
138
+ when 'self'
139
+ # we do nothing
140
+ end
141
+ else
142
+ session.logger.warn link.errors.join("\n")
143
+ end
144
+ end
62
145
  end
63
146
  end