stormmq-client 0.0.4

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.
@@ -0,0 +1,70 @@
1
+ #--
2
+ # Copyright (c) 2010, Tony Byrne & StormMQ Ltd.
3
+ # All rights reserved.
4
+ #
5
+ # Please refer to the LICENSE file that accompanies this source
6
+ # for terms of use and redistribution.
7
+ #++
8
+
9
+ require 'singleton'
10
+ require 'json'
11
+ require 'base64'
12
+ require 'stormmq/base64_extensions'
13
+ require 'stormmq/errors'
14
+
15
+ module StormMQ
16
+
17
+ SECRET_KEYS_SEARCH_PATH = ['~/.stormmq','/etc']
18
+ SECRET_KEYS_FILENAME = 'secret-keys.json'
19
+
20
+ class SecretKeys
21
+ include Singleton
22
+ attr_writer :key_cache
23
+
24
+ # Returns the base64 encoded secret key for the given user name from the secret keys file.
25
+ def key_for(user)
26
+ raise Error::UserNotProvidedError, "user cannot be nil." if user.nil?
27
+ raise Error::UserNotProvidedError, "user cannot be empty." if user.empty?
28
+ keys[user] || (raise Error::SecretKeyNotFoundError, "a secret key for user '#{user}' could not be found in the secret key file.", caller)
29
+ end
30
+
31
+ # Returns an <tt>Array</tt> of user names of users who have keys in the secret keys file.
32
+ def users
33
+ keys.keys
34
+ end
35
+
36
+ private
37
+
38
+ # Return a hash of keys stored in the secret keys file.
39
+ def keys
40
+ @secret_keys_cache ||= load_secret_keys
41
+ end
42
+
43
+ # Load the keys from the secret keys file <tt>keyfile</tt>. Walks the locations specified in
44
+ # <tt>search_path</tt> in order of preference.
45
+ def load_secret_keys(search_path=SECRET_KEYS_SEARCH_PATH, keyfile=SECRET_KEYS_FILENAME)
46
+ full_paths = search_path.map{|p| File.expand_path("#{p}/#{keyfile}")}
47
+ full_paths.each do |full_path|
48
+ begin
49
+ return SecretKeys.secret_keys_hash_from_json(IO.read(full_path))
50
+ rescue
51
+ end
52
+ end
53
+ raise Error::LoadSecretKeysError,
54
+ "Could not read the secret keys file from any of [#{full_paths.join ', '}]. Please ensure that a valid keyfile exists in one of these locations and that it is readable.",
55
+ caller
56
+ end
57
+
58
+ def self.key_for(*args)
59
+ self.instance.key_for(*args)
60
+ end
61
+
62
+ # Create a hash of the keys contained in json fragment.
63
+ def self.secret_keys_hash_from_json(json_string)
64
+ secret_keys_hash = JSON.parse(json_string)
65
+ secret_keys_hash.inject({}) { |h,(k,v)| h[k] = Base64.urlsafe_decode64(v); h }
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,125 @@
1
+ #--
2
+ # Copyright (c) 2010, Tony Byrne & StormMQ Ltd.
3
+ # All rights reserved.
4
+ #
5
+ # Please refer to the LICENSE file that accompanies this source
6
+ # for terms of use and redistribution.
7
+ #++
8
+
9
+ require 'uri'
10
+ require 'cgi'
11
+ require 'hmac'
12
+ require 'hmac-sha2'
13
+ require 'base64'
14
+
15
+ require 'stormmq/errors'
16
+
17
+ module StormMQ
18
+
19
+ SCHEMES_DEFAULT_PORTS = {
20
+ 'http' => 80,
21
+ 'https' => 443
22
+ }
23
+
24
+ class URL
25
+
26
+ def initialize(url)
27
+ if url.is_a?(Hash)
28
+ @url = URI::Generic.build(url)
29
+ else
30
+ @url = URI.parse(url)
31
+ end
32
+ raise Error::InvalidURLError, "'#{@url.to_s}' is not a valid URL." unless self.valid?
33
+ end
34
+
35
+ def valid?
36
+ begin
37
+ URI.parse(@url.to_s).class != URI::Generic
38
+ rescue URI::InvalidURIError
39
+ false
40
+ end
41
+ end
42
+
43
+ def to_s
44
+ @url.to_s
45
+ end
46
+
47
+ def add_query_params(params_hash)
48
+ components = to_h
49
+ query_hash = StormMQ::URL.querystring_to_hash(components[:query])
50
+ components[:query] = StormMQ::URL.hash_to_canonical_querystring(
51
+ query_hash.merge(params_hash)
52
+ )
53
+ self.class.new(components)
54
+ end
55
+
56
+ def add_user_and_version_query_params(user, version=0)
57
+ add_query_params(:user => [user], :version => [version.to_s])
58
+ end
59
+
60
+ def canonicalise(user, version=0)
61
+ components = self.add_user_and_version_query_params(user, version).to_h
62
+ components[:query] = StormMQ::URL.canonicalise_query_string(components[:query]) unless components[:query].nil?
63
+ canonical = { :port => StormMQ::URL.default_port_for_scheme(components[:scheme]) }.merge(components)
64
+ self.class.new(canonical)
65
+ end
66
+
67
+ def sign(secret_key, method='GET')
68
+ self.add_query_params('signature' => compute_signature(secret_key, method))
69
+ end
70
+
71
+ def compute_signature(secret_key, method='GET')
72
+ hmac = HMAC::SHA256.new(secret_key)
73
+ hmac.update("#{method.upcase}#{self.to_s}")
74
+ Base64.urlsafe_encode64(hmac.digest).chomp
75
+ end
76
+
77
+ def canonicalise_and_sign(user, secret_key, method='GET', verison=0)
78
+ self.canonicalise(user,verison).sign(secret_key, method)
79
+ end
80
+
81
+ def to_h
82
+ components = URI.split(@url.to_s)
83
+ {
84
+ :scheme => components[0],
85
+ :userinfo => components[1],
86
+ :host => components[2],
87
+ :port => components[3],
88
+ :registry => components[4],
89
+ :path => components[5],
90
+ :opaque => components[6],
91
+ :query => components[7],
92
+ :fragment => components[8]
93
+ }.reject {|k,v| v.nil?}
94
+ end
95
+
96
+ def self.escape(string)
97
+ return '' if string.nil?
98
+ URI.escape(string, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
99
+ end
100
+
101
+ def self.default_port_for_scheme(scheme)
102
+ SCHEMES_DEFAULT_PORTS[scheme.downcase]
103
+ end
104
+
105
+ def self.querystring_to_hash(querystring)
106
+ return {} if querystring.nil?
107
+ CGI.parse(querystring)
108
+ end
109
+
110
+ def self.hash_to_canonical_querystring(options)
111
+ components = []
112
+ options.each do |key,values|
113
+ [values].flatten.each {|v| components << "#{StormMQ::URL.escape(key.to_s)}=#{StormMQ::URL.escape(v.to_s)}" }
114
+ end
115
+ components.sort.join('&')
116
+ end
117
+
118
+ def self.canonicalise_query_string(querystring)
119
+ query_hash = StormMQ::URL.querystring_to_hash(querystring)
120
+ StormMQ::URL.hash_to_canonical_querystring(query_hash)
121
+ end
122
+
123
+ end
124
+
125
+ end
@@ -0,0 +1,27 @@
1
+ #--
2
+ # Copyright (c) 2010, Tony Byrne & StormMQ Ltd.
3
+ # All rights reserved.
4
+ #
5
+ # Please refer to the LICENSE file that accompanies this source
6
+ # for terms of use and redistribution.
7
+ #++
8
+
9
+ module StormMQ
10
+ module Utils
11
+ # def self.stormmq_credentials_string(user, password)
12
+ # "\0#{user}\0#{password}"
13
+ # end
14
+ end
15
+ end
16
+
17
+ class String
18
+ def blank?
19
+ self.gsub(/\s+/,'') == ""
20
+ end
21
+ end
22
+
23
+ class NilClass
24
+ def blank?
25
+ true
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ #--
2
+ # Copyright (c) 2010, Tony Byrne & StormMQ Ltd.
3
+ # All rights reserved.
4
+ #
5
+ # Please refer to the LICENSE file that accompanies this source
6
+ # for terms of use and redistribution.
7
+ #++
8
+
9
+ require File.dirname(__FILE__) + '/../../lib/stormmq/amqp'
10
+
11
+ describe StormMQ::AMQPClient do
12
+ it "constructs the virtual host string from the StormMQ specific options in the connect option hash" do
13
+ options = {
14
+ :company => 'a_company',
15
+ :system => 'a_system',
16
+ :environment => 'an_environment'
17
+ }
18
+ StormMQ::AMQPClient.vhost_from_options(options).should == '/a_company/a_system/an_environment'
19
+ end
20
+ end
21
+
@@ -0,0 +1,25 @@
1
+ #--
2
+ # Copyright (c) 2010, Tony Byrne & StormMQ Ltd.
3
+ # All rights reserved.
4
+ #
5
+ # Please refer to the LICENSE file that accompanies this source
6
+ # for terms of use and redistribution.
7
+ #++
8
+
9
+ require 'base64'
10
+ require File.dirname(__FILE__) + '/../../lib/stormmq/secret_keys'
11
+ require File.dirname(__FILE__) + '/../../lib/stormmq/base64_extensions'
12
+
13
+ describe StormMQ::SecretKeys do
14
+
15
+ describe "secret_keys_hash_from_json" do
16
+
17
+ it "should load the secret key for a user from the json format string" do
18
+ url_safe_base64_key = "BNuWk1agaAUPTZ15sx44kHvNkTnJXsevqTjIo1M1iwFOeNaUqr3qP-_5Dnk=="
19
+ json = %Q|{"tonybyrne":"#{url_safe_base64_key}"}|
20
+ StormMQ::SecretKeys.secret_keys_hash_from_json(json).should == { 'tonybyrne' => Base64.urlsafe_decode64(url_safe_base64_key) }
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,129 @@
1
+ #--
2
+ # Copyright (c) 2010, Tony Byrne & StormMQ Ltd.
3
+ # All rights reserved.
4
+ #
5
+ # Please refer to the LICENSE file that accompanies this source
6
+ # for terms of use and redistribution.
7
+ #++
8
+
9
+ require File.dirname(__FILE__) + '/../../lib/stormmq/url'
10
+
11
+ describe StormMQ::URL do
12
+
13
+ describe "to_s" do
14
+
15
+ it "should return the url as a string" do
16
+ StormMQ::URL.new('https://api.stormmq.com/').to_s.should == 'https://api.stormmq.com/'
17
+ end
18
+
19
+ end
20
+
21
+ describe "canonicalise" do
22
+
23
+ it "adds explicit port for HTTP" do
24
+ StormMQ::URL.new('http://api.stormmq.com/').canonicalise('test').to_s.should == 'http://api.stormmq.com:80/?user=test&version=0'
25
+ end
26
+
27
+ it "adds explicit port for HTTPS" do
28
+ StormMQ::URL.new('https://api.stormmq.com/').canonicalise('test').to_s.should == 'https://api.stormmq.com:443/?user=test&version=0'
29
+ end
30
+
31
+ it "sorts query string params by param name" do
32
+ StormMQ::URL.new('https://api.stormmq.com/?z=3&x=1&y=2').canonicalise('test').to_s.should == 'https://api.stormmq.com:443/?user=test&version=0&x=1&y=2&z=3'
33
+ end
34
+
35
+ it "sorts query string params with multiple values by value" do
36
+ StormMQ::URL.new('https://api.stormmq.com/?z=3&x=1&y=2&z=1').canonicalise('test').to_s.should == 'https://api.stormmq.com:443/?user=test&version=0&x=1&y=2&z=1&z=3'
37
+ end
38
+
39
+ it "should canonicalise complex example from http://stormmq.com/rest-apis/for-security-reasons (without user and version params)" do
40
+ url = 'https://api.stormmq.com/api/2009-01-01/%3D%25?empty=&%20novalue&foo=%2Fvalue'
41
+ expected = 'https://api.stormmq.com:443/api/2009-01-01/%3D%25?%20novalue=&empty=&foo=%2Fvalue&user=raph&version=0'
42
+ StormMQ::URL.new(url).canonicalise('raph').to_s.should == expected
43
+ end
44
+
45
+ end
46
+
47
+ describe "add_query_params" do
48
+
49
+ before(:each) do
50
+ @url = StormMQ::URL.new('http://api.stormmq.com/')
51
+ end
52
+
53
+ it "should add params to the querystring and canonicalise the querystring" do
54
+ @url.add_query_params('z' => 3, 'x' => 1, 'y' => 2).to_s.should == 'http://api.stormmq.com/?x=1&y=2&z=3'
55
+ end
56
+
57
+ it "should add params as symbols to the querystring and canonicalise the querystring" do
58
+ @url.add_query_params(:z => 3, :x => 1, :y => 2).to_s.should == 'http://api.stormmq.com/?x=1&y=2&z=3'
59
+ end
60
+
61
+ it "should add params with multiple values to the querystring and canonicalise the querystring" do
62
+ @url.add_query_params(:z => [3,1,2]).to_s.should == 'http://api.stormmq.com/?z=1&z=2&z=3'
63
+ end
64
+
65
+ it "should URI escape params and values added to a URL" do
66
+ @url.add_query_params(' test ' => 'a+value').to_s.should == 'http://api.stormmq.com/?%20test%20=a%2Bvalue'
67
+ end
68
+
69
+ end
70
+
71
+ describe "add_user_and_version_query_params" do
72
+
73
+ before(:each) do
74
+ @url = StormMQ::URL.new('http://api.stormmq.com/')
75
+ end
76
+
77
+ it "should add the user and version querystring params to a URL" do
78
+ @url.add_user_and_version_query_params('tonybyrne',1).to_s.should == 'http://api.stormmq.com/?user=tonybyrne&version=1'
79
+ end
80
+
81
+ it "version should default to '0'" do
82
+ @url.add_user_and_version_query_params('tonybyrne').to_s.should == 'http://api.stormmq.com/?user=tonybyrne&version=0'
83
+ end
84
+
85
+ end
86
+
87
+ describe "query_string_to_hash" do
88
+
89
+ it "should convert a simple query string to a hash representation" do
90
+ StormMQ::URL.querystring_to_hash("param=value").should == { 'param' => ['value'] }
91
+ end
92
+
93
+ it "should uri decode the query string" do
94
+ StormMQ::URL.querystring_to_hash("%20param=value%20").should == { ' param' => ['value '] }
95
+ end
96
+
97
+ end
98
+
99
+ describe "hash_to_canonical_querystring" do
100
+
101
+ it "should convert a hash with a single key value pair to a query string" do
102
+ StormMQ::URL.hash_to_canonical_querystring({'param' => ['value']}).should == "param=value"
103
+ end
104
+
105
+ it "should uri encode components of query string" do
106
+ StormMQ::URL.hash_to_canonical_querystring(
107
+ {
108
+ 'param with spaces' => ['value with spaces']
109
+ }
110
+ ).should == "param%20with%20spaces=value%20with%20spaces"
111
+ end
112
+
113
+ it "should convert a hash with a single key, but multiple values to a query string, and sort the components" do
114
+ StormMQ::URL.hash_to_canonical_querystring({'param' => ['Z','X','Y']}).should == "param=X&param=Y&param=Z"
115
+ end
116
+
117
+ it "should convert a hash with a multiple keys, and multiple values to a query string, and sort the components" do
118
+ StormMQ::URL.hash_to_canonical_querystring(
119
+ {
120
+ 'paramZ' => ['Z','X','Y'],
121
+ 'paramX' => ['1','3','2'],
122
+ 'paramY' => ['c','a','b']
123
+ }
124
+ ).should == "paramX=1&paramX=2&paramX=3&paramY=a&paramY=b&paramY=c&paramZ=X&paramZ=Y&paramZ=Z"
125
+ end
126
+
127
+ end
128
+
129
+ end
@@ -0,0 +1,13 @@
1
+ #--
2
+ # Copyright (c) 2010, Tony Byrne & StormMQ Ltd.
3
+ # All rights reserved.
4
+ #
5
+ # Please refer to the LICENSE file that accompanies this source
6
+ # for terms of use and redistribution.
7
+ #++
8
+
9
+ require File.dirname(__FILE__) + '/../../lib/stormmq/utils'
10
+
11
+ describe StormMQ::Utils do
12
+
13
+ end
metadata ADDED
@@ -0,0 +1,254 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stormmq-client
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 4
10
+ version: 0.0.4
11
+ platform: ruby
12
+ authors:
13
+ - Tony Byrne
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain:
17
+ date: 2010-06-07 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: amqp
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 9
29
+ segments:
30
+ - 0
31
+ - 6
32
+ - 7
33
+ version: 0.6.7
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: rest-client
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 1
47
+ - 4
48
+ - 2
49
+ version: 1.4.2
50
+ type: :runtime
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: ruby-hmac
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 15
61
+ segments:
62
+ - 0
63
+ - 4
64
+ - 0
65
+ version: 0.4.0
66
+ type: :runtime
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: json
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 1
79
+ - 4
80
+ - 2
81
+ version: 1.4.2
82
+ type: :runtime
83
+ version_requirements: *id004
84
+ - !ruby/object:Gem::Dependency
85
+ name: commandline
86
+ prerelease: false
87
+ requirement: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 23
93
+ segments:
94
+ - 0
95
+ - 7
96
+ - 10
97
+ version: 0.7.10
98
+ type: :runtime
99
+ version_requirements: *id005
100
+ - !ruby/object:Gem::Dependency
101
+ name: rspec
102
+ prerelease: false
103
+ requirement: &id006 !ruby/object:Gem::Requirement
104
+ none: false
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ hash: 27
109
+ segments:
110
+ - 1
111
+ - 3
112
+ - 0
113
+ version: 1.3.0
114
+ type: :development
115
+ version_requirements: *id006
116
+ - !ruby/object:Gem::Dependency
117
+ name: rake
118
+ prerelease: false
119
+ requirement: &id007 !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ hash: 49
125
+ segments:
126
+ - 0
127
+ - 8
128
+ - 7
129
+ version: 0.8.7
130
+ type: :development
131
+ version_requirements: *id007
132
+ - !ruby/object:Gem::Dependency
133
+ name: rcov
134
+ prerelease: false
135
+ requirement: &id008 !ruby/object:Gem::Requirement
136
+ none: false
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ hash: 25
141
+ segments:
142
+ - 0
143
+ - 9
144
+ - 7
145
+ - 1
146
+ version: 0.9.7.1
147
+ type: :development
148
+ version_requirements: *id008
149
+ - !ruby/object:Gem::Dependency
150
+ name: rdoc
151
+ prerelease: false
152
+ requirement: &id009 !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ hash: 11
158
+ segments:
159
+ - 2
160
+ - 5
161
+ - 8
162
+ version: 2.5.8
163
+ type: :development
164
+ version_requirements: *id009
165
+ description: A client library for StormMQ's Cloud Messaging service. See http://www.stormmq.com/ for details of the service.
166
+ email: stormmq@byrnehq.com
167
+ executables:
168
+ - stormmq-amqp-echo-test
169
+ - stormmq-create-system
170
+ - stormmq-delete-system
171
+ - stormmq-describe-company
172
+ - stormmq-describe-system
173
+ - stormmq-get-amqpuser-password
174
+ - stormmq-list-amqpusers
175
+ - stormmq-list-apis
176
+ - stormmq-list-bindings
177
+ - stormmq-list-clusters
178
+ - stormmq-list-companies
179
+ - stormmq-list-exchanges
180
+ - stormmq-list-queues
181
+ - stormmq-list-systems
182
+ - stormmq-url-signer
183
+ extensions: []
184
+
185
+ extra_rdoc_files: []
186
+
187
+ files:
188
+ - lib/stormmq/amqp.rb
189
+ - lib/stormmq/application.rb
190
+ - lib/stormmq/base64_extensions.rb
191
+ - lib/stormmq/errors.rb
192
+ - lib/stormmq/rest.rb
193
+ - lib/stormmq/secret_keys.rb
194
+ - lib/stormmq/url.rb
195
+ - lib/stormmq/utils.rb
196
+ - bin/stormmq-amqp-echo-test
197
+ - bin/stormmq-create-system
198
+ - bin/stormmq-delete-system
199
+ - bin/stormmq-describe-company
200
+ - bin/stormmq-describe-system
201
+ - bin/stormmq-get-amqpuser-password
202
+ - bin/stormmq-list-amqpusers
203
+ - bin/stormmq-list-apis
204
+ - bin/stormmq-list-bindings
205
+ - bin/stormmq-list-clusters
206
+ - bin/stormmq-list-companies
207
+ - bin/stormmq-list-exchanges
208
+ - bin/stormmq-list-queues
209
+ - bin/stormmq-list-systems
210
+ - bin/stormmq-url-signer
211
+ - spec/stormmq/amqp_spec.rb
212
+ - spec/stormmq/secret_keys_spec.rb
213
+ - spec/stormmq/url_spec.rb
214
+ - spec/stormmq/utils_spec.rb
215
+ - LICENSE
216
+ - Rakefile
217
+ - README
218
+ - TODO
219
+ has_rdoc: true
220
+ homepage: http://github.com/tonybyrne/StormMQ-Ruby-Client
221
+ licenses: []
222
+
223
+ post_install_message:
224
+ rdoc_options: []
225
+
226
+ require_paths:
227
+ - lib
228
+ required_ruby_version: !ruby/object:Gem::Requirement
229
+ none: false
230
+ requirements:
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ hash: 3
234
+ segments:
235
+ - 0
236
+ version: "0"
237
+ required_rubygems_version: !ruby/object:Gem::Requirement
238
+ none: false
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ hash: 3
243
+ segments:
244
+ - 0
245
+ version: "0"
246
+ requirements: []
247
+
248
+ rubyforge_project:
249
+ rubygems_version: 1.3.7
250
+ signing_key:
251
+ specification_version: 3
252
+ summary: A client library for StormMQ's Cloud Messaging service
253
+ test_files: []
254
+