th_shopify_api 1.2.6.pre
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.
- data/.document +5 -0
- data/.gitignore +5 -0
- data/CHANGELOG +57 -0
- data/LICENSE +20 -0
- data/README.rdoc +60 -0
- data/RELEASING +16 -0
- data/Rakefile +41 -0
- data/bin/shopify +4 -0
- data/lib/active_resource/connection_ext.rb +16 -0
- data/lib/shopify_api.rb +18 -0
- data/lib/shopify_api/countable.rb +7 -0
- data/lib/shopify_api/events.rb +7 -0
- data/lib/shopify_api/limits.rb +76 -0
- data/lib/shopify_api/metafields.rb +18 -0
- data/lib/shopify_api/resources.rb +40 -0
- data/lib/shopify_api/resources/address.rb +4 -0
- data/lib/shopify_api/resources/application_charge.rb +9 -0
- data/lib/shopify_api/resources/article.rb +12 -0
- data/lib/shopify_api/resources/asset.rb +95 -0
- data/lib/shopify_api/resources/base.rb +5 -0
- data/lib/shopify_api/resources/billing_address.rb +4 -0
- data/lib/shopify_api/resources/blog.rb +10 -0
- data/lib/shopify_api/resources/cli.rb +161 -0
- data/lib/shopify_api/resources/collect.rb +5 -0
- data/lib/shopify_api/resources/comment.rb +13 -0
- data/lib/shopify_api/resources/countable.rb +7 -0
- data/lib/shopify_api/resources/country.rb +4 -0
- data/lib/shopify_api/resources/custom_collection.rb +19 -0
- data/lib/shopify_api/resources/customer.rb +4 -0
- data/lib/shopify_api/resources/customer_group.rb +4 -0
- data/lib/shopify_api/resources/event.rb +10 -0
- data/lib/shopify_api/resources/fulfillment.rb +5 -0
- data/lib/shopify_api/resources/image.rb +16 -0
- data/lib/shopify_api/resources/line_item.rb +4 -0
- data/lib/shopify_api/resources/metafield.rb +15 -0
- data/lib/shopify_api/resources/note_attribute.rb +4 -0
- data/lib/shopify_api/resources/option.rb +4 -0
- data/lib/shopify_api/resources/order.rb +25 -0
- data/lib/shopify_api/resources/page.rb +6 -0
- data/lib/shopify_api/resources/payment_details.rb +4 -0
- data/lib/shopify_api/resources/product.rb +33 -0
- data/lib/shopify_api/resources/product_search_engine.rb +4 -0
- data/lib/shopify_api/resources/province.rb +5 -0
- data/lib/shopify_api/resources/receipt.rb +4 -0
- data/lib/shopify_api/resources/recurring_application_charge.rb +23 -0
- data/lib/shopify_api/resources/redirect.rb +4 -0
- data/lib/shopify_api/resources/rule.rb +4 -0
- data/lib/shopify_api/resources/script_tag.rb +4 -0
- data/lib/shopify_api/resources/shipping_address.rb +4 -0
- data/lib/shopify_api/resources/shipping_line.rb +4 -0
- data/lib/shopify_api/resources/shop.rb +23 -0
- data/lib/shopify_api/resources/smart_collection.rb +10 -0
- data/lib/shopify_api/resources/tax_line.rb +4 -0
- data/lib/shopify_api/resources/theme.rb +4 -0
- data/lib/shopify_api/resources/transaction.rb +5 -0
- data/lib/shopify_api/resources/variant.rb +11 -0
- data/lib/shopify_api/resources/webhook.rb +4 -0
- data/lib/shopify_api/session.rb +166 -0
- data/shopify_api.gemspec +35 -0
- data/test/cli_test.rb +109 -0
- data/test/limits_test.rb +37 -0
- data/test/order_test.rb +48 -0
- data/test/shopify_api_test.rb +55 -0
- data/test/test_helper.rb +29 -0
- metadata +153 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'abbrev'
|
3
|
+
|
4
|
+
module ShopifyAPI
|
5
|
+
class Cli < Thor
|
6
|
+
include Thor::Actions
|
7
|
+
|
8
|
+
class ConfigFileError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "list", "list available connections"
|
12
|
+
def list
|
13
|
+
available_connections.each do |c|
|
14
|
+
prefix = default?(c) ? " * " : " "
|
15
|
+
puts prefix + c
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "add CONNECTION", "create a config file for a connection named CONNECTION"
|
20
|
+
def add(connection)
|
21
|
+
file = config_file(connection)
|
22
|
+
if File.exist?(file)
|
23
|
+
raise ConfigFileError, "There is already a config file at #{file}"
|
24
|
+
else
|
25
|
+
config = {'protocol' => 'https'}
|
26
|
+
config['domain'] = ask("Domain? (leave blank for #{connection}.myshopify.com)")
|
27
|
+
config['domain'] = "#{connection}.myshopify.com" if config['domain'].blank?
|
28
|
+
puts "\nopen https://#{config['domain']}/admin/api in your browser to get API credentials\n"
|
29
|
+
config['api_key'] = ask("API key?")
|
30
|
+
config['password'] = ask("Password?")
|
31
|
+
create_file(file, config.to_yaml)
|
32
|
+
end
|
33
|
+
if available_connections.one?
|
34
|
+
default(connection)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "remove CONNECTION", "remove the config file for CONNECTION"
|
39
|
+
def remove(connection)
|
40
|
+
file = config_file(connection)
|
41
|
+
if File.exist?(file)
|
42
|
+
remove_file(default_symlink) if default?(connection)
|
43
|
+
remove_file(file)
|
44
|
+
else
|
45
|
+
no_config_file_error(file)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "edit [CONNECTION]", "open the config file for CONNECTION with your default editor"
|
50
|
+
def edit(connection=nil)
|
51
|
+
file = config_file(connection)
|
52
|
+
if File.exist?(file)
|
53
|
+
if ENV['EDITOR'].present?
|
54
|
+
system(ENV['EDITOR'], file)
|
55
|
+
else
|
56
|
+
puts "Please set an editor in the EDITOR environment variable"
|
57
|
+
end
|
58
|
+
else
|
59
|
+
no_config_file_error(file)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "show [CONNECTION]", "output the location and contents of the CONNECTION's config file"
|
64
|
+
def show(connection=nil)
|
65
|
+
connection ||= default_connection
|
66
|
+
file = config_file(connection)
|
67
|
+
if File.exist?(file)
|
68
|
+
puts file
|
69
|
+
puts `cat #{file}`
|
70
|
+
else
|
71
|
+
no_config_file_error(file)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "default [CONNECTION]", "show the default connection, or make CONNECTION the default"
|
76
|
+
def default(connection=nil)
|
77
|
+
if connection
|
78
|
+
target = config_file(connection)
|
79
|
+
if File.exist?(target)
|
80
|
+
remove_file(default_symlink)
|
81
|
+
`ln -s #{target} #{default_symlink}`
|
82
|
+
else
|
83
|
+
no_config_file_error(target)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
if File.exist?(default_symlink)
|
87
|
+
puts "Default connection is #{default_connection}"
|
88
|
+
else
|
89
|
+
puts "There is no default connection set"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "console [CONNECTION]", "start an API console for CONNECTION"
|
94
|
+
def console(connection=nil)
|
95
|
+
file = config_file(connection)
|
96
|
+
|
97
|
+
config = YAML.load(File.read(file))
|
98
|
+
puts "using #{config['domain']}"
|
99
|
+
ShopifyAPI::Base.site = site_from_config(config)
|
100
|
+
|
101
|
+
require 'irb'
|
102
|
+
require 'irb/completion'
|
103
|
+
ARGV.clear
|
104
|
+
IRB.start
|
105
|
+
end
|
106
|
+
|
107
|
+
tasks.keys.abbrev.each do |shortcut, command|
|
108
|
+
map shortcut => command.to_sym
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def shop_config_dir
|
114
|
+
@shop_config_dir ||= File.join(ENV['HOME'], '.shopify', 'shops')
|
115
|
+
end
|
116
|
+
|
117
|
+
def default_symlink
|
118
|
+
@default_symlink ||= File.join(shop_config_dir, 'default')
|
119
|
+
end
|
120
|
+
|
121
|
+
def config_file(connection)
|
122
|
+
if connection
|
123
|
+
File.join(shop_config_dir, "#{connection}.yml")
|
124
|
+
else
|
125
|
+
default_symlink
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def site_from_config(config)
|
130
|
+
protocol = config['protocol'] || 'https'
|
131
|
+
api_key = config['api_key']
|
132
|
+
password = config['password']
|
133
|
+
domain = config['domain']
|
134
|
+
|
135
|
+
ShopifyAPI::Base.site = "#{protocol}://#{api_key}:#{password}@#{domain}/admin"
|
136
|
+
end
|
137
|
+
|
138
|
+
def available_connections
|
139
|
+
@available_connections ||= begin
|
140
|
+
pattern = File.join(shop_config_dir, "*.yml")
|
141
|
+
Dir.glob(pattern).map { |f| File.basename(f, ".yml") }
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def default_connection_target
|
146
|
+
@default_connection_target ||= File.readlink(default_symlink)
|
147
|
+
end
|
148
|
+
|
149
|
+
def default_connection
|
150
|
+
@default_connection ||= File.basename(default_connection_target, ".yml")
|
151
|
+
end
|
152
|
+
|
153
|
+
def default?(connection)
|
154
|
+
default_connection == connection
|
155
|
+
end
|
156
|
+
|
157
|
+
def no_config_file_error(filename)
|
158
|
+
raise ConfigFileError, "There is no config file at #{filename}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Comment < Base
|
3
|
+
def remove; load_attributes_from_response(post(:remove, {}, only_id)); end
|
4
|
+
def spam; load_attributes_from_response(post(:spam, {}, only_id)); end
|
5
|
+
def approve; load_attributes_from_response(post(:approve, {}, only_id)); end
|
6
|
+
def restore; load_attributes_from_response(post(:restore, {}, only_id)); end
|
7
|
+
def not_spam; load_attributes_from_response(post(:not_spam, {}, only_id)); end
|
8
|
+
|
9
|
+
def only_id
|
10
|
+
encode(:only => :id)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class CustomCollection < Base
|
3
|
+
include Events
|
4
|
+
include Metafields
|
5
|
+
|
6
|
+
def products
|
7
|
+
Product.find(:all, :params => {:collection_id => self.id})
|
8
|
+
end
|
9
|
+
|
10
|
+
def add_product(product)
|
11
|
+
Collect.create(:collection_id => self.id, :product_id => product.id)
|
12
|
+
end
|
13
|
+
|
14
|
+
def remove_product(product)
|
15
|
+
collect = Collect.find(:first, :params => {:collection_id => self.id, :product_id => product.id})
|
16
|
+
collect.destroy if collect
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Event < Base
|
3
|
+
self.prefix = "/admin/:resource/:resource_id/"
|
4
|
+
|
5
|
+
# Hack to allow both Shop and other Events in through the same AR class
|
6
|
+
def self.prefix(options={})
|
7
|
+
options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Image < Base
|
3
|
+
self.prefix = "/admin/products/:product_id/"
|
4
|
+
|
5
|
+
# generate a method for each possible image variant
|
6
|
+
[:pico, :icon, :thumb, :small, :compact, :medium, :large, :grande, :original].each do |m|
|
7
|
+
reg_exp_match = "/\\1_#{m}.\\2"
|
8
|
+
define_method(m) { src.gsub(/\/(.*)\.(\w{2,4})/, reg_exp_match) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def attach_image(data, filename = nil)
|
12
|
+
attributes['attachment'] = Base64.encode64(data)
|
13
|
+
attributes['filename'] = filename unless filename.nil?
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Metafield < Base
|
3
|
+
self.prefix = "/admin/:resource/:resource_id/"
|
4
|
+
|
5
|
+
# Hack to allow both Shop and other Metafields in through the same AR class
|
6
|
+
def self.prefix(options={})
|
7
|
+
options[:resource].nil? ? "/admin/" : "/admin/#{options[:resource]}/#{options[:resource_id]}/"
|
8
|
+
end
|
9
|
+
|
10
|
+
def value
|
11
|
+
return if attributes["value"].nil?
|
12
|
+
attributes["value_type"] == "integer" ? attributes["value"].to_i : attributes["value"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Order < Base
|
3
|
+
include Events
|
4
|
+
include Metafields
|
5
|
+
|
6
|
+
def close; load_attributes_from_response(post(:close, {}, only_id)); end
|
7
|
+
def open; load_attributes_from_response(post(:open, {}, only_id)); end
|
8
|
+
|
9
|
+
def cancel(options = {})
|
10
|
+
load_attributes_from_response(post(:cancel, options, only_id))
|
11
|
+
end
|
12
|
+
|
13
|
+
def transactions
|
14
|
+
Transaction.find(:all, :params => { :order_id => id })
|
15
|
+
end
|
16
|
+
|
17
|
+
def capture(amount = "")
|
18
|
+
Transaction.create(:amount => amount, :kind => "capture", :order_id => id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def only_id
|
22
|
+
encode(:only => :id, :include => [], :methods => [], :fields => [])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class Product < Base
|
3
|
+
include Events
|
4
|
+
include Metafields
|
5
|
+
|
6
|
+
# compute the price range
|
7
|
+
def price_range
|
8
|
+
prices = variants.collect(&:price)
|
9
|
+
format = "%0.2f"
|
10
|
+
if prices.min != prices.max
|
11
|
+
"#{format % prices.min} - #{format % prices.max}"
|
12
|
+
else
|
13
|
+
format % prices.min
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def collections
|
18
|
+
CustomCollection.find(:all, :params => {:product_id => self.id})
|
19
|
+
end
|
20
|
+
|
21
|
+
def smart_collections
|
22
|
+
SmartCollection.find(:all, :params => {:product_id => self.id})
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_to_collection(collection)
|
26
|
+
collection.add_product(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_from_collection(collection)
|
30
|
+
collection.remove_product(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class RecurringApplicationCharge < Base
|
3
|
+
undef_method :test
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def current
|
7
|
+
all.find { |c| c.status == 'active' }
|
8
|
+
end
|
9
|
+
|
10
|
+
[:pending, :cancelled, :accepted, :declined].each do |status|
|
11
|
+
define_method(status) { all.select { |c| c.status == status.to_s } }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def cancel
|
16
|
+
load_attributes_from_response(self.destroy)
|
17
|
+
end
|
18
|
+
|
19
|
+
def activate
|
20
|
+
load_attributes_from_response(post(:activate))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|