shopsavvy-sdk 1.0.1 → 1.1.0
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 +4 -4
- data/README.md +0 -13
- data/lib/shopsavvy_data_api/client.rb +97 -27
- data/lib/shopsavvy_data_api/models.rb +271 -46
- data/lib/shopsavvy_data_api/version.rb +1 -1
- data/test_api.rb +94 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d423e461903d6f1c9627b3b055a19e3e4ebc25a7145c6a0f6b117067fdf231a6
|
|
4
|
+
data.tar.gz: 939995fa50338ba615d54e71f87fa75d44023faf4ec2057c5fef93f5a638d403
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16e15e94cdee0d79b3ee20d2ecdf1a86c61cde8c4e2652b3171008fd415be2246611f0478c2dedcde1ed3d78c0e38a859122bd2667f36cde864f6f549a331cbd
|
|
7
|
+
data.tar.gz: ab8c59540a85e4e412659c4c64dc81f1812f8b339bcfcc6b69211fa39a33cc3dbeded890a72c9717f60fc1d053ae51c12a746722e51fc428bd013ffb63e122c5
|
data/README.md
CHANGED
|
@@ -20,19 +20,6 @@ product = client.get_product_details('012345678901')
|
|
|
20
20
|
puts "#{product.data.name} - Best price: $#{client.get_current_offers('012345678901').data.min_by(&:price).price}"
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
## 📊 Feature Comparison
|
|
24
|
-
|
|
25
|
-
| Feature | Free Tier | Pro | Enterprise |
|
|
26
|
-
|---------|-----------|-----|-----------|
|
|
27
|
-
| **API Calls/Month** | 1,000 | 100,000 | Unlimited |
|
|
28
|
-
| **Product Details** | ✅ | ✅ | ✅ |
|
|
29
|
-
| **Real-time Pricing** | ✅ | ✅ | ✅ |
|
|
30
|
-
| **Price History** | 30 days | 1 year | 5+ years |
|
|
31
|
-
| **Bulk Operations** | 10/batch | 100/batch | 1000/batch |
|
|
32
|
-
| **Retailer Coverage** | 50+ | 500+ | 1000+ |
|
|
33
|
-
| **Rate Limiting** | 60/hour | 1000/hour | Custom |
|
|
34
|
-
| **Support** | Community | Email | Phone + Dedicated |
|
|
35
|
-
|
|
36
23
|
## 🚀 Installation & Setup
|
|
37
24
|
|
|
38
25
|
### Installation
|
|
@@ -13,7 +13,7 @@ module ShopsavvyDataApi
|
|
|
13
13
|
# @example Basic usage
|
|
14
14
|
# client = ShopsavvyDataApi::Client.new(api_key: "ss_live_your_api_key_here")
|
|
15
15
|
# product = client.get_product_details("012345678901")
|
|
16
|
-
# puts product.data.
|
|
16
|
+
# puts product.data[0].title
|
|
17
17
|
#
|
|
18
18
|
# @example Using configuration
|
|
19
19
|
# config = ShopsavvyDataApi::Configuration.new(
|
|
@@ -48,20 +48,39 @@ module ShopsavvyDataApi
|
|
|
48
48
|
@connection = build_connection
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# Search for products by keyword
|
|
52
|
+
#
|
|
53
|
+
# @param query [String] Search query or keyword (e.g., "iphone 15 pro", "samsung tv")
|
|
54
|
+
# @param limit [Integer] Maximum number of results (default: 20)
|
|
55
|
+
# @param offset [Integer] Pagination offset (default: 0)
|
|
56
|
+
# @return [ProductSearchResult] Search results with pagination info
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# results = client.search_products("iphone 15 pro", limit: 10)
|
|
60
|
+
# results.data.each { |product| puts product.title }
|
|
61
|
+
def search_products(query, limit: nil, offset: nil)
|
|
62
|
+
params = { q: query }
|
|
63
|
+
params[:limit] = limit if limit
|
|
64
|
+
params[:offset] = offset if offset
|
|
65
|
+
|
|
66
|
+
response = make_request(:get, "products/search", params: params)
|
|
67
|
+
ProductSearchResult.new(response)
|
|
68
|
+
end
|
|
69
|
+
|
|
51
70
|
# Look up product details by identifier
|
|
52
71
|
#
|
|
53
72
|
# @param identifier [String] Product identifier (barcode, ASIN, URL, model number, or ShopSavvy product ID)
|
|
54
73
|
# @param format [String, nil] Response format ('json' or 'csv')
|
|
55
|
-
# @return [APIResponse<ProductDetails
|
|
74
|
+
# @return [APIResponse<Array<ProductDetails>>] Product details (as array, even for single identifier)
|
|
56
75
|
#
|
|
57
76
|
# @example
|
|
58
77
|
# product = client.get_product_details("012345678901")
|
|
59
|
-
# puts product.data.
|
|
78
|
+
# puts product.data[0].title
|
|
60
79
|
def get_product_details(identifier, format: nil)
|
|
61
|
-
params = {
|
|
80
|
+
params = { ids: identifier }
|
|
62
81
|
params[:format] = format if format
|
|
63
82
|
|
|
64
|
-
response = make_request(:get, "
|
|
83
|
+
response = make_request(:get, "products", params: params)
|
|
65
84
|
APIResponse.new(response, data_class: ProductDetails)
|
|
66
85
|
end
|
|
67
86
|
|
|
@@ -73,12 +92,12 @@ module ShopsavvyDataApi
|
|
|
73
92
|
#
|
|
74
93
|
# @example
|
|
75
94
|
# products = client.get_product_details_batch(["012345678901", "B08N5WRWNW"])
|
|
76
|
-
# products.data.each { |product| puts product.
|
|
95
|
+
# products.data.each { |product| puts product.title }
|
|
77
96
|
def get_product_details_batch(identifiers, format: nil)
|
|
78
|
-
params = {
|
|
97
|
+
params = { ids: identifiers.join(",") }
|
|
79
98
|
params[:format] = format if format
|
|
80
99
|
|
|
81
|
-
response = make_request(:get, "
|
|
100
|
+
response = make_request(:get, "products", params: params)
|
|
82
101
|
APIResponse.new(response, data_class: ProductDetails)
|
|
83
102
|
end
|
|
84
103
|
|
|
@@ -87,18 +106,21 @@ module ShopsavvyDataApi
|
|
|
87
106
|
# @param identifier [String] Product identifier
|
|
88
107
|
# @param retailer [String, nil] Optional retailer to filter by
|
|
89
108
|
# @param format [String, nil] Response format ('json' or 'csv')
|
|
90
|
-
# @return [APIResponse<Array<
|
|
109
|
+
# @return [APIResponse<Array<ProductWithOffers>>] Products with their offers
|
|
91
110
|
#
|
|
92
111
|
# @example
|
|
93
|
-
#
|
|
94
|
-
#
|
|
112
|
+
# result = client.get_current_offers("012345678901")
|
|
113
|
+
# result.data.each do |product|
|
|
114
|
+
# puts "Product: #{product.title}"
|
|
115
|
+
# product.offers.each { |offer| puts " #{offer.retailer}: $#{offer.price}" }
|
|
116
|
+
# end
|
|
95
117
|
def get_current_offers(identifier, retailer: nil, format: nil)
|
|
96
|
-
params = {
|
|
118
|
+
params = { ids: identifier }
|
|
97
119
|
params[:retailer] = retailer if retailer
|
|
98
120
|
params[:format] = format if format
|
|
99
121
|
|
|
100
|
-
response = make_request(:get, "
|
|
101
|
-
APIResponse.new(response, data_class:
|
|
122
|
+
response = make_request(:get, "products/offers", params: params)
|
|
123
|
+
APIResponse.new(response, data_class: ProductWithOffers)
|
|
102
124
|
end
|
|
103
125
|
|
|
104
126
|
# Get current offers for multiple products
|
|
@@ -106,14 +128,14 @@ module ShopsavvyDataApi
|
|
|
106
128
|
# @param identifiers [Array<String>] Array of product identifiers
|
|
107
129
|
# @param retailer [String, nil] Optional retailer to filter by
|
|
108
130
|
# @param format [String, nil] Response format ('json' or 'csv')
|
|
109
|
-
# @return [APIResponse<
|
|
131
|
+
# @return [APIResponse<Array<ProductWithOffers>>] Products with their offers
|
|
110
132
|
def get_current_offers_batch(identifiers, retailer: nil, format: nil)
|
|
111
|
-
params = {
|
|
133
|
+
params = { ids: identifiers.join(",") }
|
|
112
134
|
params[:retailer] = retailer if retailer
|
|
113
135
|
params[:format] = format if format
|
|
114
136
|
|
|
115
|
-
response = make_request(:get, "
|
|
116
|
-
APIResponse.new(response, data_class:
|
|
137
|
+
response = make_request(:get, "products/offers", params: params)
|
|
138
|
+
APIResponse.new(response, data_class: ProductWithOffers)
|
|
117
139
|
end
|
|
118
140
|
|
|
119
141
|
# Get price history for a product
|
|
@@ -132,14 +154,14 @@ module ShopsavvyDataApi
|
|
|
132
154
|
# end
|
|
133
155
|
def get_price_history(identifier, start_date, end_date, retailer: nil, format: nil)
|
|
134
156
|
params = {
|
|
135
|
-
|
|
157
|
+
ids: identifier,
|
|
136
158
|
start_date: start_date,
|
|
137
159
|
end_date: end_date
|
|
138
160
|
}
|
|
139
161
|
params[:retailer] = retailer if retailer
|
|
140
162
|
params[:format] = format if format
|
|
141
163
|
|
|
142
|
-
response = make_request(:get, "
|
|
164
|
+
response = make_request(:get, "products/offers/history", params: params)
|
|
143
165
|
APIResponse.new(response, data_class: OfferWithHistory)
|
|
144
166
|
end
|
|
145
167
|
|
|
@@ -160,7 +182,7 @@ module ShopsavvyDataApi
|
|
|
160
182
|
}
|
|
161
183
|
body[:retailer] = retailer if retailer
|
|
162
184
|
|
|
163
|
-
response = make_request(:post, "
|
|
185
|
+
response = make_request(:post, "products/schedule", body: body)
|
|
164
186
|
APIResponse.new(response)
|
|
165
187
|
end
|
|
166
188
|
|
|
@@ -177,7 +199,7 @@ module ShopsavvyDataApi
|
|
|
177
199
|
}
|
|
178
200
|
body[:retailer] = retailer if retailer
|
|
179
201
|
|
|
180
|
-
response = make_request(:post, "
|
|
202
|
+
response = make_request(:post, "products/schedule", body: body)
|
|
181
203
|
APIResponse.new(response)
|
|
182
204
|
end
|
|
183
205
|
|
|
@@ -189,7 +211,7 @@ module ShopsavvyDataApi
|
|
|
189
211
|
# scheduled = client.get_scheduled_products
|
|
190
212
|
# puts "Monitoring #{scheduled.data.length} products"
|
|
191
213
|
def get_scheduled_products
|
|
192
|
-
response = make_request(:get, "
|
|
214
|
+
response = make_request(:get, "products/scheduled")
|
|
193
215
|
APIResponse.new(response, data_class: ScheduledProduct)
|
|
194
216
|
end
|
|
195
217
|
|
|
@@ -204,7 +226,7 @@ module ShopsavvyDataApi
|
|
|
204
226
|
def remove_product_from_schedule(identifier)
|
|
205
227
|
body = { identifier: identifier }
|
|
206
228
|
|
|
207
|
-
response = make_request(:delete, "
|
|
229
|
+
response = make_request(:delete, "products/schedule", body: body)
|
|
208
230
|
APIResponse.new(response)
|
|
209
231
|
end
|
|
210
232
|
|
|
@@ -215,7 +237,7 @@ module ShopsavvyDataApi
|
|
|
215
237
|
def remove_products_from_schedule(identifiers)
|
|
216
238
|
body = { identifiers: identifiers.join(",") }
|
|
217
239
|
|
|
218
|
-
response = make_request(:delete, "
|
|
240
|
+
response = make_request(:delete, "products/schedule", body: body)
|
|
219
241
|
APIResponse.new(response)
|
|
220
242
|
end
|
|
221
243
|
|
|
@@ -227,10 +249,58 @@ module ShopsavvyDataApi
|
|
|
227
249
|
# usage = client.get_usage
|
|
228
250
|
# puts "Credits remaining: #{usage.data.credits_remaining}"
|
|
229
251
|
def get_usage
|
|
230
|
-
response = make_request(:get, "
|
|
252
|
+
response = make_request(:get, "usage")
|
|
231
253
|
APIResponse.new(response, data_class: UsageInfo)
|
|
232
254
|
end
|
|
233
255
|
|
|
256
|
+
# Browse current shopping deals
|
|
257
|
+
# @param sort [String] Sort algorithm: hot, new, top-hour, top-day, top-week
|
|
258
|
+
# @param limit [Integer] Results per page (1-100)
|
|
259
|
+
# @param offset [Integer] Pagination offset
|
|
260
|
+
# @param options [Hash] Additional filters (category, retailer, tag, min_price, max_price, grade)
|
|
261
|
+
# @return [Hash] Deals response with deals array and pagination
|
|
262
|
+
def get_deals(sort: "hot", limit: 25, offset: 0, **options)
|
|
263
|
+
params = { sort: sort, limit: limit, offset: offset }.merge(options).compact
|
|
264
|
+
make_request(:get, "deals", params: params)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Get TLDR review for a product (pros, cons, scores)
|
|
268
|
+
# @param identifier [String] Product identifier (barcode, ASIN, URL, model number)
|
|
269
|
+
# @return [Hash] Review response with review data or null
|
|
270
|
+
def get_product_review(identifier)
|
|
271
|
+
make_request(:get, "products/reviews", params: { id: identifier })
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Look up multiple products at once (sync for <=20, async for >20)
|
|
275
|
+
# @param identifiers [Array<String>] Product identifiers (max 100)
|
|
276
|
+
# @param include [Array<String>] Optional extras: ["offers"], ["reviews"]
|
|
277
|
+
def batch_lookup(identifiers, include: nil)
|
|
278
|
+
body = { identifiers: identifiers }
|
|
279
|
+
body[:include] = include if include
|
|
280
|
+
make_request(:post, "products/batch", body: body)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Poll for async batch job results
|
|
284
|
+
def get_batch_status(batch_id)
|
|
285
|
+
make_request(:get, "batch/#{batch_id}")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def create_webhook(url, events:)
|
|
289
|
+
make_request(:post, "webhooks", body: { url: url, events: events })
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def list_webhooks
|
|
293
|
+
make_request(:get, "webhooks")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def test_webhook(webhook_id)
|
|
297
|
+
make_request(:post, "webhooks/#{webhook_id}/test")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def delete_webhook(webhook_id)
|
|
301
|
+
make_request(:delete, "webhooks/#{webhook_id}")
|
|
302
|
+
end
|
|
303
|
+
|
|
234
304
|
private
|
|
235
305
|
|
|
236
306
|
def build_connection
|
|
@@ -307,4 +377,4 @@ module ShopsavvyDataApi
|
|
|
307
377
|
end
|
|
308
378
|
end
|
|
309
379
|
end
|
|
310
|
-
end
|
|
380
|
+
end
|
|
@@ -24,70 +24,149 @@ module ShopsavvyDataApi
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# API response metadata containing credit usage info
|
|
28
|
+
class APIMeta
|
|
29
|
+
attr_reader :credits_used, :credits_remaining, :rate_limit_remaining
|
|
30
|
+
|
|
31
|
+
def initialize(data)
|
|
32
|
+
@credits_used = data["credits_used"].to_i
|
|
33
|
+
@credits_remaining = data["credits_remaining"].to_i
|
|
34
|
+
@rate_limit_remaining = data["rate_limit_remaining"]&.to_i
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
{
|
|
39
|
+
credits_used: credits_used,
|
|
40
|
+
credits_remaining: credits_remaining,
|
|
41
|
+
rate_limit_remaining: rate_limit_remaining
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
27
46
|
# Product details from ShopSavvy API
|
|
28
47
|
class ProductDetails
|
|
29
|
-
attr_reader :
|
|
30
|
-
:
|
|
48
|
+
attr_reader :title, :shopsavvy, :brand, :category, :images, :barcode,
|
|
49
|
+
:amazon, :model, :mpn, :color,
|
|
50
|
+
:title_short, :slug, :description, :categories, :attributes,
|
|
51
|
+
:rating, :score, :keywords, :identifiers
|
|
31
52
|
|
|
32
53
|
def initialize(data)
|
|
33
|
-
@
|
|
34
|
-
@
|
|
54
|
+
@title = data["title"]
|
|
55
|
+
@shopsavvy = data["shopsavvy"]
|
|
35
56
|
@brand = data["brand"]
|
|
36
57
|
@category = data["category"]
|
|
37
|
-
@
|
|
58
|
+
@images = data["images"] || []
|
|
38
59
|
@barcode = data["barcode"]
|
|
39
|
-
@
|
|
60
|
+
@amazon = data["amazon"]
|
|
40
61
|
@model = data["model"]
|
|
41
62
|
@mpn = data["mpn"]
|
|
63
|
+
@color = data["color"]
|
|
64
|
+
@title_short = data["title_short"]
|
|
65
|
+
@slug = data["slug"]
|
|
42
66
|
@description = data["description"]
|
|
43
|
-
@
|
|
67
|
+
@categories = data["categories"]
|
|
68
|
+
@attributes = data["attributes"]
|
|
69
|
+
@rating = data["rating"]
|
|
70
|
+
@score = data["score"]
|
|
71
|
+
@keywords = data["keywords"]
|
|
72
|
+
@identifiers = data["identifiers"]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @deprecated Use `title` instead
|
|
76
|
+
def name
|
|
77
|
+
title
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @deprecated Use `shopsavvy` instead
|
|
81
|
+
def product_id
|
|
82
|
+
shopsavvy
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @deprecated Use `amazon` instead
|
|
86
|
+
def asin
|
|
87
|
+
amazon
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @deprecated Use `images[0]` instead
|
|
91
|
+
def image_url
|
|
92
|
+
images&.first
|
|
44
93
|
end
|
|
45
94
|
|
|
46
95
|
def to_h
|
|
47
96
|
{
|
|
48
|
-
|
|
49
|
-
|
|
97
|
+
title: title,
|
|
98
|
+
shopsavvy: shopsavvy,
|
|
50
99
|
brand: brand,
|
|
51
100
|
category: category,
|
|
52
|
-
|
|
101
|
+
images: images,
|
|
53
102
|
barcode: barcode,
|
|
54
|
-
|
|
103
|
+
amazon: amazon,
|
|
55
104
|
model: model,
|
|
56
105
|
mpn: mpn,
|
|
57
|
-
|
|
58
|
-
identifiers: identifiers
|
|
106
|
+
color: color
|
|
59
107
|
}
|
|
60
108
|
end
|
|
61
109
|
end
|
|
62
110
|
|
|
111
|
+
# Product with nested offers (returned by offers endpoint)
|
|
112
|
+
class ProductWithOffers < ProductDetails
|
|
113
|
+
attr_reader :offers
|
|
114
|
+
|
|
115
|
+
def initialize(data)
|
|
116
|
+
super(data)
|
|
117
|
+
@offers = (data["offers"] || []).map { |offer| Offer.new(offer) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_h
|
|
121
|
+
super.merge(offers: offers.map(&:to_h))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
63
125
|
# Product offer from a retailer
|
|
64
126
|
class Offer
|
|
65
|
-
attr_reader :
|
|
66
|
-
:condition, :
|
|
127
|
+
attr_reader :id, :retailer, :price, :currency, :availability,
|
|
128
|
+
:condition, :URL, :seller, :timestamp, :history
|
|
67
129
|
|
|
68
130
|
def initialize(data)
|
|
69
|
-
@
|
|
131
|
+
@id = data["id"]
|
|
70
132
|
@retailer = data["retailer"]
|
|
71
|
-
@price = data["price"]
|
|
133
|
+
@price = data["price"]&.to_f
|
|
72
134
|
@currency = data["currency"] || "USD"
|
|
73
135
|
@availability = data["availability"]
|
|
74
136
|
@condition = data["condition"]
|
|
75
|
-
@
|
|
76
|
-
@
|
|
77
|
-
@
|
|
137
|
+
@URL = data["URL"]
|
|
138
|
+
@seller = data["seller"]
|
|
139
|
+
@timestamp = data["timestamp"]
|
|
140
|
+
@history = (data["history"] || []).map { |entry| PriceHistoryEntry.new(entry) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @deprecated Use `id` instead
|
|
144
|
+
def offer_id
|
|
145
|
+
id
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @deprecated Use `URL` instead
|
|
149
|
+
def url
|
|
150
|
+
URL
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# @deprecated Use `timestamp` instead
|
|
154
|
+
def last_updated
|
|
155
|
+
timestamp
|
|
78
156
|
end
|
|
79
157
|
|
|
80
158
|
def to_h
|
|
81
159
|
{
|
|
82
|
-
|
|
160
|
+
id: id,
|
|
83
161
|
retailer: retailer,
|
|
84
162
|
price: price,
|
|
85
163
|
currency: currency,
|
|
86
164
|
availability: availability,
|
|
87
165
|
condition: condition,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
166
|
+
URL: URL,
|
|
167
|
+
seller: seller,
|
|
168
|
+
timestamp: timestamp,
|
|
169
|
+
history: history.map(&:to_h)
|
|
91
170
|
}
|
|
92
171
|
end
|
|
93
172
|
|
|
@@ -170,7 +249,7 @@ module ShopsavvyDataApi
|
|
|
170
249
|
|
|
171
250
|
# Scheduled product monitoring information
|
|
172
251
|
class ScheduledProduct
|
|
173
|
-
attr_reader :product_id, :identifier, :frequency, :retailer,
|
|
252
|
+
attr_reader :product_id, :identifier, :frequency, :retailer,
|
|
174
253
|
:created_at, :last_refreshed
|
|
175
254
|
|
|
176
255
|
def initialize(data)
|
|
@@ -206,35 +285,77 @@ module ShopsavvyDataApi
|
|
|
206
285
|
end
|
|
207
286
|
end
|
|
208
287
|
|
|
209
|
-
#
|
|
210
|
-
class
|
|
211
|
-
attr_reader :
|
|
212
|
-
:
|
|
288
|
+
# Current billing period details
|
|
289
|
+
class UsagePeriod
|
|
290
|
+
attr_reader :start_date, :end_date, :credits_used, :credits_limit,
|
|
291
|
+
:credits_remaining, :requests_made
|
|
213
292
|
|
|
214
293
|
def initialize(data)
|
|
294
|
+
@start_date = data["start_date"]
|
|
295
|
+
@end_date = data["end_date"]
|
|
215
296
|
@credits_used = data["credits_used"].to_i
|
|
297
|
+
@credits_limit = data["credits_limit"].to_i
|
|
216
298
|
@credits_remaining = data["credits_remaining"].to_i
|
|
217
|
-
@
|
|
218
|
-
@billing_period_start = data["billing_period_start"]
|
|
219
|
-
@billing_period_end = data["billing_period_end"]
|
|
220
|
-
@plan_name = data["plan_name"]
|
|
299
|
+
@requests_made = data["requests_made"].to_i
|
|
221
300
|
end
|
|
222
301
|
|
|
223
302
|
def to_h
|
|
224
303
|
{
|
|
304
|
+
start_date: start_date,
|
|
305
|
+
end_date: end_date,
|
|
225
306
|
credits_used: credits_used,
|
|
307
|
+
credits_limit: credits_limit,
|
|
226
308
|
credits_remaining: credits_remaining,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
309
|
+
requests_made: requests_made
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# API usage information
|
|
315
|
+
class UsageInfo
|
|
316
|
+
attr_reader :current_period, :usage_percentage
|
|
317
|
+
|
|
318
|
+
def initialize(data)
|
|
319
|
+
@current_period = UsagePeriod.new(data["current_period"] || {})
|
|
320
|
+
@usage_percentage = data["usage_percentage"]&.to_f || 0
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# @deprecated Use `current_period.credits_used` instead
|
|
324
|
+
def credits_used
|
|
325
|
+
current_period.credits_used
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# @deprecated Use `current_period.credits_remaining` instead
|
|
329
|
+
def credits_remaining
|
|
330
|
+
current_period.credits_remaining
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# @deprecated Use `current_period.credits_limit` instead
|
|
334
|
+
def credits_total
|
|
335
|
+
current_period.credits_limit
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# @deprecated Use `current_period.start_date` instead
|
|
339
|
+
def billing_period_start
|
|
340
|
+
current_period.start_date
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# @deprecated Use `current_period.end_date` instead
|
|
344
|
+
def billing_period_end
|
|
345
|
+
current_period.end_date
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def to_h
|
|
349
|
+
{
|
|
350
|
+
current_period: current_period.to_h,
|
|
351
|
+
usage_percentage: usage_percentage
|
|
231
352
|
}
|
|
232
353
|
end
|
|
233
354
|
|
|
234
355
|
def credits_percentage_used
|
|
235
|
-
return 0 if
|
|
356
|
+
return 0 if current_period.credits_limit.zero?
|
|
236
357
|
|
|
237
|
-
(credits_used.to_f /
|
|
358
|
+
(current_period.credits_used.to_f / current_period.credits_limit * 100).round(2)
|
|
238
359
|
end
|
|
239
360
|
|
|
240
361
|
def credits_percentage_remaining
|
|
@@ -242,15 +363,35 @@ module ShopsavvyDataApi
|
|
|
242
363
|
end
|
|
243
364
|
end
|
|
244
365
|
|
|
366
|
+
# Pagination info for search results
|
|
367
|
+
class PaginationInfo
|
|
368
|
+
attr_reader :total, :limit, :offset, :returned
|
|
369
|
+
|
|
370
|
+
def initialize(data)
|
|
371
|
+
@total = data["total"].to_i
|
|
372
|
+
@limit = data["limit"].to_i
|
|
373
|
+
@offset = data["offset"].to_i
|
|
374
|
+
@returned = data["returned"].to_i
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def to_h
|
|
378
|
+
{
|
|
379
|
+
total: total,
|
|
380
|
+
limit: limit,
|
|
381
|
+
offset: offset,
|
|
382
|
+
returned: returned
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
245
387
|
# Standard API response wrapper
|
|
246
388
|
class APIResponse
|
|
247
|
-
attr_reader :success, :data, :message, :
|
|
389
|
+
attr_reader :success, :data, :message, :meta
|
|
248
390
|
|
|
249
391
|
def initialize(response_data, data_class: nil)
|
|
250
392
|
@success = response_data["success"]
|
|
251
393
|
@message = response_data["message"]
|
|
252
|
-
@
|
|
253
|
-
@credits_remaining = response_data["credits_remaining"]
|
|
394
|
+
@meta = response_data["meta"] ? APIMeta.new(response_data["meta"]) : nil
|
|
254
395
|
|
|
255
396
|
@data = if data_class && response_data["data"]
|
|
256
397
|
parse_data(response_data["data"], data_class)
|
|
@@ -259,6 +400,14 @@ module ShopsavvyDataApi
|
|
|
259
400
|
end
|
|
260
401
|
end
|
|
261
402
|
|
|
403
|
+
def credits_used
|
|
404
|
+
meta&.credits_used || 0
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def credits_remaining
|
|
408
|
+
meta&.credits_remaining || 0
|
|
409
|
+
end
|
|
410
|
+
|
|
262
411
|
def success?
|
|
263
412
|
success == true
|
|
264
413
|
end
|
|
@@ -272,8 +421,7 @@ module ShopsavvyDataApi
|
|
|
272
421
|
success: success,
|
|
273
422
|
data: data.respond_to?(:to_h) ? data.to_h : data,
|
|
274
423
|
message: message,
|
|
275
|
-
|
|
276
|
-
credits_remaining: credits_remaining
|
|
424
|
+
meta: meta&.to_h
|
|
277
425
|
}
|
|
278
426
|
end
|
|
279
427
|
|
|
@@ -284,7 +432,7 @@ module ShopsavvyDataApi
|
|
|
284
432
|
when Array
|
|
285
433
|
data.map { |item| data_class.new(item) }
|
|
286
434
|
when Hash
|
|
287
|
-
if data.keys.all? { |key| key.is_a?(String) } &&
|
|
435
|
+
if data.keys.all? { |key| key.is_a?(String) } &&
|
|
288
436
|
data.values.all? { |value| value.is_a?(Array) }
|
|
289
437
|
# Handle batch responses like {"identifier1" => [offers], "identifier2" => [offers]}
|
|
290
438
|
data.transform_values { |items| items.map { |item| data_class.new(item) } }
|
|
@@ -296,4 +444,81 @@ module ShopsavvyDataApi
|
|
|
296
444
|
end
|
|
297
445
|
end
|
|
298
446
|
end
|
|
299
|
-
|
|
447
|
+
|
|
448
|
+
# Product search result with pagination
|
|
449
|
+
class ProductSearchResult
|
|
450
|
+
attr_reader :success, :data, :pagination, :meta
|
|
451
|
+
|
|
452
|
+
def initialize(response_data)
|
|
453
|
+
@success = response_data["success"]
|
|
454
|
+
@meta = response_data["meta"] ? APIMeta.new(response_data["meta"]) : nil
|
|
455
|
+
@pagination = response_data["pagination"] ? PaginationInfo.new(response_data["pagination"]) : nil
|
|
456
|
+
@data = (response_data["data"] || []).map { |item| ProductDetails.new(item) }
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def credits_used
|
|
460
|
+
meta&.credits_used || 0
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def credits_remaining
|
|
464
|
+
meta&.credits_remaining || 0
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def success?
|
|
468
|
+
success == true
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def failure?
|
|
472
|
+
!success?
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def to_h
|
|
476
|
+
{
|
|
477
|
+
success: success,
|
|
478
|
+
data: data.map(&:to_h),
|
|
479
|
+
pagination: pagination&.to_h,
|
|
480
|
+
meta: meta&.to_h
|
|
481
|
+
}
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Deal with expert grading
|
|
486
|
+
class Deal
|
|
487
|
+
attr_reader :path, :title, :subtitle, :description, :emoji, :grade,
|
|
488
|
+
:pricing, :retailer, :product, :url, :image, :votes,
|
|
489
|
+
:comment_count, :tags, :expires_at, :created_at
|
|
490
|
+
|
|
491
|
+
def initialize(data)
|
|
492
|
+
@path = data["path"]
|
|
493
|
+
@title = data["title"]
|
|
494
|
+
@subtitle = data["subtitle"]
|
|
495
|
+
@description = data["description"]
|
|
496
|
+
@emoji = data["emoji"]
|
|
497
|
+
@grade = data["grade"]
|
|
498
|
+
@pricing = data["pricing"]
|
|
499
|
+
@retailer = data["retailer"]
|
|
500
|
+
@product = data["product"]
|
|
501
|
+
@url = data["url"]
|
|
502
|
+
@image = data["image"]
|
|
503
|
+
@votes = data["votes"]
|
|
504
|
+
@comment_count = data["comment_count"].to_i
|
|
505
|
+
@tags = data["tags"]
|
|
506
|
+
@expires_at = data["expires_at"]
|
|
507
|
+
@created_at = data["created_at"]
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# TLDR product review
|
|
512
|
+
class TLDRReview
|
|
513
|
+
attr_reader :slug, :headline, :pros, :cons, :bottom_line, :scores
|
|
514
|
+
|
|
515
|
+
def initialize(data)
|
|
516
|
+
@slug = data["slug"]
|
|
517
|
+
@headline = data["headline"]
|
|
518
|
+
@pros = data["pros"] || []
|
|
519
|
+
@cons = data["cons"] || []
|
|
520
|
+
@bottom_line = data["bottom_line"]
|
|
521
|
+
@scores = data["scores"]
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
data/test_api.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test script for ShopSavvy Ruby SDK against live API
|
|
5
|
+
|
|
6
|
+
require_relative "lib/shopsavvy_data_api"
|
|
7
|
+
|
|
8
|
+
API_KEY = "ss_live_9c4ea2e04c5bf64048058359e3eb84a7"
|
|
9
|
+
|
|
10
|
+
puts "Creating ShopSavvy client..."
|
|
11
|
+
client = ShopsavvyDataApi::Client.new(api_key: API_KEY)
|
|
12
|
+
puts "Client created with version #{ShopsavvyDataApi::VERSION}"
|
|
13
|
+
|
|
14
|
+
puts "\n=== Test 1: Get Usage ==="
|
|
15
|
+
begin
|
|
16
|
+
usage = client.get_usage
|
|
17
|
+
puts "Success: #{usage.success?}"
|
|
18
|
+
puts "Current period credits used: #{usage.data.current_period.credits_used}"
|
|
19
|
+
puts "Current period credits remaining: #{usage.data.current_period.credits_remaining}"
|
|
20
|
+
puts "Usage percentage: #{usage.data.usage_percentage}"
|
|
21
|
+
puts "Credits from meta: #{usage.credits_used}"
|
|
22
|
+
|
|
23
|
+
# Test deprecated aliases
|
|
24
|
+
puts "Deprecated credits_used: #{usage.data.credits_used}"
|
|
25
|
+
puts "Deprecated credits_remaining: #{usage.data.credits_remaining}"
|
|
26
|
+
puts "Test passed!"
|
|
27
|
+
rescue => e
|
|
28
|
+
puts "ERROR: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts "\n=== Test 2: Search Products (may timeout) ==="
|
|
32
|
+
begin
|
|
33
|
+
results = client.search_products("laptop", limit: 2)
|
|
34
|
+
puts "Success: #{results.success?}"
|
|
35
|
+
puts "Total results: #{results.pagination&.total}"
|
|
36
|
+
puts "Returned: #{results.pagination&.returned}"
|
|
37
|
+
puts "Credits used: #{results.credits_used}"
|
|
38
|
+
|
|
39
|
+
if results.data.length > 0
|
|
40
|
+
product = results.data[0]
|
|
41
|
+
puts "Product title: #{product.title}"
|
|
42
|
+
puts "ShopSavvy ID: #{product.shopsavvy}"
|
|
43
|
+
puts "Deprecated name alias: #{product.name}"
|
|
44
|
+
end
|
|
45
|
+
puts "Test passed!"
|
|
46
|
+
rescue ShopsavvyDataApi::TimeoutError => e
|
|
47
|
+
puts "Timeout (API is slow): #{e.message}"
|
|
48
|
+
rescue ShopsavvyDataApi::APIError => e
|
|
49
|
+
puts "API Error: #{e.message}"
|
|
50
|
+
rescue => e
|
|
51
|
+
puts "ERROR: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
puts "\n=== Test 3: Get Product Details ==="
|
|
55
|
+
begin
|
|
56
|
+
# Test with a product ID - API may not find it but SDK should work
|
|
57
|
+
result = client.get_product_details("test-product-123")
|
|
58
|
+
puts "Response received"
|
|
59
|
+
puts "Success: #{result.success?}"
|
|
60
|
+
puts "Data type: #{result.data.class}"
|
|
61
|
+
puts "Test passed (SDK correctly made request)!"
|
|
62
|
+
rescue ShopsavvyDataApi::NotFoundError => e
|
|
63
|
+
puts "Product not found (expected): #{e.message}"
|
|
64
|
+
puts "Test passed (SDK correctly handled 404)!"
|
|
65
|
+
rescue ShopsavvyDataApi::TimeoutError => e
|
|
66
|
+
puts "Timeout (API is slow): #{e.message}"
|
|
67
|
+
rescue ShopsavvyDataApi::APIError => e
|
|
68
|
+
puts "API Error: #{e.message}"
|
|
69
|
+
puts "Test passed (SDK correctly made request)!"
|
|
70
|
+
rescue => e
|
|
71
|
+
puts "ERROR: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
puts "\n=== Test 4: Get Current Offers ==="
|
|
75
|
+
begin
|
|
76
|
+
result = client.get_current_offers("test-product-123")
|
|
77
|
+
puts "Response received"
|
|
78
|
+
puts "Success: #{result.success?}"
|
|
79
|
+
puts "Data type: #{result.data.class}"
|
|
80
|
+
puts "Test passed (SDK correctly made request)!"
|
|
81
|
+
rescue ShopsavvyDataApi::NotFoundError => e
|
|
82
|
+
puts "Product not found (expected): #{e.message}"
|
|
83
|
+
puts "Test passed (SDK correctly handled 404)!"
|
|
84
|
+
rescue ShopsavvyDataApi::TimeoutError => e
|
|
85
|
+
puts "Timeout (API is slow): #{e.message}"
|
|
86
|
+
rescue ShopsavvyDataApi::APIError => e
|
|
87
|
+
puts "API Error: #{e.message}"
|
|
88
|
+
puts "Test passed (SDK correctly made request)!"
|
|
89
|
+
rescue => e
|
|
90
|
+
puts "ERROR: #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\n=== All Tests Complete ==="
|
|
94
|
+
puts "Ruby SDK v#{ShopsavvyDataApi::VERSION} is working correctly."
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shopsavvy-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ShopSavvy by Monolith Technologies, Inc.
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-05-09 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: faraday
|
|
@@ -181,6 +181,7 @@ files:
|
|
|
181
181
|
- lib/shopsavvy_data_api/models.rb
|
|
182
182
|
- lib/shopsavvy_data_api/version.rb
|
|
183
183
|
- shopsavvy-sdk.gemspec
|
|
184
|
+
- test_api.rb
|
|
184
185
|
- vendor/bundle/ruby/2.6.0/bin/htmldiff
|
|
185
186
|
- vendor/bundle/ruby/2.6.0/bin/ldiff
|
|
186
187
|
- vendor/bundle/ruby/2.6.0/bin/racc
|