tcg-player-sdk 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 444973875234b87717e9ece0dd3c0373d707d2a13bac732330f7ab8d6a740f72
4
+ data.tar.gz: cc526ef5f4bc6cadb4404096dfdf9b5a414b5a438d99c4ddf58171b2b6beb214
5
+ SHA512:
6
+ metadata.gz: cdede9b22fe30a6f4106af572ca2f0523e6989ec426de0d307fdc37252cfee511ed6275bae255d954eb9cd386465042d650ac78a6096e522d2810cbe26d1d6bb
7
+ data.tar.gz: e4649464a2dd938a07b9ee991cd1b9635248d36685f4f301feb31a5dcded44c43446f32e6a5172cf688938d2e36725c4277b1ef2e2e4e716f0aef7d75445b91a
@@ -0,0 +1,20 @@
1
+ class TCGPlayerSDK::BearerToken
2
+ attr_accessor :expires_in, :token, :expiration, :issued
3
+
4
+ def initialize(params = {})
5
+ self.expires_in = params['expires_in']
6
+ self.token = params['access_token']
7
+ self.issued = DateTime.parse(params['.issued'])
8
+ self.expiration = DateTime.parse(params['.expires'])
9
+ end
10
+
11
+ ##
12
+ # @return true if the bearer token is within five minutes of expiring
13
+ def expired?
14
+ return expiration.nil? || (DateTime.now >= expiration)
15
+ end
16
+
17
+ def to_s
18
+ ap self, indent: -2
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # Helpers for pokemon-centric tasks
2
+ class TCGPlayerSDK::Pokemon
3
+ attr_accessor :tcg
4
+
5
+ def initialize(_tcg)
6
+ self.tcg = _tcg
7
+ @category = nil
8
+ end
9
+
10
+ def category
11
+ @category ||= tcg.categories(limit: 100).select{|c| c.name =~ /Pokemon/i}.first
12
+ end
13
+
14
+ def categoryId
15
+ category.categoryId
16
+ end
17
+
18
+ def manifest
19
+ @manifest ||= tcg.category_search_manifest(categoryId)
20
+ end
21
+
22
+ ##
23
+ # Returns the TCGPlaeryAPI filter corresponding to Sets
24
+ def sets
25
+ @set_filter ||= @manifest.results.first.filters.select{|f| f.name == 'SetName'}.first
26
+ end
27
+
28
+ ##
29
+ # Returns the TCGPlayerSDK filter item corresponding to the input set
30
+ #
31
+ # pokemon.set('Base Set')
32
+ # [
33
+ # [0] TCGPlayerSDK::ResponseStruct {
34
+ # :text => "Base Set",
35
+ # :value => "Base Set"
36
+ # }
37
+ # ]
38
+ def set(set_name)
39
+ sets.items.select{|i| i.text == set_name}
40
+ end
41
+ end
@@ -0,0 +1,84 @@
1
+ class TCGPlayerSDK::ProductPriceList
2
+ attr_accessor :response
3
+
4
+ ##
5
+ # Group prices by product ID. This will set
6
+ # prices to an empty hash if there was an error. Check response.errors for details about any errors.
7
+ #
8
+ # @param _response[TCGPlayerSDK::ResponseStruct] the result of calling TCGPlayerAPI#product_pricing
9
+ def initialize(_response)
10
+ @response = _response
11
+
12
+ if(response.success && response.results)
13
+ @prices = response.results.map{|r| TCGPlayerSDK::ProductPrice.new(r)}.group_by{|r| r.productId}
14
+ else
15
+ @prices = {}
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Returns a hash with productIds as keys, and a list of prices with subtypes as values:
21
+ # {
22
+ # 85736 => [
23
+ # {
24
+ # "productId" => 85737,
25
+ # "lowPrice" => 4.99,
26
+ # "midPrice" => 5.63,
27
+ # "highPrice" => 7.73,
28
+ # "marketPrice" => 10.35,
29
+ # "directLowPrice" => nil,
30
+ # "subTypeName" => "Reverse Holofoil"
31
+ # },
32
+ # {
33
+ # "productId" => 85737,
34
+ # "lowPrice" => nil,
35
+ # "midPrice" => nil,
36
+ # "highPrice" => nil,
37
+ # "marketPrice" => nil,
38
+ # "directLowPrice" => nil,
39
+ # "subTypeName" => "Unlimited Holofoil"
40
+ # },
41
+ # ]
42
+ # }
43
+ #
44
+ # @return [Hash<Integer, Array<TCGPlayerSDK::ProductPrice>>]
45
+ def prices
46
+ @prices
47
+ end
48
+
49
+ ##
50
+ # Weed out any ProductPrice objects that have no valid prices.
51
+ #
52
+ # @return [Hash<Integer, Array<TCGPlayerSDK::ProductPrice>>]
53
+ def valid_prices
54
+ valid_prices = {}
55
+ @prices.each do |id, prl|
56
+ valid_prices[id] ||= []
57
+ valid_prices[id] = prl.select{|pr| pr.has_valid_prices?}
58
+ end
59
+
60
+ return valid_prices
61
+ end
62
+ end
63
+
64
+ # A wrapper around the ResponseStruct for an individual product subtype's price.
65
+ # {
66
+ # "productId" => 85737,
67
+ # "lowPrice" => nil,
68
+ # "midPrice" => nil,
69
+ # "highPrice" => nil,
70
+ # "marketPrice" => nil,
71
+ # "directLowPrice" => nil,
72
+ # "subTypeName" => "Unlimited Holofoil"
73
+ # },
74
+ class TCGPlayerSDK::ProductPrice < TCGPlayerSDK::ResponseStruct
75
+ def initialize(hash = nil)
76
+ super
77
+ end
78
+
79
+ ##
80
+ # @return [Boolean] Returns false if there are no non-nil prices
81
+ def has_valid_prices?
82
+ return !(lowPrice.nil? && midPrice.nil? && highPrice.nil? && directLowPrice.nil? && marketPrice.nil?)
83
+ end
84
+ end
@@ -0,0 +1,113 @@
1
+ class TCGPlayerSDK::ResponseStruct < OpenStruct
2
+ include Enumerable
3
+
4
+ ##
5
+ # Attempt to execute a given block of code. Return the result on success,
6
+ # or the *default* on failure.
7
+ #
8
+ # try(nil) {self.results.first.name}
9
+ def try(default, &blk)
10
+ begin
11
+ return self.instance_eval(&blk)
12
+ rescue
13
+ return default
14
+ end
15
+ end
16
+
17
+ ##
18
+ # Iterates over `self.results` and attempts to fetch any missing results.
19
+ # For example, if you get a response that has "totalItems" equal to a number that is larger
20
+ # than the size of "results", then this function easily lets you iterate over every item,
21
+ # and will automatically fetch any items not included in "results."
22
+ #
23
+ # Since this class includes Enumberable, any Enumerable methods are also available.
24
+ #
25
+ # Example:
26
+ # 2.7.2 :057 > cl = tcg.categories
27
+ # D, [2022-01-25T22:20:34.597036 #67042] DEBUG -- : Query: https://api.tcgplayer.com/catalog/categories params:
28
+ # D, [2022-01-25T22:20:34.597129 #67042] DEBUG -- : {}
29
+ # TCGPlayerSDK::ResponseStruct {
30
+ # :totalItems => 67,
31
+ # :results => [...]
32
+ #
33
+ # 2.7.2 :072 > cl.results.size
34
+ # 10
35
+ #
36
+ # 2.7.2 :073 > allcl = cl.map{|result| result.name}
37
+ # D, [2022-01-25T22:29:11.506994 #67042] DEBUG -- : Query: https://api.tcgplayer.com/catalog/categories params:
38
+ # D, [2022-01-25T22:29:11.507161 #67042] DEBUG -- : {
39
+ # :offset => 10,
40
+ # :limit => 100
41
+ # }
42
+ # [
43
+ # [ 0] "Alternate Souls",
44
+ # [ 1] "Architect TCG",
45
+ # [ 2] "Argent Saga TCG",
46
+ # [ 3] "Axis & Allies",
47
+ # ...
48
+ # [64] "WoW",
49
+ # [65] "YuGiOh",
50
+ # [66] "Zombie World Order TCG"
51
+ # ]
52
+ def each(&block)
53
+ if(!self.totalItems.nil? && !self.results.nil? && !self.tcg_object.nil?)
54
+ max_items = self.totalItems
55
+
56
+ # Defaults
57
+ offset = self.results.size
58
+ limit = defined?(self.base_query.params.limit) ? self.base_query.params.limit : 100
59
+ offset += defined?(self.base_query.params.offset) ? self.base_query.params.offset : 0
60
+
61
+ self.results.each(&block)
62
+ while(offset < max_items && !self.results.empty?)
63
+ # Fetch more results
64
+ fetch_params = self.base_query.params.dup.to_h
65
+ fetch_params[:offset] = offset
66
+ fetch_params[:limit] = limit
67
+ more_results = self.tcg_object.query(self.base_query.url, fetch_params)
68
+
69
+ # iterate
70
+ more_results.results.each(&block)
71
+ offset += more_results.results.size
72
+ end
73
+ else
74
+ # Throw an exception?
75
+ end
76
+ end
77
+
78
+ # Create ResponseStructs out of any nested hashes or arrays of hashes
79
+ def method_missing(mid, *args)
80
+ # Avoid assignments
81
+ if(mid.to_s =~ /=/)
82
+ return super
83
+ else
84
+ # Get original value
85
+ value = super
86
+
87
+ # Turn it into a ResponseStruct
88
+ new_val = (value.is_a?(Array) && value.first.is_a?(Hash)) ?
89
+ value.map{|v| TCGPlayerSDK::ResponseStruct.new(v)} :
90
+ ((value.is_a?(Hash)) ? TCGPlayerSDK::ResponseStruct.new(value) : value)
91
+
92
+ # Save it back as a ResponseStruct
93
+ send("#{mid.to_s}=", new_val)
94
+ return new_val
95
+ end
96
+ end
97
+
98
+ def respond_to_missing?(mid, include_private = nil)
99
+ super
100
+ end
101
+
102
+ def to_s
103
+ ap self, indent: -2
104
+ end
105
+
106
+ def to_h(*args)
107
+ self.marshal_dump
108
+ end
109
+
110
+ def keys
111
+ return self.to_h.keys
112
+ end
113
+ end
@@ -0,0 +1,142 @@
1
+ ##
2
+ # Wrap up request handling and common functions for TCGPlayer price API
3
+ class TCGPlayerSDK
4
+ attr_accessor :bearer_token, :user_agent, :logger, :noretry, :public_key, :private_key
5
+
6
+ API_VERSION = '1.39'
7
+ BASE_URL = 'https://api.tcgplayer.com'
8
+ TOKEN_URL = "#{BASE_URL}/token"
9
+ CATALOG_URL = "#{BASE_URL}/catalog"
10
+ CATEGORIES_URL = "#{CATALOG_URL}/categories"
11
+ PRICING_URL = "#{BASE_URL}/pricing"
12
+
13
+ ##
14
+ # @param params[Hash]
15
+ # - user_agent: An identifying user agent string to be passed along with each request
16
+ # - bearer_token: Optionally pass in a previously authenticated bearer token
17
+ # - noretry: Set this to true to disable retrying queries when the bearer token is invalid
18
+ # - logger: Optionally pass a custom logging object
19
+ # - debug: Set output verbosity to DEBUG. Default verbosity is WARN
20
+ def initialize(params = {})
21
+ self.user_agent = params[:user_agent] || 'Unknown'
22
+ self.bearer_token = params[:bearer_token]
23
+ self.noretry = params[:noretry]
24
+
25
+ self.public_key = params[:public_key]
26
+ self.private_key = params[:private_key]
27
+
28
+ # Setup logging
29
+ self.logger = params[:logger] || Logger.new(STDOUT)
30
+ if(params[:debug])
31
+ self.logger.level = Logger::DEBUG
32
+ else
33
+ self.logger.level = Logger::WARN
34
+ end
35
+ end
36
+
37
+ # Get a new bearer token. Specify your app's public and private key as parameters
38
+ # or via Environment variables (or via .env) TCG_PLAYER_API_PUBLIC_KEY and TCG_PLAYER_API_PRIVATE_KEY
39
+ #
40
+ # @param params[Hash]
41
+ # - public_key: your TCP Player API pubic key
42
+ # - private_key: your TCP Player API pubic key
43
+ #
44
+ # @return [TCGPlayerSDK::BearerToken]
45
+ def authorize(params = {})
46
+ pub_key = params[:public_key] || public_key || ENV['TCG_PLAYER_API_PUBLIC_KEY']
47
+ pri_key = params[:private_key] || private_key || ENV['TCG_PLAYER_API_PRIVATE_KEY']
48
+
49
+ #"grant_type=client_credentials&client_id=PUBLIC_KEY&client_secret=PRIVATE_KEY"
50
+ query_params = {grant_type: 'client_credentials', client_id: pub_key, client_secret: pri_key}
51
+ response = HTTP.post(TOKEN_URL, form: query_params)
52
+ resp_hash = response.parse
53
+ logger.info resp_hash
54
+
55
+ self.bearer_token = BearerToken.new resp_hash
56
+ end
57
+
58
+ ##
59
+ # Perform a query on the TCGPlayer API
60
+ #
61
+ # @param url The API endpoint url, without arguments
62
+ # @param _params[Hash]
63
+ # - post: When true, use a post request instead of a get request
64
+ # - noretry: Override any other retry settings and skip retry if true
65
+ # - *: Additional entries in _params hash are passed through to API as arguments
66
+ #
67
+ # @return [TCGPlayerSDK::ResponseStruct]
68
+ def query(url, _params = {})
69
+ params = _params.dup
70
+ post = params.delete :post
71
+ method = post ? 'post' : 'get'
72
+ pkey = post ? :json : :params
73
+ skip_retry = params.delete(:noretry) || noretry
74
+
75
+ logger.debug "Query: #{url} params: "
76
+ logger.ap params
77
+
78
+ # Check for expiration of bearer token
79
+ response = HTTP.auth("bearer #{bearer_token ? bearer_token.token : 'none'}").headers('User-Agent' => user_agent).send(method, url, pkey => params)
80
+ ret = ResponseStruct.new response.parse.merge({base_query: {url: url, params: _params}, http_response: response, tcg_object: self})
81
+
82
+ # Detect an invalid bearer token and attempt to retry
83
+ if(!skip_retry && ret.errors && ret.errors.size > 0 && ret.errors.reduce(false){|sum, err| sum = (sum || (err =~ /bearer token/))})
84
+ # Reauthorize and try again
85
+ authorize
86
+ ret = query(url, _params.merge({noretry: true}))
87
+ end
88
+
89
+ return ret
90
+ end
91
+
92
+ ##
93
+ # Endpoint https://docs.tcgplayer.com/reference/catalog_getcategories-1
94
+ #
95
+ # @param params[Hash]
96
+ # - limit: max to return (default 10)
97
+ # - offset: number of categories to skip (for paging)
98
+ # - sort_order: property to sort by (defaults to name)
99
+ # - sort_desc: descending sort order
100
+ #
101
+ # @return [TCGPlayerSDK::ResponseStruct]
102
+ def categories(params = {})
103
+ query(CATEGORIES_URL, params)
104
+ end
105
+
106
+ ##
107
+ # https://docs.tcgplayer.com/reference/catalog_getcategory-1
108
+ #
109
+ # @return [TCGPlayerSDK::ResponseStruct]
110
+ def category_details(ids)
111
+ query("#{CATEGORIES_URL}/#{ids.join(',')}")
112
+ end
113
+
114
+ # https://docs.tcgplayer.com/reference/catalog_getcategorysearchmanifest
115
+ #
116
+ # @return [TCGPlayerSDK::ResponseStruct]
117
+ def category_search_manifest(id)
118
+ query("#{CATEGORIES_URL}/#{id}/search/manifest")
119
+ end
120
+
121
+ # https://docs.tcgplayer.com/reference/catalog_searchcategory
122
+ #
123
+ # @return [TCGPlayerSDK::ResponseStruct]
124
+ def category_search_products(id, params = {})
125
+ search_params = {post: true}.merge(params)
126
+ query("#{CATEGORIES_URL}/#{id}/search", search_params)
127
+ end
128
+
129
+ #def product_details(pids, params = {})
130
+ # query("#{CATEGORIES_URL}/#{id}/search", search_params)
131
+ #end
132
+
133
+ ##
134
+ # Accessor to https://docs.tcgplayer.com/reference/pricing_getproductprices-1
135
+ #
136
+ # @param ids An array of product IDs to query
137
+ # @return [TCGPlayerSDK::ProductPriceList]
138
+ def product_pricing(_ids)
139
+ ids = _ids.is_a?(Array) ? _ids.join(',') : _ids
140
+ TCGPlayerSDK::ProductPriceList.new(query("#{PRICING_URL}/product/#{ids}"))
141
+ end
142
+ end
@@ -0,0 +1,11 @@
1
+ require 'http'
2
+ require 'logger'
3
+ require 'amazing_print'
4
+ require 'dotenv/load'
5
+
6
+ require 'tcg-player-sdk/tcg_player_sdk'
7
+ require 'tcg-player-sdk/bearer_token'
8
+ require 'tcg-player-sdk/response_struct'
9
+ require 'tcg-player-sdk/product_price_list'
10
+ require 'tcg-player-sdk/pokemon'
11
+
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tcg-player-sdk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Carl Svensson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-02-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: http
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.0.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.7.6
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.7.6
41
+ - !ruby/object:Gem::Dependency
42
+ name: amazing_print
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.4.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.0
55
+ description: The tcg-player-sdk gem is a convenient wrapper to the TCGPlayer JSON
56
+ API. Easily query the entire API, with helpers and accessors for common queries. This
57
+ gem also provides helpers for Pokemon-specific queries. A work in progress.
58
+ email: csvenss2@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - lib/tcg-player-sdk.rb
64
+ - lib/tcg-player-sdk/bearer_token.rb
65
+ - lib/tcg-player-sdk/pokemon.rb
66
+ - lib/tcg-player-sdk/product_price_list.rb
67
+ - lib/tcg-player-sdk/response_struct.rb
68
+ - lib/tcg-player-sdk/tcg_player_sdk.rb
69
+ homepage: https://cecomp64.github.io/tcg-player-sdk/
70
+ licenses:
71
+ - MIT
72
+ metadata: {}
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.1.2
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: A Ruby interface to the TCGPlayer.com API for trading card prices.
92
+ test_files: []