paapi 0.0.2 → 0.0.3

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