blythedunham-base4r 0.2.0.6 → 0.2.0.7
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 +339 -0
- data/README +19 -0
- data/cert/cacert.pem +10908 -0
- data/lib/attribute.rb +332 -0
- data/lib/base4r.rb +25 -0
- data/lib/base_client.rb +149 -0
- data/lib/client_login.rb +98 -0
- data/lib/http_logger.rb +32 -0
- data/lib/item.rb +290 -0
- data/test/attribute_test.rb +58 -0
- data/test/base_client_test.rb +93 -0
- data/test/client_login_test.rb +60 -0
- data/test/item_test.rb +51 -0
- metadata +15 -3
data/lib/client_login.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Base4R is a ruby interface to Google Base
|
2
|
+
# Copyright 2007, 2008 Dan Dukeson
|
3
|
+
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
|
14
|
+
# You should have received a copy of the GNU General Public License along
|
15
|
+
# with this program; if not, write to the Free Software Foundation, Inc.,
|
16
|
+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
17
|
+
|
18
|
+
#
|
19
|
+
# Authenticates the user with google ClientLogin
|
20
|
+
#
|
21
|
+
#
|
22
|
+
require 'net/https'
|
23
|
+
|
24
|
+
module Base4R
|
25
|
+
|
26
|
+
#
|
27
|
+
# ClientLogin authenticates the user with Google Accounts using the ClientLogin service.
|
28
|
+
#
|
29
|
+
class ClientLogin
|
30
|
+
|
31
|
+
include Base4R::HTTPLogger
|
32
|
+
ServiceURL = URI.parse('https://www.google.com/accounts/ClientLogin')
|
33
|
+
|
34
|
+
# Create a ClientLogin. _options_ is optional and by default logs into
|
35
|
+
# the Google Base API.
|
36
|
+
def initialize(options={})
|
37
|
+
|
38
|
+
defaults = {:accountType => 'GOOGLE',
|
39
|
+
:service => 'gbase',
|
40
|
+
:source => 'base4r-clientloginapp-v1'}
|
41
|
+
|
42
|
+
@config = defaults.merge(options)
|
43
|
+
end
|
44
|
+
|
45
|
+
# attempt to authenticate _email_ and _password_ with Google.
|
46
|
+
# Returns an authentication token on success, raises Exception on failure.
|
47
|
+
# todo: make exception handling more useful
|
48
|
+
def authenticate(email, password)
|
49
|
+
|
50
|
+
params = {
|
51
|
+
'accountType' => @config[:accountType],
|
52
|
+
'Email' => email,
|
53
|
+
'Passwd' => password,
|
54
|
+
'service' => @config[:service],
|
55
|
+
'source' => @config[:source]
|
56
|
+
}
|
57
|
+
|
58
|
+
req = Net::HTTP::Post.new(ServiceURL.path)
|
59
|
+
req.set_form_data(params)
|
60
|
+
|
61
|
+
http = Net::HTTP.new(ServiceURL.host, 443)
|
62
|
+
http.use_ssl = true
|
63
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
64
|
+
http.ca_file = File.expand_path(File.dirname(__FILE__)+"/../cert/cacert.pem")
|
65
|
+
# todo - above is probably reading file for every request, cache certs instead
|
66
|
+
|
67
|
+
log_request req
|
68
|
+
resp = http.request(req)
|
69
|
+
log_response resp
|
70
|
+
|
71
|
+
unless resp.instance_of? Net::HTTPOK then
|
72
|
+
resp.body =~ /^Error=(.+)$/
|
73
|
+
|
74
|
+
raise CaptchaRequiredException.new(email) if 'CaptchaRequired' == $1
|
75
|
+
raise 'unknown exception authenticating with google:'+$1
|
76
|
+
end
|
77
|
+
|
78
|
+
# parse the auth key from the body
|
79
|
+
resp.body =~ /^Auth=(.+)$/
|
80
|
+
return $1
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# thrown when ClientLogin fails to authenticate because the ClientLogin API has
|
86
|
+
# requested that the user complete a captcha validation.
|
87
|
+
class CaptchaRequiredException < Exception
|
88
|
+
|
89
|
+
attr_reader :email
|
90
|
+
|
91
|
+
# Create a CaptchaRequiredException to notify the user that _email_ must complete
|
92
|
+
# a captcha validation
|
93
|
+
def initialize(email)
|
94
|
+
@email = email
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
data/lib/http_logger.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module Base4R
|
2
|
+
module HTTPLogger
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend self
|
6
|
+
end
|
7
|
+
|
8
|
+
def verbose?; false; end
|
9
|
+
|
10
|
+
def log(message)
|
11
|
+
return unless verbose?
|
12
|
+
STDOUT.puts message
|
13
|
+
end
|
14
|
+
|
15
|
+
def log_request(request, options={})
|
16
|
+
log "-------#{request.to_s}----------"
|
17
|
+
log "URL: #{options[:url]}" if options[:url]
|
18
|
+
log "METHOD: #{options[:method]}" if options[:method]
|
19
|
+
|
20
|
+
request.each_capitalized {|k, v| log "#{k}: #{v}" } if request.respond_to? :each_capitalized
|
21
|
+
|
22
|
+
log((options[:data] ? options[:data] : request.body).to_s.gsub('><',">\n<"))
|
23
|
+
|
24
|
+
log "-------#{request.to_s}----------"
|
25
|
+
end
|
26
|
+
|
27
|
+
def log_response(response)
|
28
|
+
log_request(response)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
data/lib/item.rb
ADDED
@@ -0,0 +1,290 @@
|
|
1
|
+
# Base4R is a ruby interface to Google Base
|
2
|
+
# Copyright 2007, 2008 Dan Dukeson
|
3
|
+
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
|
14
|
+
# You should have received a copy of the GNU General Public License along
|
15
|
+
# with this program; if not, write to the Free Software Foundation, Inc.,
|
16
|
+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
17
|
+
|
18
|
+
require 'rexml/document'
|
19
|
+
|
20
|
+
module Base4R
|
21
|
+
module ItemAttributeMethods
|
22
|
+
def self.included(base)
|
23
|
+
base.send :include, InstanceMethods
|
24
|
+
base.send :extend, ClassMethods
|
25
|
+
end
|
26
|
+
module ClassMethods
|
27
|
+
#set the attribute definitions and define setters and getters
|
28
|
+
def define_attributes(options)
|
29
|
+
@attribute_definitions ||= {}
|
30
|
+
@attribute_definitions.update(options)
|
31
|
+
options.each {|k,v| define_setter_method(k, v)}
|
32
|
+
end
|
33
|
+
|
34
|
+
#define setter methods on the object
|
35
|
+
def define_setter_method(method, options={})
|
36
|
+
class_eval <<-EOS, __FILE__, __LINE__
|
37
|
+
def set_#{method}(value); add_attribute(:#{method}, value); end
|
38
|
+
alias_method "#{method}=", "set_#{method}"
|
39
|
+
EOS
|
40
|
+
end
|
41
|
+
|
42
|
+
# Define setter and getter methods. The example will define author_name and author_name=
|
43
|
+
# define_child_method :author, :name
|
44
|
+
def define_child_accessor(parent, child)
|
45
|
+
class_eval <<-EOF
|
46
|
+
def #{parent}_#{child}; child_value(:#{parent}, :#{child}); end
|
47
|
+
def #{parent}_#{child}=(v); set_child_value(:#{parent}, :#{child}, v); end
|
48
|
+
EOF
|
49
|
+
end
|
50
|
+
|
51
|
+
# return all actual and inherited definitions
|
52
|
+
def attribute_definitions#:nodoc
|
53
|
+
@all_attribute_definitions ||=
|
54
|
+
(superclass.respond_to?(:attribute_definitions) ?
|
55
|
+
superclass.attribute_definitions :
|
56
|
+
{}).merge(@attribute_definitions||{})
|
57
|
+
end
|
58
|
+
|
59
|
+
# return the attribute definition for a specific attribute
|
60
|
+
def attribute_definition(name)#:nodoc:
|
61
|
+
attribute_definitions[name.to_sym]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
module InstanceMethods
|
65
|
+
|
66
|
+
def child_value(parent_name, child_name)
|
67
|
+
parent = get_attribute(parent_name)
|
68
|
+
child = parent.send(child_name) if parent
|
69
|
+
child.value if child
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_child_value(parent_name, child_name, value)
|
73
|
+
parent = get_attribute(parent_name)||add_attribute(parent_name, nil)
|
74
|
+
parent.add_child(child_name, value)
|
75
|
+
end
|
76
|
+
|
77
|
+
# return the attribute objects that correspond to this element
|
78
|
+
def get_attributes(name); attributes.select {|a| a.name.to_s == name.to_s }; end
|
79
|
+
def get_attribute(name); attributes.detect {|a| a.name.to_s == name.to_s }; end
|
80
|
+
|
81
|
+
# set the google private flag for the specified attribute
|
82
|
+
# item.set_attribute_private :location
|
83
|
+
def set_attribute_private(name, is_private=true)
|
84
|
+
attributes.each { |attr| attr.private_attribute = is_private if name.to_s == attr.name.to_s }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Add the attribute to the item
|
88
|
+
# item.add_attribute :price, :value => 5, :units => 'USD'
|
89
|
+
# item.add_attribute :item_type, 'Products'
|
90
|
+
def add_attribute(attribute_name, value)
|
91
|
+
# get the options passed in from the value
|
92
|
+
options = options_from_value(value)
|
93
|
+
|
94
|
+
attr_def = self.class.attribute_definition(attribute_name)||{}
|
95
|
+
attr_class = type_to_attribute_class(options[:type]||attr_def[:type]||:text)
|
96
|
+
|
97
|
+
# add a namespace param if there is one
|
98
|
+
options[:namespace] ||= attr_def[:namespace] if attr_def.has_key?(:namespace)
|
99
|
+
|
100
|
+
#raise options.inspect if attribute_name.to_s == 'customme'
|
101
|
+
# create a new attribute
|
102
|
+
attr = attr_class.new(attr_def[:name]||attribute_name, options)
|
103
|
+
|
104
|
+
@attributes << attr
|
105
|
+
attr
|
106
|
+
end
|
107
|
+
# convert type field to the corresponding Attribute class
|
108
|
+
# Example :text is AttributeText
|
109
|
+
def type_to_attribute_class(type_name)#:nodoc:
|
110
|
+
return type_name if type_name.is_a?(Attribute)
|
111
|
+
|
112
|
+
Base4R.const_get "#{type_name.to_s.gsub(/(?:^|_)(.)/) { $1.upcase } }Attribute"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# Item describes a single entry that will be or is already stored in Google Base
|
119
|
+
class Item
|
120
|
+
|
121
|
+
include ItemAttributeMethods
|
122
|
+
|
123
|
+
# array of Attribute objects that describe the Item
|
124
|
+
attr_accessor :attributes
|
125
|
+
|
126
|
+
# Title of the Item
|
127
|
+
attr_accessor :title
|
128
|
+
|
129
|
+
# ID of this Item as assigned by Google
|
130
|
+
attr_accessor :base_id
|
131
|
+
|
132
|
+
# unique alphnumeric identifier for the item - e.g. your internal ID code.
|
133
|
+
# IMPORTANT: Once you submit an item with a unique id, this identifier must
|
134
|
+
# not change when you send in a new data feed. Each item must retain the same
|
135
|
+
# id in subsequent feeds.
|
136
|
+
attr_accessor :unique_id
|
137
|
+
|
138
|
+
attr_accessor :draft
|
139
|
+
|
140
|
+
define_child_accessor :author, :name
|
141
|
+
define_child_accessor :author, :email
|
142
|
+
|
143
|
+
def options_from_value(value, default_options={})#:nodoc:
|
144
|
+
default_options.merge(value.is_a?(Hash) ? value : {:value => value})
|
145
|
+
end
|
146
|
+
|
147
|
+
# Represents this Item as Atom XML which is the format required by the Base API.
|
148
|
+
def to_xml
|
149
|
+
|
150
|
+
doc = REXML::Document.new('<?xml version="1.0" ?>')
|
151
|
+
|
152
|
+
entry = doc.add_element 'entry',
|
153
|
+
'xmlns'=>'http://www.w3.org/2005/Atom',
|
154
|
+
'xmlns:g' => 'http://base.google.com/ns/1.0',
|
155
|
+
'xmlns:app'=>'http://purl.org/atom/app#',
|
156
|
+
'xmlns:gm'=>'http://base.google.com/ns-metadata/1.0'
|
157
|
+
|
158
|
+
#bjd: not sure why these are instance variables but others are not
|
159
|
+
#if @author_name || @author_email
|
160
|
+
# AuthorAttribute.to_xml(entry, :author, nil, :email => @author_email, :name => @author_name)
|
161
|
+
#end
|
162
|
+
|
163
|
+
entry.add_element 'category',
|
164
|
+
'scheme'=>'http://www.google.com/type',
|
165
|
+
'term' => 'googlebase.item'
|
166
|
+
|
167
|
+
if draft?
|
168
|
+
goog_control = entry.add_element('app:control', 'xmlns:app'=>'http://purl.org/atom/app#')
|
169
|
+
goog_control.add_element('app:draft').text = 'yes'
|
170
|
+
end
|
171
|
+
|
172
|
+
entry.add_element('title').text= @title
|
173
|
+
|
174
|
+
@attributes.each do |attr|
|
175
|
+
attr.to_xml(entry)
|
176
|
+
end
|
177
|
+
|
178
|
+
doc
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
def draft=(value)
|
183
|
+
@draft = value.is_a?(TrueClass) || value.to_s.downcase == 'yes'
|
184
|
+
end
|
185
|
+
|
186
|
+
def draft?; @draft; end
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
#
|
191
|
+
# Item with a minimal set of Attributes, extend for specific Item Types
|
192
|
+
#
|
193
|
+
#
|
194
|
+
class UniversalItem < Item
|
195
|
+
define_attributes(
|
196
|
+
:description => nil,
|
197
|
+
:contact_phone => nil,
|
198
|
+
:item_type => nil,
|
199
|
+
:item_language => nil,
|
200
|
+
:target_country => nil,
|
201
|
+
:application => nil,
|
202
|
+
:link => { :type => :url, :namespace => nil },
|
203
|
+
:expiration_date=> { :type => :dateTime },
|
204
|
+
:label => nil,
|
205
|
+
:unique_id => { :name => :id },
|
206
|
+
:author => { :value => nil, :namespace => nil, :type => :author },
|
207
|
+
:image_link => { :type => :bare },
|
208
|
+
:location => { :type => :location}
|
209
|
+
)
|
210
|
+
|
211
|
+
# Create a new UniversalItem, with _unique_id_, created by _author_name_ who's email is _author_email_,
|
212
|
+
# described by _description_, found at URL _link_, entitled _title_, phone number is
|
213
|
+
# _contact_phone_, item type is _item_type_, _target_country_ e.g. 'GB', _item_language_ e.g. 'EN'
|
214
|
+
#
|
215
|
+
# Args can also be a hash of attributes
|
216
|
+
def initialize(*args)
|
217
|
+
options = initialize_args_to_options(args)
|
218
|
+
#allow an option to specify which columns are private
|
219
|
+
private_attributes = [options.delete(:private_attributes)].flatten.inject([]){|list, val| list << val.to_s if val; list}.uniq
|
220
|
+
|
221
|
+
@title = options.delete(:title)
|
222
|
+
self.draft = options.delete(:draft)
|
223
|
+
@attributes = []
|
224
|
+
|
225
|
+
options.each do |key, value|
|
226
|
+
v = options_from_value(value, :private_attribute => private_attributes.include?(key.to_s))
|
227
|
+
#back support for author name and email
|
228
|
+
if value && [:author_name, :author_email].include?(key.to_sym)
|
229
|
+
send "#{key}=", value
|
230
|
+
else
|
231
|
+
add_attribute key, v
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
protected
|
237
|
+
# convert old style input args to new style
|
238
|
+
def initialize_args_to_options(args)#:nodoc:
|
239
|
+
options = if args.first.is_a?(Hash)
|
240
|
+
args.first.dup
|
241
|
+
else
|
242
|
+
%w(unique_id author_name author_email description link title contact_phone item_type target_country item_lang).inject({}) {|map, option|
|
243
|
+
map[option.to_sym] = args.shift
|
244
|
+
map
|
245
|
+
}
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
public
|
250
|
+
|
251
|
+
# Define for backwards compatibility
|
252
|
+
alias_method :add_image_link, :set_image_link
|
253
|
+
alias_method :add_label, :set_label
|
254
|
+
alias_method :context=, :set_description
|
255
|
+
alias_method :item_lang=, :set_item_language
|
256
|
+
end
|
257
|
+
|
258
|
+
# ProductItem is a standard item type. This class includes some of the attributes that are
|
259
|
+
# suggested or required for the Product item type.
|
260
|
+
#
|
261
|
+
class ProductItem < UniversalItem
|
262
|
+
|
263
|
+
|
264
|
+
define_attributes({
|
265
|
+
:condition => nil,
|
266
|
+
:will_deliver => {:type => :boolean },
|
267
|
+
:delivery_notes => nil,
|
268
|
+
:department => nil,
|
269
|
+
:payment => nil,
|
270
|
+
:payment_notes => nil,
|
271
|
+
:pickup => {:type => :boolean },
|
272
|
+
:price_type => nil,
|
273
|
+
:price_units => nil,
|
274
|
+
:price => {:type => :float_unit},
|
275
|
+
:image_like => { :type => :bare },
|
276
|
+
:quantity => { :type => :int }})
|
277
|
+
|
278
|
+
#rewrite to satisfy old style of price setting
|
279
|
+
# set_price 5, 'usd'
|
280
|
+
# set_price :value => 5, :units => 'usd'
|
281
|
+
def price(price_amount, price_units=nil)
|
282
|
+
add_attribute :price, :value => price_amount, :units => price_units
|
283
|
+
end
|
284
|
+
|
285
|
+
alias_method :add_payment, :payment=
|
286
|
+
alias_method :add_custom, :add_attribute
|
287
|
+
alias_method :add_custom_test, :add_attribute
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Base4R is a ruby interface to Google Base
|
2
|
+
# Copyright 2007 Dan Dukeson
|
3
|
+
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation; either version 2 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
|
14
|
+
# You should have received a copy of the GNU General Public License along
|
15
|
+
# with this program; if not, write to the Free Software Foundation, Inc.,
|
16
|
+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
17
|
+
#
|
18
|
+
# tests for Attribute and subclasses
|
19
|
+
#
|
20
|
+
require 'test/unit'
|
21
|
+
require 'attribute'
|
22
|
+
|
23
|
+
module Base4R
|
24
|
+
|
25
|
+
class AttributeTest < Test::Unit::TestCase #:nodoc:
|
26
|
+
|
27
|
+
def setup
|
28
|
+
|
29
|
+
@text_attr = TextAttribute.new('testname', 'testvalue', :g)
|
30
|
+
@url_attr = UrlAttribute.new('testname', 'testvalue', :g)
|
31
|
+
@date_time_attr = DateTimeAttribute.new('testname', 'testvalue', :g)
|
32
|
+
@int_attr = IntAttribute.new('testname', 'testvalue', :g)
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_type_names
|
37
|
+
assert_equal 'text', @text_attr.type_name
|
38
|
+
assert_equal 'url', @url_attr.type_name
|
39
|
+
assert_equal 'dateTime', @date_time_attr.type_name
|
40
|
+
assert_equal 'int', @int_attr.type_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_accessors
|
44
|
+
|
45
|
+
@text_attr.name = 'foo'
|
46
|
+
@text_attr.value = 'bar'
|
47
|
+
@text_attr.type_name = 'text'
|
48
|
+
@text_attr.namespace = :atom
|
49
|
+
|
50
|
+
assert_equal 'foo', @text_attr.name
|
51
|
+
assert_equal 'bar', @text_attr.value
|
52
|
+
assert_equal 'text',@text_attr.type_name
|
53
|
+
assert_equal :atom, @text_attr.namespace
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|