fassbinder 0.0.8 → 0.0.9

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/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