fassbinder 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,3 +1,8 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ gem 'rspec', '~> 2.5.0'
6
+ gem 'ruby-debug19', '~> 0.11.6'
7
+ gem 'vcr', '~> 1.9.0'
8
+ gem 'webmock', '~> 1.6.2'
data/Rakefile CHANGED
@@ -5,6 +5,7 @@ Bundler::GemHelper.install_tasks
5
5
 
6
6
  desc 'Run all specs in spec directory'
7
7
  RSpec::Core::RakeTask.new(:spec) do |spec|
8
+ spec.rspec_opts = %w(-fd -c)
8
9
  spec.pattern = 'spec/**/*_spec.rb'
9
10
  end
10
11
 
data/bin/rainer ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fassbinder/cli'
3
+ Fassbinder::CLI.start
data/fassbinder.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |s|
9
9
  s.authors = ['Paper Cavalier']
10
10
  s.email = 'code@papercavalier.com'
11
11
  s.homepage = 'https://rubygems.org/gems/fassbinder'
12
- s.summary = %q{Wraps Amazon in a loving embrace.}
13
- s.description = %q{Fassbinder wraps Amazon in a loving embrace.}
12
+ s.summary = %q{Crawls book offers on Amazon}
13
+ s.description = %q{Fassbinder crawls book offers on Amazon.}
14
14
 
15
15
  s.rubyforge_project = 'fassbinder'
16
16
 
@@ -19,10 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ['lib']
21
21
 
22
- s.add_dependency('kosher', '~> 0.2.18')
22
+ s.add_dependency('kosher', '~> 0.2.23')
23
23
  s.add_dependency('sucker', '~> 1.4.0')
24
- s.add_development_dependency('rspec', '~> 2.5.0')
25
- s.add_development_dependency('ruby-debug19', '~> 0.11.6')
26
- s.add_development_dependency('vcr', '~> 1.7.0')
27
- s.add_development_dependency('webmock', '~> 1.6.2')
24
+ s.add_dependency('thor', '~> 0.14.6')
28
25
  end
@@ -0,0 +1,35 @@
1
+ require 'kosher'
2
+ require 'fassbinder/offer_builder'
3
+
4
+ module Fassbinder
5
+ class BookBuilder
6
+ attr_reader :book
7
+
8
+ def initialize
9
+ @book = Kosher::Book.new
10
+ @book.offers = []
11
+ end
12
+
13
+ def add_offer(hash)
14
+ builder = OfferBuilder.new
15
+ builder.id = hash['OfferListing']['OfferListingId']
16
+ builder.venue = @book.venue
17
+ builder.add_item(hash)
18
+ builder.add_seller(hash['Merchant'])
19
+ builder.add_shipping(hash)
20
+ @book.offers << builder.offer
21
+ end
22
+
23
+ def asin=(asin)
24
+ @book.asin = asin
25
+ end
26
+
27
+ def venue=(venue)
28
+ @book.venue = venue
29
+ end
30
+
31
+ def offers_total=(count)
32
+ @book.offers_total = count.to_i
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ require 'pp'
2
+ require 'thor'
3
+ require 'fassbinder'
4
+
5
+ module Fassbinder
6
+ class CLI < Thor
7
+ desc 'all', 'Looks up all offers for an ASIN'
8
+ method_option :locale, :required => true, :aliases => '-l'
9
+ def all(asin)
10
+ locale = options[:locale] || :us
11
+ lookup(asin, locale)
12
+ end
13
+ map 'a' => 'all'
14
+
15
+ desc 'kosher', 'Looks up kosher offers for an ASIN'
16
+ method_option :locale, :required => true, :aliases => '-l'
17
+ def kosher(asin)
18
+ locale = options[:locale] || :us
19
+ lookup(asin, locale, true)
20
+ end
21
+ map 'k' => 'kosher'
22
+
23
+ private
24
+
25
+ def lookup(asin, locale, kosher_only=false)
26
+ request = Request.new(credentials)
27
+ request.locale = locale
28
+ request.batchify(asin)
29
+ offers = request.get.to_a.first.offers
30
+
31
+ offers.select!(&:kosher?) if kosher_only
32
+
33
+ offers.each do |offer|
34
+ puts offer.kosher? ? 'kosher' : 'unkosher'
35
+
36
+ offer.id = offer.id[0, 24] + '...'
37
+
38
+ description = offer.item.description.text
39
+ if description.size > 77
40
+ offer.item.description.text = description[0, 77] + '...'
41
+ end
42
+
43
+ pp offer
44
+ puts
45
+ end
46
+ end
47
+ def credentials
48
+ { 'key' => ENV['AMAZON_KEY'],
49
+ 'secret' => ENV['AMAZON_SECRET'] }
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ module Fassbinder
2
+ class InvalidResponse < StandardError
3
+ end
4
+ end
@@ -0,0 +1,46 @@
1
+ module Fassbinder
2
+ class ItemBuilder
3
+ attr_reader :item
4
+
5
+ def initialize
6
+ @item = Kosher::Item.new
7
+ end
8
+
9
+ def price=(price)
10
+ cents = price['Amount'].to_i
11
+ currency = price['CurrencyCode']
12
+
13
+ cents *= 100 if currency == 'JPY'
14
+
15
+ @item.cents = cents
16
+ @item.currency = currency
17
+ end
18
+
19
+ def quantity=(quantity)
20
+ @item.quantity = quantity.to_i
21
+ end
22
+
23
+ def add_condition(grade)
24
+ condition = Kosher::Condition.new
25
+ condition.grade =
26
+ case grade
27
+ when 'new' then 1
28
+ when 'mint' then 2
29
+ when 'verygood' then 3
30
+ when 'good' then 4
31
+ when 'acceptable' then 5
32
+ else 6
33
+ end
34
+
35
+ @item.condition = condition
36
+ end
37
+
38
+ def add_description(text)
39
+ text ||= ''
40
+ description = Kosher::Description.new
41
+ description.text = text
42
+
43
+ @item.description = description
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ require 'fassbinder/item_builder'
2
+ require 'fassbinder/seller_builder'
3
+ require 'fassbinder/shipping_builder'
4
+
5
+ module Fassbinder
6
+ class OfferBuilder
7
+ attr_reader :offer
8
+
9
+ def initialize
10
+ @offer = Kosher::Offer.new
11
+ end
12
+
13
+ def add_item(hash)
14
+ builder = ItemBuilder.new
15
+ builder.price = hash['OfferListing']['Price']
16
+ builder.quantity = hash['OfferListing']['Quantity']
17
+ builder.add_condition(hash['OfferAttributes']['SubCondition'])
18
+ builder.add_description(hash['OfferAttributes']['ConditionNote'])
19
+ @offer.item = builder.item
20
+ end
21
+
22
+ def add_seller(hash)
23
+ builder = SellerBuilder.new
24
+ builder.id = hash['MerchantId']
25
+ builder.name = hash['Name']
26
+ builder.rating = hash['AverageFeedbackRating']
27
+ builder.add_location(hash['Location']) if hash['Location']
28
+ @offer.seller = builder.seller
29
+ end
30
+
31
+ def add_shipping(hash)
32
+ builder = ShippingBuilder.new
33
+ builder.add_availability(hash['OfferListing']['AvailabilityAttributes']['MaximumHours'])
34
+ is_free = (hash['OfferListing']['IsEligibleForSuperSaverShipping'] == '1')
35
+ builder.calculate_price(is_free, @offer.venue, hash['OfferListing']['Price']['CurrencyCode'])
36
+ @offer.shipping = builder.shipping
37
+ end
38
+
39
+ def id=(id)
40
+ @offer.id = id
41
+ end
42
+
43
+ def venue=(venue)
44
+ @offer.venue = venue
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,6 @@
1
+ require 'sucker'
2
+ require 'fassbinder/response'
3
+
1
4
  module Fassbinder
2
5
  class Request < Sucker::Request
3
6
  def initialize(args = {})
@@ -7,10 +10,16 @@ module Fassbinder
7
10
  'ItemLookup.Shared.IdType' => 'ASIN',
8
11
  'ItemLookup.Shared.Condition' => 'All',
9
12
  'ItemLookup.Shared.MerchantId' => 'All',
10
- 'ItemLookup.Shared.ResponseGroup' => ['OfferFull', 'SalesRank'] })
13
+ 'ItemLookup.Shared.ResponseGroup' => ['OfferFull'] })
11
14
  end
12
15
 
13
16
  def batchify(asins)
17
+ asins = [asins].flatten
18
+
19
+ if asins.size > 20
20
+ raise ArgumentError, 'You cannot add more than 20 ASINs to a batch'
21
+ end
22
+
14
23
  self.<<({ 'ItemLookup.1.ItemId' => asins[0, 10] })
15
24
  self.<<({ 'ItemLookup.2.ItemId' => asins[10, 10] }) if asins.size > 10
16
25
  end
@@ -1,29 +1,30 @@
1
- module Fassbinder
1
+ require 'fassbinder/book_builder'
2
+ require 'fassbinder/errors'
2
3
 
3
- # And I don't believe that melodramatic feelings are laughable - they should
4
- # be taken absolutely seriously.
5
- #
4
+ module Fassbinder
6
5
  class Response
7
6
  include Enumerable
8
7
 
9
- DEFAULT_SHIPPING_CENTS = { :us => 399,
10
- :uk => 280,
11
- :de => 299,
12
- :ca => 649,
13
- :fr => 300,
14
- :jp => 25000 }
15
-
16
8
  def initialize(response, locale)
17
- raise InvalidResponseError unless response.valid?
9
+ unless response.valid?
10
+ message =
11
+ if response.has_errors?
12
+ response.errors.first['Message']
13
+ else
14
+ response.code
15
+ end
16
+
17
+ raise InvalidResponse, message
18
+ end
18
19
 
19
20
  @response = response
20
- @locale = locale
21
+ @locale = locale.to_sym
21
22
  end
22
23
 
23
24
  # Yields each snapshot to given block.
24
25
  #
25
26
  def each(&block)
26
- @response.each('Item') { |doc| block.call(parse(doc)) }
27
+ @response.each('Item') { |doc| block.call(build_book(doc)) }
27
28
  end
28
29
 
29
30
  def errors
@@ -34,44 +35,18 @@ module Fassbinder
34
35
 
35
36
  private
36
37
 
37
- def parse(doc)
38
- Kosher::Book.new(
39
- 'amazon.' + Sucker::Request::HOSTS[@locale].match(/[^.]+$/).to_s,
40
- nil,
41
- doc['ASIN'],
42
- doc['SalesRank'].to_i,
43
- doc['Offers']['TotalOffers'].to_i,
44
- [doc['Offers']['Offer']].flatten.compact.map do |doc|
45
- if doc['OfferListing']['Price']['CurrencyCode'] == 'JPY'
46
- doc['OfferListing']['Price']['Amount'] = doc['OfferListing']['Price']['Amount'].to_i * 100
47
- end
38
+ def build_book(doc)
39
+ builder = BookBuilder.new
40
+
41
+ builder.asin = doc['ASIN']
42
+ builder.offers_total = doc['Offers']['TotalOffers']
43
+ host = Sucker::Request::HOSTS[@locale]
44
+ builder.venue = "amazon.#{host.match(/[^.]+$/)}"
45
+
46
+ offers = doc['Offers']['Offer']
47
+ offers.each { |offer| builder.add_offer(offer) }
48
48
 
49
- Kosher::Offer.new(
50
- doc['OfferListing']['OfferListingId'],
51
- Kosher::Item.new(doc['OfferListing']['Price']['Amount'].to_i,
52
- doc['OfferListing']['Price']['CurrencyCode'],
53
- doc['OfferListing']['Quantity'].to_i,
54
- Kosher::Condition.new(case doc['OfferAttributes']['SubCondition']
55
- when 'new' then 1
56
- when 'mint' then 2
57
- when 'verygood' then 3
58
- when 'good' then 4
59
- when 'acceptable' then 5
60
- else 6
61
- end),
62
- Kosher::Description.new(doc['OfferAttributes']['ConditionNote'].to_s)),
63
- Kosher::Seller.new(doc['Merchant']['MerchantId'],
64
- doc['Merchant']['Name'],
65
- doc['Merchant']['AverageFeedbackRating'].to_f,
66
- Kosher::Location.new((doc['Merchant']['Location']['CountryCode'] rescue nil),
67
- (doc['Merchant']['Location']['StateCode'] rescue nil))),
68
- Kosher::Shipping.new(doc['OfferListing']['IsEligibleForSuperSaverShipping'] == '1' ?
69
- 0 : DEFAULT_SHIPPING_CENTS[@locale],
70
- doc['OfferListing']['Price']['CurrencyCode'],
71
- Kosher::Availability.new(doc['OfferListing']['AvailabilityAttributes']['MaximumHours'].to_i))
72
- )
73
- end
74
- )
49
+ builder.book
75
50
  end
76
51
  end
77
52
  end
@@ -0,0 +1,28 @@
1
+ module Fassbinder
2
+ class SellerBuilder
3
+ attr_reader :seller
4
+
5
+ def initialize
6
+ @seller = Kosher::Seller.new
7
+ end
8
+
9
+ def id=(id)
10
+ @seller.id = id
11
+ end
12
+
13
+ def name=(name)
14
+ @seller.name = name
15
+ end
16
+
17
+ def rating=(rating)
18
+ @seller.rating = rating.to_f
19
+ end
20
+
21
+ def add_location(hash)
22
+ location = Kosher::Location.new
23
+ location.country = hash['CountryCode']
24
+ location.state = hash['StateCode']
25
+ @seller.location = location
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,27 @@
1
+ module Fassbinder
2
+ class ShippingBuilder
3
+ DEFAULT_SHIPPING_CENTS = { 'amazon.com' => 399,
4
+ 'amazon.co.uk' => 280,
5
+ 'amazon.de' => 299,
6
+ 'amazon.ca' => 649,
7
+ 'amazon.fr' => 300,
8
+ 'amazon.co.jp' => 25000 }
9
+
10
+ attr_reader :shipping
11
+
12
+ def initialize
13
+ @shipping = Kosher::Shipping.new
14
+ end
15
+
16
+ def add_availability(hours)
17
+ availability = Kosher::Availability.new
18
+ availability.hours = hours.to_i
19
+ @shipping.availability = availability
20
+ end
21
+
22
+ def calculate_price(is_free, venue, currency)
23
+ @shipping.cents = is_free ? 0 : DEFAULT_SHIPPING_CENTS[venue]
24
+ @shipping.currency = currency
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module Fassbinder
2
- VERSION = '0.0.8'
2
+ VERSION = '0.0.9'
3
3
  end
data/lib/fassbinder.rb CHANGED
@@ -1,9 +1 @@
1
- require 'kosher'
2
- require 'sucker'
3
-
4
1
  require 'fassbinder/request'
5
- require 'fassbinder/response'
6
-
7
- module Fassbinder
8
- class InvalidResponseError < StandardError; end
9
- end
@@ -5,22 +5,52 @@ module Fassbinder
5
5
  let(:request) { Request.new(credentials) }
6
6
 
7
7
  describe ".new" do
8
- it "defines a batch request" do
8
+ it "sets up an item request" do
9
9
  request.parameters['Operation'].should eql 'ItemLookup'
10
+ end
11
+
12
+ it "looks up ASINs" do
10
13
  request.parameters['ItemLookup.Shared.IdType'].should eql 'ASIN'
14
+ end
15
+
16
+ it "looks up all conditions" do
11
17
  request.parameters['ItemLookup.Shared.Condition'].should eql 'All'
18
+ end
19
+
20
+ it "looks up all merchants" do
12
21
  request.parameters['ItemLookup.Shared.MerchantId'].should eql 'All'
13
- request.parameters['ItemLookup.Shared.ResponseGroup'].should eql ['OfferFull', 'SalesRank']
22
+ end
23
+
24
+ it "looks up full offers" do
25
+ request.parameters['ItemLookup.Shared.ResponseGroup'].should eql ['OfferFull']
14
26
  end
15
27
  end
16
28
 
17
29
  describe "#batchify" do
18
- it "adds up to 20 ASINs to the worker's parameters" do
19
- asins = (0..19).to_a
20
- request.batchify(asins)
30
+ context "when passed one ASIN" do
31
+ it "adds the ASIN to the batch" do\
32
+ request.batchify('foo')
33
+ request.parameters['ItemLookup.1.ItemId'].should eql ['foo']
34
+ end
35
+ end
36
+
37
+ context "when passed up to 20 ASINs" do
38
+ it "adds the ASINs to the batch" do
39
+ asins = (0..19).to_a
40
+ request.batchify(asins)
41
+
42
+ request.parameters['ItemLookup.1.ItemId'].should eql (0..9).to_a
43
+ request.parameters['ItemLookup.2.ItemId'].should eql (10..19).to_a
44
+ end
45
+ end
21
46
 
22
- request.parameters['ItemLookup.1.ItemId'].should eql (0..9).to_a
23
- request.parameters['ItemLookup.2.ItemId'].should eql (10..19).to_a
47
+ context "when passed over 20 ASINs" do
48
+ it "raises an error" do
49
+ expect do
50
+ asins = (0..20).to_a
51
+ request.batchify(asins)
52
+ end.to raise_error ArgumentError
53
+ end
24
54
  end
25
55
  end
26
56
 
@@ -29,7 +59,7 @@ module Fassbinder
29
59
  VCR.http_stubbing_adapter.http_connections_allowed = true
30
60
  end
31
61
 
32
- it "returns an algorithm" do
62
+ it "returns a response" do
33
63
  Request.stub!(:get)
34
64
 
35
65
  request.locale = :us
@@ -21,13 +21,20 @@ module Fassbinder
21
21
  end
22
22
 
23
23
  describe ".new" do
24
- it "raises an error if response is not valid" do
25
- response = mock('Response')
26
- response.stub!(:valid?).and_return(false)
27
-
28
- expect do
29
- Response.new(response, :us)
30
- end.to raise_error InvalidResponseError
24
+ context "when response is not valid" do
25
+ it "raises an error" do
26
+ response = mock('Response')
27
+ response.stub!(:valid?).and_return(false)
28
+ response.stub!(:has_errors?).and_return(true)
29
+ response.stub!(:errors).and_return([{
30
+ 'Code' => 'AccountLimitExceeded',
31
+ 'Message' => 'YOU FAIL'
32
+ }])
33
+
34
+ expect do
35
+ Response.new(response, :us)
36
+ end.to raise_error InvalidResponse, 'YOU FAIL'
37
+ end
31
38
  end
32
39
  end
33
40
 
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: fassbinder
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.0.8
5
+ version: 0.0.9
6
6
  platform: ruby
7
7
  authors:
8
8
  - Paper Cavalier
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-03-21 00:00:00 +00:00
13
+ date: 2011-04-15 00:00:00 +01:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -21,7 +21,7 @@ dependencies:
21
21
  requirements:
22
22
  - - ~>
23
23
  - !ruby/object:Gem::Version
24
- version: 0.2.18
24
+ version: 0.2.23
25
25
  type: :runtime
26
26
  version_requirements: *id001
27
27
  - !ruby/object:Gem::Dependency
@@ -36,53 +36,20 @@ dependencies:
36
36
  type: :runtime
37
37
  version_requirements: *id002
38
38
  - !ruby/object:Gem::Dependency
39
- name: rspec
39
+ name: thor
40
40
  prerelease: false
41
41
  requirement: &id003 !ruby/object:Gem::Requirement
42
42
  none: false
43
43
  requirements:
44
44
  - - ~>
45
45
  - !ruby/object:Gem::Version
46
- version: 2.5.0
47
- type: :development
46
+ version: 0.14.6
47
+ type: :runtime
48
48
  version_requirements: *id003
49
- - !ruby/object:Gem::Dependency
50
- name: ruby-debug19
51
- prerelease: false
52
- requirement: &id004 !ruby/object:Gem::Requirement
53
- none: false
54
- requirements:
55
- - - ~>
56
- - !ruby/object:Gem::Version
57
- version: 0.11.6
58
- type: :development
59
- version_requirements: *id004
60
- - !ruby/object:Gem::Dependency
61
- name: vcr
62
- prerelease: false
63
- requirement: &id005 !ruby/object:Gem::Requirement
64
- none: false
65
- requirements:
66
- - - ~>
67
- - !ruby/object:Gem::Version
68
- version: 1.7.0
69
- type: :development
70
- version_requirements: *id005
71
- - !ruby/object:Gem::Dependency
72
- name: webmock
73
- prerelease: false
74
- requirement: &id006 !ruby/object:Gem::Requirement
75
- none: false
76
- requirements:
77
- - - ~>
78
- - !ruby/object:Gem::Version
79
- version: 1.6.2
80
- type: :development
81
- version_requirements: *id006
82
- description: Fassbinder wraps Amazon in a loving embrace.
49
+ description: Fassbinder crawls book offers on Amazon.
83
50
  email: code@papercavalier.com
84
- executables: []
85
-
51
+ executables:
52
+ - rainer
86
53
  extensions: []
87
54
 
88
55
  extra_rdoc_files: []
@@ -93,11 +60,19 @@ files:
93
60
  - LICENSE
94
61
  - README.md
95
62
  - Rakefile
63
+ - bin/rainer
96
64
  - fassbinder.gemspec
97
65
  - fassbinder.jpg
98
66
  - lib/fassbinder.rb
67
+ - lib/fassbinder/book_builder.rb
68
+ - lib/fassbinder/cli.rb
69
+ - lib/fassbinder/errors.rb
70
+ - lib/fassbinder/item_builder.rb
71
+ - lib/fassbinder/offer_builder.rb
99
72
  - lib/fassbinder/request.rb
100
73
  - lib/fassbinder/response.rb
74
+ - lib/fassbinder/seller_builder.rb
75
+ - lib/fassbinder/shipping_builder.rb
101
76
  - lib/fassbinder/version.rb
102
77
  - spec/fassbinder/request_spec.rb
103
78
  - spec/fassbinder/response_spec.rb
@@ -128,10 +103,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
103
  requirements: []
129
104
 
130
105
  rubyforge_project: fassbinder
131
- rubygems_version: 1.5.2
106
+ rubygems_version: 1.6.2
132
107
  signing_key:
133
108
  specification_version: 3
134
- summary: Wraps Amazon in a loving embrace.
109
+ summary: Crawls book offers on Amazon
135
110
  test_files:
136
111
  - spec/fassbinder/request_spec.rb
137
112
  - spec/fassbinder/response_spec.rb