shoptet 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +88 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/shoptet.rb +206 -0
- data/lib/shoptet/request.rb +32 -0
- data/shoptet.gemspec +26 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5707a03aee00c4da7081d5240e584e722e3acd3223a4e0a58c224c98b9a7dc77
|
4
|
+
data.tar.gz: d58ec4a17e917631a70bc2e662f5f16edeaf321b2a3f77df8a63450c17995380
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4b0e431494bcbc05f7c75c8cf50e298cfece53d8421c74f63fcd424dd7efdaa6e4ed44d7bfc8c2a5846d2819eab620933bfa85700613bf83673fc00f4381da93
|
7
|
+
data.tar.gz: 372d9d14962d9aefc387678197c7db28b95ccf9717a41a6517bd5043c1fe0a86b462f1935d2bf6c6fd261ffd637a8ef025500f1c6f2da9ee6d5ce2ba6a2c45a8
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
shoptet (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
io-console (0.5.6)
|
10
|
+
irb (1.2.7)
|
11
|
+
reline (>= 0.1.5)
|
12
|
+
reline (0.1.6)
|
13
|
+
io-console (~> 0.5)
|
14
|
+
|
15
|
+
PLATFORMS
|
16
|
+
ruby
|
17
|
+
|
18
|
+
DEPENDENCIES
|
19
|
+
irb
|
20
|
+
shoptet!
|
21
|
+
|
22
|
+
BUNDLED WITH
|
23
|
+
2.1.4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Premysl Donat
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Shoptet
|
2
|
+
|
3
|
+
This is Ruby API wrapper for [Shoptet API](https://developers.shoptet.com/) which provides access to e-shop data for addon developers.
|
4
|
+
|
5
|
+
This is currently under development and hasn't been released to Rubygems yet.
|
6
|
+
|
7
|
+
# How to install
|
8
|
+
|
9
|
+
Currently only through Github:
|
10
|
+
```
|
11
|
+
gem 'shoptet', github: 'Masa331/shoptet'
|
12
|
+
```
|
13
|
+
|
14
|
+
# How to use
|
15
|
+
|
16
|
+
## Setup
|
17
|
+
|
18
|
+
First instantiate Shoptet which then represents connection to one specific shop
|
19
|
+
```
|
20
|
+
api = Shoptet.new(oauth_url, oauth_token, api_token, on_token_error)
|
21
|
+
```
|
22
|
+
And now you can access the data
|
23
|
+
```
|
24
|
+
api.price_lists
|
25
|
+
# => returns Enumerator with all price lists
|
26
|
+
|
27
|
+
```
|
28
|
+
### params for `::new`
|
29
|
+
|
30
|
+
#### * oauth_url
|
31
|
+
string with the oauth url for the partner shop under which is the addon registered.
|
32
|
+
|
33
|
+
#### oauth_token
|
34
|
+
string or nil with oauth token(the one you get during addon instalation process) used for creating api tokens. This gem can function without it but isn't able to auto re-create api tokens when necessary then.
|
35
|
+
|
36
|
+
#### api_token
|
37
|
+
string or nil with api token for accessing actual data. If it's not provided or is expired then this gem will request new one with the provided oauth token.
|
38
|
+
|
39
|
+
#### on_token_error
|
40
|
+
proc with what happens when missing or expired api token is encountered. If it's not provided then the default behaviour is to request new api token and store and use the new token only in api instance. The default proc looks like this
|
41
|
+
```
|
42
|
+
DEFAULT_ON_TOKEN_ERROR = -> (api) do
|
43
|
+
api.api_token = api.new_api_token
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Custom proc can be used to add some special logic for this event like for example storing the new token also somewhere in the databse. The proc will be called with the api instance.
|
48
|
+
|
49
|
+
For the common scenario when working with Rails and ActiveRecord this gem also provides proc which stores the new token in ActiveRecord model. The proc can be instantiated like this `Shoptet.ar_on_token_error(my_model_instance)` and expects the model to have `#api_token` setter defined. The exact behaviour can be seen in the [code]().
|
50
|
+
|
51
|
+
## Parallel requests
|
52
|
+
|
53
|
+
This gem fires only one network request at a time(of course) so some paralelization or whatnot is up to you.
|
54
|
+
|
55
|
+
## Exceptions
|
56
|
+
|
57
|
+
This gem fires special exceptions on various events.
|
58
|
+
|
59
|
+
`Shoptet::Error` when some general error occurs
|
60
|
+
`Shoptet::AddonSuspended` when you are trying to access api for shop which has your addon suspended
|
61
|
+
`Shoptet::NoRights` when you try to access api endpoint which is not approved
|
62
|
+
`Shoptet::SlowDown` when 429 is returned from the api
|
63
|
+
`Shoptet::InvalidOauthToken` when invalid oauth token is used
|
64
|
+
|
65
|
+
## Collection endpoints
|
66
|
+
|
67
|
+
Methods for accessing collections(products, orders, ...) automatically handle pagination so you don't have to do it manually. These collection methods return instances of `Enumerator` on which you can run standard methods like `#each`, `#map`, etc.
|
68
|
+
|
69
|
+
Also they accept hash with params which will be passed to Shoptet api. Through this you can set various filters and pagination.
|
70
|
+
|
71
|
+
* `Shoptet#products(api_params: {})`
|
72
|
+
* `Shoptet#products_changes(api_params: {})`
|
73
|
+
* `Shoptet#supplies(api_params: {})`
|
74
|
+
* `Shoptet#product_categories(api_params: {})`
|
75
|
+
* `Shoptet#price_lists(api_params: {})`
|
76
|
+
|
77
|
+
## Detail endpoints
|
78
|
+
|
79
|
+
* `Shoptet#product(guid)`
|
80
|
+
* `Shoptet#order(code)`
|
81
|
+
|
82
|
+
## Other
|
83
|
+
|
84
|
+
* `Shoptet#new_api_token` - this returns new api token created with oauth token
|
85
|
+
|
86
|
+
## License
|
87
|
+
|
88
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'shoptet'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/shoptet.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
require_relative 'shoptet/request'
|
2
|
+
|
3
|
+
class Shoptet
|
4
|
+
class Error < StandardError
|
5
|
+
#TODO: check that this works
|
6
|
+
attr_reader :additional_data
|
7
|
+
|
8
|
+
def initialize message, additional_data = {}
|
9
|
+
super(message)
|
10
|
+
@additional_data = additional_data
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class AddonSuspended < StandardError; end
|
14
|
+
|
15
|
+
DEFAULT_ON_TOKEN_ERROR = -> (api) do
|
16
|
+
api.api_token = api.new_api_token
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.version
|
20
|
+
'0.0.1'
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.ar_on_token_error(model)
|
24
|
+
-> (api) do
|
25
|
+
model.with_lock do
|
26
|
+
model.reload
|
27
|
+
|
28
|
+
if model.api_token != api.api_token
|
29
|
+
api.api_token = model.api_token
|
30
|
+
else
|
31
|
+
new_token = api.new_api_token
|
32
|
+
api.api_token = new_token
|
33
|
+
model.api_token = new_token
|
34
|
+
model.save!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_accessor :api_token
|
41
|
+
|
42
|
+
def initialize(oauth_url, oauth_token, api_token = nil, on_token_error = nil)
|
43
|
+
@oauth_url = oauth_url
|
44
|
+
@oauth_token = oauth_token
|
45
|
+
@api_token = api_token
|
46
|
+
@on_token_error = on_token_error || DEFAULT_ON_TOKEN_ERROR
|
47
|
+
end
|
48
|
+
|
49
|
+
def shop_info api_params = {}
|
50
|
+
request 'https://api.myshoptet.com/api/eshop'
|
51
|
+
end
|
52
|
+
|
53
|
+
def warehouses api_params = {}
|
54
|
+
enumerize 'https://api.myshoptet.com/api/stocks', api_params
|
55
|
+
end
|
56
|
+
|
57
|
+
def products api_params = {}
|
58
|
+
enumerize "https://api.myshoptet.com/api/products", api_params
|
59
|
+
end
|
60
|
+
|
61
|
+
def supplies warehouse_id, api_params = {}
|
62
|
+
uri = "https://api.myshoptet.com/api/stocks/#{warehouse_id}/supplies"
|
63
|
+
enumerize uri, api_params
|
64
|
+
end
|
65
|
+
|
66
|
+
def product_categories api_params = {}
|
67
|
+
enumerize 'https://api.myshoptet.com/api/categories', api_params
|
68
|
+
end
|
69
|
+
|
70
|
+
def products_changes api_params = {}
|
71
|
+
uri = 'https://api.myshoptet.com/api/products/changes'
|
72
|
+
enumerize uri, api_params
|
73
|
+
end
|
74
|
+
|
75
|
+
def price_lists api_params = {}
|
76
|
+
enumerize 'https://api.myshoptet.com/api/pricelists', api_params
|
77
|
+
end
|
78
|
+
|
79
|
+
def prices price_list_id, api_params = {}
|
80
|
+
uri = "https://api.myshoptet.com/api/pricelists/#{price_list_id}"
|
81
|
+
enumerize uri, api_params, 'pricelist'
|
82
|
+
end
|
83
|
+
|
84
|
+
def orders api_params = {}
|
85
|
+
enumerize 'https://api.myshoptet.com/api/orders', api_params
|
86
|
+
end
|
87
|
+
|
88
|
+
def orders_changes api_params = {}
|
89
|
+
uri = 'https://api.myshoptet.com/api/orders/changes'
|
90
|
+
enumerize uri, api_params
|
91
|
+
end
|
92
|
+
|
93
|
+
def order code, api_params = {}
|
94
|
+
uri = "https://api.myshoptet.com/api/orders/#{code}"
|
95
|
+
result = request assemble_uri(uri, api_params)
|
96
|
+
result.dig('data', 'order').merge(detail_url: uri)
|
97
|
+
end
|
98
|
+
|
99
|
+
def product guid, api_params = {}
|
100
|
+
uri = "https://api.myshoptet.com/api/products/#{guid}"
|
101
|
+
result = request assemble_uri(uri, api_params)
|
102
|
+
result.dig('data')
|
103
|
+
end
|
104
|
+
|
105
|
+
def new_api_token
|
106
|
+
headers = { 'Authorization' => "Bearer #{@oauth_token}" }
|
107
|
+
|
108
|
+
result = Shoptet::Request.get @oauth_url, headers
|
109
|
+
handle_errors result
|
110
|
+
|
111
|
+
# error = result['error']
|
112
|
+
# errors = result['errors'] || []
|
113
|
+
#
|
114
|
+
# #TODO: unite error handling with #request
|
115
|
+
# if error || errors.any?
|
116
|
+
# additional_data = {
|
117
|
+
# uri: @oauth_url,
|
118
|
+
# headers: scrub_sensitive_headers(headers)
|
119
|
+
# }
|
120
|
+
#
|
121
|
+
# raise Error.new result, additional_data
|
122
|
+
# else
|
123
|
+
# result.fetch 'access_token'
|
124
|
+
# end
|
125
|
+
|
126
|
+
result.fetch 'access_token'
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def assemble_uri base, params = {}
|
132
|
+
u = URI(base)
|
133
|
+
u.query = URI.encode_www_form(params) if params.any?
|
134
|
+
|
135
|
+
u.to_s
|
136
|
+
end
|
137
|
+
|
138
|
+
def enumerize base_uri, filters = {}, data_key = nil
|
139
|
+
data_key ||= URI(base_uri).path.split('/').last
|
140
|
+
uri = assemble_uri base_uri, filters
|
141
|
+
size_proc = -> () { request(uri)['data']['paginator']['totalCount'] }
|
142
|
+
|
143
|
+
Enumerator.new(size_proc) do |y|
|
144
|
+
first_page = request uri
|
145
|
+
total_pages = first_page.dig('data', 'paginator', 'pageCount') || 0
|
146
|
+
other_pages = 2..total_pages
|
147
|
+
|
148
|
+
first_page.dig('data', data_key).each { y.yield _1.merge(page_url: uri) }
|
149
|
+
|
150
|
+
other_pages.each do |page|
|
151
|
+
uri = assemble_uri base_uri, filters.merge(page: page)
|
152
|
+
result = request uri
|
153
|
+
result.dig('data', data_key).each { y.yield _1.merge(page_url: uri) }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def request uri
|
159
|
+
headers = { 'Shoptet-Access-Token' => @api_token,
|
160
|
+
'Content-Type' => 'application/vnd.shoptet.v1.0' }
|
161
|
+
|
162
|
+
result = Shoptet::Request.get uri, headers
|
163
|
+
token_errors = handle_errors result
|
164
|
+
|
165
|
+
if token_errors.any?
|
166
|
+
@on_token_error.call self
|
167
|
+
request uri
|
168
|
+
else
|
169
|
+
result
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def handle_errors result
|
174
|
+
error = result['error']
|
175
|
+
errors = result['errors'] || []
|
176
|
+
token_errors, non_token_errors = errors.partition { |err| ['invalid-token', 'expired-token'].include? err['errorCode'] }
|
177
|
+
|
178
|
+
if error || non_token_errors.any?
|
179
|
+
if error == 'addon_suspended'
|
180
|
+
raise AddonSuspended
|
181
|
+
else
|
182
|
+
additional_data = {
|
183
|
+
uri: uri,
|
184
|
+
headers: scrub_sensitive_headers(headers)
|
185
|
+
}
|
186
|
+
|
187
|
+
raise Error.new result, additional_data
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
token_errors
|
192
|
+
end
|
193
|
+
|
194
|
+
def scrub_sensitive_headers headers
|
195
|
+
scrubbed = {}
|
196
|
+
|
197
|
+
to_scrub = ['Shoptet-Access-Token', 'Authorization']
|
198
|
+
to_scrub.each do |header|
|
199
|
+
if headers[header]
|
200
|
+
scrubbed[header] = "#{headers[header][0..20]}..."
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
headers.merge scrubbed
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
|
3
|
+
#TODO: keep_alive_timeout ?
|
4
|
+
|
5
|
+
class Shoptet
|
6
|
+
class Request
|
7
|
+
def self.get uri, headers
|
8
|
+
attempt ||= 0
|
9
|
+
attempt += 1
|
10
|
+
|
11
|
+
parsed_uri = URI(uri)
|
12
|
+
|
13
|
+
http = Net::HTTP.new parsed_uri.host, parsed_uri.port
|
14
|
+
http.use_ssl = true
|
15
|
+
http.open_timeout = 10
|
16
|
+
http.read_timeout = 10
|
17
|
+
http.write_timeout = 10
|
18
|
+
http.ssl_timeout = 10
|
19
|
+
|
20
|
+
request = Net::HTTP::Get.new parsed_uri
|
21
|
+
headers.each do |key, value|
|
22
|
+
request[key] = value
|
23
|
+
end
|
24
|
+
|
25
|
+
response = http.request request
|
26
|
+
|
27
|
+
JSON.parse response.body
|
28
|
+
rescue Net::OpenTimeout
|
29
|
+
retry if attempt < 4
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/shoptet.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative 'lib/shoptet'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'shoptet'
|
5
|
+
spec.version = Shoptet.version
|
6
|
+
spec.authors = ['Premysl Donat']
|
7
|
+
spec.email = ['pdonat@seznam.cz']
|
8
|
+
|
9
|
+
spec.summary = 'API wrapper for interacting with Shoptet api'
|
10
|
+
spec.description = spec.summary
|
11
|
+
spec.homepage = 'https://github.com/Masa331/shoptet'
|
12
|
+
spec.license = 'MIT'
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
14
|
+
|
15
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
16
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency 'irb'
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shoptet
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Premysl Donat
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-11-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: irb
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: API wrapper for interacting with Shoptet api
|
28
|
+
email:
|
29
|
+
- pdonat@seznam.cz
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- bin/console
|
41
|
+
- bin/setup
|
42
|
+
- lib/shoptet.rb
|
43
|
+
- lib/shoptet/request.rb
|
44
|
+
- shoptet.gemspec
|
45
|
+
homepage: https://github.com/Masa331/shoptet
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
metadata:
|
49
|
+
homepage_uri: https://github.com/Masa331/shoptet
|
50
|
+
source_code_uri: https://github.com/Masa331/shoptet
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: 2.3.0
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubygems_version: 3.1.4
|
67
|
+
signing_key:
|
68
|
+
specification_version: 4
|
69
|
+
summary: API wrapper for interacting with Shoptet api
|
70
|
+
test_files: []
|