paapi 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9cad942d2b1dc53bddcf3aae1397a9e7381fc58c0f0cac8205dc104375bb3579
4
- data.tar.gz: '09cbf5cf7e8a57ff2741bc3f60870dbe5f07f20983808399164a17c292b5726c'
3
+ metadata.gz: a4bac67043fb32a957f0b015eb468efbcc852b9e5285940ff152bdc012b4c751
4
+ data.tar.gz: e4f35c14a82bf615d95167b869cb7b73c69cc85e7f05bd0ef1c37a96556b5b78
5
5
  SHA512:
6
- metadata.gz: ed07665c0791353feccdbc0589208592c4c032a45e9d8656d8ad63d7783e9380c5b5851aa6458c85fb736d4fc9e6e0beabd0beb8102a2b72ec0d43f4e8dd855b
7
- data.tar.gz: 56418706b3d9a0c0cd8a2e6df5231a50d7b8df8963289a59fe0401ffe0fb015d634f9695ae750d7fbfff581cf7847baee4ae7e05a93c0d9d38a308ba034d1558
6
+ metadata.gz: 5943daef94869da7e96ca4d1b1b35fdd98f58f90bcebd7a00c24e1b11baf32cc5488986104b2d8edc95637076630437aaa2a5467683a8ca219e8a2a31b99aed4
7
+ data.tar.gz: 7dd1fd49d4c63a6e6ff67d2881137f0c7bd5c0fe6e166a917929b3f08d69f1a31dbbf63d3d5720f75fce53f3ff67fe0b5b3d097858eff99c362e0731fed68aec
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  `paapi` is a slim wrapper around [Amazon's Product Advertising API 5.0](https://webservices.amazon.com/paapi5/documentation/).
4
4
 
5
+ This gem is under development and currently incomplete.
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -1,21 +1,27 @@
1
1
  require 'paapi/version'
2
2
 
3
- require 'paapi/locales'
4
3
  require 'paapi/aws_request'
5
4
  require 'paapi/client'
5
+ require 'paapi/item'
6
6
  require 'paapi/response'
7
7
 
8
8
  module Paapi
9
9
  class Error < StandardError; end
10
10
  class NotImplemented < StandardError; end
11
+ SEARCH_PARAMS = %i[keywords actor, artist, author, brand title].freeze
11
12
  DEFAULT_PARTNER_TYPE = 'Associates'
12
13
  DEFAULT_MARKET = :us
13
14
  DEFAULT_RESOURCES = [
14
15
  'Images.Primary.Large',
16
+ 'ItemInfo.ByLineInfo',
15
17
  'ItemInfo.ContentInfo',
18
+ 'ItemInfo.ExternalIds',
19
+ 'ItemInfo.Features',
20
+ 'ItemInfo.ManufactureInfo',
16
21
  'ItemInfo.ProductInfo',
22
+ 'ItemInfo.TechnicalInfo', # Includes format when Kindle
17
23
  'ItemInfo.Title',
18
- 'ItemInfo.ExternalIds',
24
+ 'ItemInfo.TradeInInfo',
19
25
  'Offers.Listings.Availability.Message',
20
26
  'Offers.Listings.Condition',
21
27
  'Offers.Listings.Condition.SubCondition',
@@ -25,7 +31,39 @@ module Paapi
25
31
  'Offers.Listings.MerchantInfo',
26
32
  'Offers.Listings.Price',
27
33
  'Offers.Listings.SavingBasis'
28
- ]
34
+ ].freeze
35
+
36
+ Locale = Struct.new(:key, :name, :host, :region) do
37
+ def site
38
+ host.sub('webservices', 'www')
39
+ end
40
+ end
41
+
42
+ MARKETPLACES = {
43
+ au: Locale.new(:au, 'Australia', 'webservices.amazon.com.au', 'us-west-2'),
44
+ br: Locale.new(:br, 'Brazil', 'webservices.amazon.com.br' 'us-east-1'),
45
+ ca: Locale.new(:ca, 'Canada', 'webservices.amazon.ca', 'us-east-1'),
46
+ fr: Locale.new(:fr, 'France', 'webservices.amazon.fr', 'eu-west-1'),
47
+ de: Locale.new(:de, 'Germany', 'webservices.amazon.de', 'eu-west-1'),
48
+ in: Locale.new(:in, 'India', 'webservices.amazon.in', 'eu-west-1'),
49
+ it: Locale.new(:it, 'Italy', 'webservices.amazon.it', 'eu-west-1'),
50
+ jp: Locale.new(:jp, 'Japan', 'webservices.amazon.co.jp', 'us-west-2'),
51
+ mx: Locale.new(:mx, 'Mexico', 'webservices.amazon.com.mx', 'us-east-1'),
52
+ es: Locale.new(:es, 'Spain', 'webservices.amazon.es', 'eu-west-1'),
53
+ tr: Locale.new(:tk, 'Turkey', 'webservices.amazon.com.tr', 'eu-west-1'),
54
+ ae: Locale.new(:ae, 'United Arab Emirates', 'webservices.amazon.ae', 'eu-west-1'),
55
+ uk: Locale.new(:uk, 'United Kingdom', 'webservices.amazon.co.uk', 'eu-west-1'),
56
+ us: Locale.new(:us, 'United States', 'webservices.amazon.com', 'us-east-1'),
57
+ }.freeze
58
+
59
+ Operation = Struct.new(:target_name, :endpoint_suffix, :http_method, :service )
60
+
61
+ OPERATIONS = {
62
+ get_browse_nodes: Operation.new( 'GetBrowseNodes', 'getbrowsenodes', 'POST', 'ProductAdvertisingAPI' ),
63
+ get_items: Operation.new( 'GetItems', 'getitems', 'POST', 'ProductAdvertisingAPI' ),
64
+ get_variations: Operation.new( 'GetVariations', 'getvariations', 'POST', 'ProductAdvertisingAPI' ),
65
+ search_items: Operation.new( 'SearchItems', 'searchitems', 'POST', 'ProductAdvertisingAPI' )
66
+ }.freeze
29
67
 
30
68
  class << self
31
69
  attr_accessor :access_key,
@@ -43,4 +81,9 @@ module Paapi
43
81
  end
44
82
  alias_method :config, :configure
45
83
  end
84
+
85
+ def symbolize_keys(hash)
86
+ Hash[hash.map{|k,v| v.is_a?(Hash) ? [k.to_sym, symbolize_keys(v)] : [k.to_sym, v] }]
87
+ end
46
88
  end
89
+
@@ -0,0 +1,47 @@
1
+ require 'http'
2
+ require 'aws-sigv4'
3
+
4
+ # https://webservices.amazon.com/paapi5/documentation/common-request-parameters.html
5
+ module Paapi
6
+ module AwsRequest
7
+ def do_request(op:, payload:)
8
+ raise ArguemntError unless Paapi::OPERATIONS.keys.include?(op)
9
+
10
+ operation = OPERATIONS[op]
11
+
12
+ headers = {
13
+ 'X-Amz-Target' => "com.amazon.paapi5.v1.ProductAdvertisingAPIv1.#{operation.target_name}",
14
+ 'Content-Encoding' => 'amz-1.0',
15
+ }
16
+
17
+ default_payload = {
18
+ 'PartnerTag' => partner_tag,
19
+ 'PartnerType' => partner_type,
20
+ 'Marketplace' => marketplace.site
21
+ }
22
+
23
+ payload = default_payload.merge(payload)
24
+
25
+ endpoint = "https://#{marketplace.host}/paapi5/#{operation.endpoint_suffix}"
26
+
27
+ signer = Aws::Sigv4::Signer.new(
28
+ service: operation.service,
29
+ region: marketplace.region,
30
+ access_key_id: access_key,
31
+ secret_access_key: secret_key,
32
+ http_method: operation.http_method,
33
+ endpoint: marketplace.host
34
+ )
35
+
36
+ signature = signer.sign_request(http_method: operation.http_method, url: endpoint, headers: headers, body: payload.to_json)
37
+
38
+ headers['Host'] = marketplace.host
39
+ headers['X-Amz-Date'] = signature.headers['x-amz-date']
40
+ headers['X-Amz-Content-Sha256']= signature.headers['x-amz-content-sha256']
41
+ headers['Authorization'] = signature.headers['authorization']
42
+ headers['Content-Type'] = 'application/json; charset=utf-8'
43
+
44
+ HTTP.headers(headers).post(endpoint, json: payload )
45
+ end
46
+ end
47
+ end
@@ -23,35 +23,38 @@ module Paapi
23
23
  @partner_tag = partner_tag if !partner_tag.nil?
24
24
  end
25
25
 
26
- def market=(_market)
27
- @market = _market
26
+ def market=(a_market)
27
+ @market = a_market
28
28
  @marketplace = MARKETPLACES[market.to_sym]
29
29
  if !Paapi.partner_market.nil?
30
- @partner_tag = Paapi.partner_market.dig(_market) || @partner_tag
30
+ @partner_tag = Paapi.partner_market.dig(a_market) || @partner_tag
31
31
  end
32
32
  end
33
33
 
34
34
  def get_items(item_ids:, **options)
35
- item_ids = Array(item_ids)
36
-
37
- payload = { ItemIds: item_ids, Resources: @resources }
35
+ payload = { ItemIds: Array(item_ids), Resources: @resources }
38
36
 
39
37
  res = do_request(op: :get_items, payload: payload)
40
38
 
41
- Response.new(res)
39
+ # Errors, ItemResults -> Items -> Item
40
+ return Response.new(res)
42
41
  end
43
42
 
44
43
  def get_variations(asin:, **options )
45
44
  payload = { ASIN: asin, Resources: @resources }
46
45
 
47
46
  res = do_request(op: :get_variations, payload: payload)
48
- Response.new(res)
47
+
48
+ # Errors, VariationsResult->Items
49
+ Response.new(res)
49
50
  end
50
51
 
51
- def search_items(keywords:, **options )
52
- search_index = 'All'
53
52
 
54
- # %i[Keywords Actor Artist Author Brand Title ]
53
+ # TODO: Currently we assume Keywords, but we need one of the follow: [Keywords Actor Artist Author Brand Title ]
54
+ def search_items(keywords: nil, **options )
55
+ raise ArgumentError("Missing keywords") unless (options.keys | SEARCH_PARAMS).length.positive?
56
+
57
+ search_index = options.dig(:SearchIndex) ||'All'
55
58
 
56
59
  payload = { Keywords: keywords, Resources: @resources, ItemCount: 10, ItemPage: 1, SearchIndex: search_index }.merge(options)
57
60
 
@@ -60,9 +63,13 @@ module Paapi
60
63
  Response.new(res)
61
64
  end
62
65
 
63
- def get_browse_nodes(keywords:, **options)
64
- raise NotImplemented
65
- #Response.new(Request.new(client: self).get_browse_nodes(keywords: keywords, **options))
66
+ def get_browse_nodes(browse_node_ids:, **options)
67
+ resources = ['BrowseNodes.Ancestor', 'BrowseNodes.Children']
68
+ payload = { BrowseNodeIds: Array(browse_node_ids), Resources: @resources }.merge(options)
69
+
70
+ res = do_request(op: :get_browse_nodes, payload: payload)
71
+
72
+ Response.new(res)
66
73
  end
67
74
  end
68
75
  end
@@ -0,0 +1,113 @@
1
+ require 'nameable'
2
+
3
+ module Paapi
4
+ class Item
5
+ attr_accessor :raw
6
+ def initialize(data)
7
+ @raw = data
8
+ end
9
+
10
+ def asin
11
+ get(['ASIN'])
12
+ end
13
+
14
+ def detail_url
15
+ get(['DetailPageURL'])
16
+ end
17
+
18
+ def image_url
19
+ get(%w{Images Primary Large Height URL})
20
+ end
21
+
22
+ def title
23
+ get(%w{ItemInfo Title DisplayValue})
24
+ end
25
+
26
+ def manufacturer
27
+ get(%w{ItemInfo ByLineInfo Manufacturer DisplayValue})
28
+ end
29
+
30
+ def publisher
31
+ manufacturer
32
+ end
33
+
34
+ def publication_date
35
+ d = get(%w{ItemInfo ContentInfo PublicationDate DisplayValue})
36
+ return d.nil? ? nil : Date.parse(d)
37
+ end
38
+
39
+ def release_date
40
+ d = get(%w{ItemInfo ProductInfo ReleaseDate DisplayValue})
41
+ return d.nil? ? nil : Date.parse(d)
42
+ end
43
+
44
+ def contributors
45
+ get(%w{ItemInfo ByLineInfo Contributors})
46
+ end
47
+
48
+ def contributors_of(kind)
49
+ contributors&.select { |e| e['Role'] == kind }&.map { |e| Nameable(e['Name'])}
50
+ end
51
+
52
+ def authors
53
+ contributors_of 'Author'
54
+ end
55
+
56
+ def illustrators
57
+ contributors_of 'Illustrator'
58
+ end
59
+
60
+ def actors
61
+ contributors_of 'Actor'
62
+ end
63
+
64
+ def narrators
65
+ contributors_of 'Narrator'
66
+ end
67
+
68
+ def publishers
69
+ contributors_of 'Publisher'
70
+ end
71
+
72
+ def release_date
73
+ get(%w{ItemInfo ReleaseDate})
74
+ end
75
+
76
+ def eans
77
+ get(%w{ItemInfo ExternalIds EANs DisplayValues})
78
+ end
79
+
80
+ def upcs
81
+ get(%w{ItemInfo ExternalIds UPCs DisplayValues})
82
+ end
83
+
84
+ def features
85
+ get(%w{ItemInfo Features DisplayValues})&.join(' ')
86
+ end
87
+
88
+ def brand
89
+ get(%w{ItemInfo ByLineInfo Brand DisplayValue})
90
+ end
91
+
92
+ def part_number
93
+ get(%w{ItemInfo ManufactureInfo ItemPartNumber DisplayValue})
94
+ end
95
+
96
+ def package
97
+ get(%w{ItemInfo TechnicalInfo Formats DisplayValues})
98
+ end
99
+
100
+ def kindle?
101
+ !package.nil? && package&.include?('Kindle eBook')
102
+ end
103
+
104
+ def get(keys)
105
+ @raw.dig(*keys)
106
+ end
107
+
108
+ def self.to_items(data)
109
+ data.map {|d| Item.new(d)}
110
+ end
111
+
112
+ end
113
+ end
@@ -2,10 +2,41 @@ require 'json'
2
2
 
3
3
  module Paapi
4
4
  class Response
5
- attr_reader :http_response, :data
5
+ attr_reader :http_response, :data, :datas, :doc, :items
6
+
6
7
  def initialize(response)
7
8
  @http_response = response
8
- @data = JSON.parse( response.body.to_s )
9
+ @data = JSON.parse(response.body.to_s)
10
+
11
+ @datas = symbolise(JSON.parse(response.body.to_s))
12
+ @doc = JSON.parse(@datas.to_json, object_class: OpenStruct)
13
+
14
+ @items_data = @data.dig('ItemsResult', 'Items')
15
+ @items_data ||= @data.dig('SearchResult', 'Items')
16
+ @items_data ||= []
17
+
18
+ @items = @items_data.map {|d| Item.new(d)}
19
+
9
20
  end
21
+
22
+ def snake_case(s)
23
+ return s.downcase if s.match(/\A[A-Z]+\z/)
24
+ s.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
25
+ gsub(/([a-z])([A-Z])/, '\1_\2').
26
+ downcase
27
+ end
28
+
29
+ def symbolise(obj)
30
+ if obj.is_a? Hash
31
+ return obj.inject({}) do |memo, (k, v)|
32
+ memo.tap { |m| m[snake_case(k)] = symbolise(v) }
33
+ end
34
+ elsif obj.is_a? Array
35
+ return obj.map { |memo| symbolise(memo) }
36
+ end
37
+ obj
38
+ end
39
+
40
+
10
41
  end
11
42
  end
@@ -1,3 +1,3 @@
1
1
  module Paapi
2
- VERSION = '0.0.2'
2
+ VERSION = '0.0.3'
3
3
  end
@@ -33,4 +33,5 @@ Gem::Specification.new do |spec|
33
33
 
34
34
  spec.add_dependency 'http', '~> 4'
35
35
  spec.add_dependency 'aws-sigv4', '~> 1'
36
+ spec.add_dependency 'nameable', '~> 1'
36
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Milne
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-09-09 00:00:00.000000000 Z
11
+ date: 2019-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '1'
111
+ - !ruby/object:Gem::Dependency
112
+ name: nameable
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1'
111
125
  description:
112
126
  email:
113
127
  - d@nmilne.com
@@ -124,7 +138,9 @@ files:
124
138
  - bin/console
125
139
  - bin/setup
126
140
  - lib/paapi.rb
141
+ - lib/paapi/aws_request.rb
127
142
  - lib/paapi/client.rb
143
+ - lib/paapi/item.rb
128
144
  - lib/paapi/response.rb
129
145
  - lib/paapi/version.rb
130
146
  - paapi.gemspec