magnum-pi 0.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.
@@ -0,0 +1 @@
1
+ require "magnum-pi/gem_ext/mechanize"
@@ -0,0 +1 @@
1
+ require "magnum-pi/gem_ext/mechanize/http"
@@ -0,0 +1 @@
1
+ require "magnum-pi/gem_ext/mechanize/http/agent"
@@ -0,0 +1,60 @@
1
+ # Taken from: http://scottwb.com/blog/2013/11/09/defeating-the-infamous-mechanize-too-many-connection-resets-bug/
2
+
3
+ class Mechanize
4
+ class HTTP
5
+ class Agent
6
+ MAX_RESET_RETRIES = 10
7
+
8
+ # We need to replace the core Mechanize HTTP method:
9
+ #
10
+ # Mechanize::HTTP::Agent#fetch
11
+ #
12
+ # with a wrapper that handles the infamous "too many connection resets"
13
+ # Mechanize bug that is described here:
14
+ #
15
+ # https://github.com/sparklemotion/mechanize/issues/123
16
+ #
17
+ # The wrapper shuts down the persistent HTTP connection when it fails with
18
+ # this error, and simply tries again. In practice, this only ever needs to
19
+ # be retried once, but I am going to let it retry a few times
20
+ # (MAX_RESET_RETRIES), just in case.
21
+ #
22
+ def fetch_with_retry(
23
+ uri,
24
+ method = :get,
25
+ headers = {},
26
+ params = [],
27
+ referer = current_page,
28
+ redirects = 0
29
+ )
30
+ action = "#{method.to_s.upcase} #{uri.to_s}"
31
+ retry_count = 0
32
+
33
+ begin
34
+ fetch_without_retry(uri, method, headers, params, referer, redirects)
35
+ rescue Net::HTTP::Persistent::Error => e
36
+ # Pass on any other type of error.
37
+ raise unless e.message =~ /too many connection resets/
38
+
39
+ # Pass on the error if we've tried too many times.
40
+ if retry_count >= MAX_RESET_RETRIES
41
+ puts "**** WARN: Mechanize retried connection reset #{MAX_RESET_RETRIES} times and never succeeded: #{action}"
42
+ raise
43
+ end
44
+
45
+ # Otherwise, shutdown the persistent HTTP connection and try again.
46
+ puts "**** WARN: Mechanize retrying connection reset error: #{action}"
47
+ retry_count += 1
48
+ self.http.shutdown
49
+ retry
50
+ end
51
+ end
52
+
53
+ # Alias so #fetch actually uses our new #fetch_with_retry to wrap the
54
+ # old one aliased as #fetch_without_retry.
55
+ alias_method :fetch_without_retry, :fetch
56
+ alias_method :fetch, :fetch_with_retry
57
+
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,7 @@
1
+ module MagnumPI
2
+ MAJOR = 0
3
+ MINOR = 1
4
+ TINY = 0
5
+
6
+ VERSION = [MAJOR, MINOR, TINY].join(".")
7
+ end
data/magnum-pi.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/magnum-pi/version", __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Paul Engel"]
6
+ gem.email = ["pm_engel@icloud.com"]
7
+ gem.summary = %q{Create an easy interface to talk with APIs}
8
+ gem.description = %q{Create an easy interface to talk with APIs}
9
+ gem.homepage = "https://github.com/archan937/magnum-pi"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "magnum-pi"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = MagnumPI::VERSION
17
+
18
+ gem.add_dependency "mechanize"
19
+ gem.add_dependency "oj"
20
+ gem.add_dependency "xml-simple"
21
+
22
+ gem.add_development_dependency "rake"
23
+ gem.add_development_dependency "pry"
24
+ gem.add_development_dependency "simplecov"
25
+ gem.add_development_dependency "minitest"
26
+ gem.add_development_dependency "mocha"
27
+ end
data/script/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler"
4
+ Bundler.require :default, :development
5
+
6
+ Dir[File.expand_path("../../examples/*.rb", __FILE__)].each do |example|
7
+ require example
8
+ end
9
+
10
+ puts "Loading MagnumPI development environment (#{MagnumPI::VERSION})"
11
+ Pry.start
@@ -0,0 +1,11 @@
1
+ require_relative "test_helper/coverage"
2
+
3
+ require "minitest/autorun"
4
+ require "mocha/setup"
5
+
6
+ def path(path)
7
+ File.expand_path "../../#{path}", __FILE__
8
+ end
9
+
10
+ require "bundler"
11
+ Bundler.require :default, :development, :test
@@ -0,0 +1,11 @@
1
+ if Dir.pwd == File.expand_path("../../..", __FILE__)
2
+ require "simplecov"
3
+ SimpleCov.coverage_dir "test/coverage"
4
+ SimpleCov.start do
5
+ add_group "MagnumPI", "lib"
6
+ add_group "Test suite", "test"
7
+ add_filter do |src|
8
+ src.filename.include?("gem_ext/mechanize")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,176 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module API
5
+ class TestConsumer < MiniTest::Test
6
+
7
+ class Consumer
8
+ include MagnumPI::API::Consumer
9
+ def to_params(url, *args)
10
+ args[0]
11
+ end
12
+ end
13
+
14
+ class IncompleteConsumer
15
+ include MagnumPI::API::Consumer
16
+ end
17
+
18
+ describe MagnumPI::API::Consumer do
19
+ before do
20
+ @consumer = Consumer.new
21
+ @consumer.stubs(:api).returns(
22
+ :uri => "http://foo.bar",
23
+ :format => :json
24
+ )
25
+ @consumer.stubs(:resources).returns(
26
+ :statistics => [:get, "stats", MagnumPI::API::Resources::Variable.new.tap{|var| var.name = :date}]
27
+ )
28
+ end
29
+ describe "#get" do
30
+ it "makes a GET request" do
31
+ response = mock
32
+ response.expects(:content).returns('{"name": "Paul Engel"}')
33
+ @consumer.expects(:request).with(:get, "http://foo.bar", {:foo => "bar"}).returns(response)
34
+ assert_equal({"name" => "Paul Engel"}, @consumer.get(:foo => "bar"))
35
+ end
36
+ end
37
+ describe "#post" do
38
+ it "makes a POST request" do
39
+ response = mock
40
+ response.expects(:content).returns('{"name": "Paul Engel"}')
41
+ @consumer.expects(:request).with(:post, "http://foo.bar", {:foo => "bar"}).returns(response)
42
+ assert_equal({"name" => "Paul Engel"}, @consumer.post(:foo => "bar"))
43
+ end
44
+ end
45
+ describe "#download" do
46
+ it "downloads using the Mechanize agent" do
47
+ response = mock
48
+ response.expects(:save_as).with("path/to/target")
49
+ @consumer.expects(:request).with(:get, "http://foo.bar", :foo => "bar").returns(response)
50
+ @consumer.download "path/to/target", :get, :foo => "bar"
51
+ end
52
+ end
53
+ describe "#resource" do
54
+ it "interpolates passed variables and makes a request" do
55
+ @consumer.expects(:send).with(:get, "stats", "2014-03-20")
56
+ @consumer.resource :statistics, :date => "2014-03-20"
57
+ end
58
+ it "interpolates passed variables and downloads a file" do
59
+ @consumer.expects(:send).with(:download, "path/to/target", :get, "stats", "2014-03-20")
60
+ @consumer.resource :statistics, {:date => "2014-03-20"}, "path/to/target"
61
+ end
62
+ end
63
+ describe "#method_missing" do
64
+ describe "corresponding resource" do
65
+ it "delegates to #resource" do
66
+ @consumer.expects(:resource).with(:statistics, :foo, :bar)
67
+ @consumer.statistics :foo, :bar
68
+ end
69
+ end
70
+ describe "no corresponding resource" do
71
+ it "raises a NoMethodError" do
72
+ assert_raises NoMethodError do
73
+ @consumer.foo
74
+ end
75
+ end
76
+ end
77
+ end
78
+ describe "#request" do
79
+ it "is delegated to the Mechanize agent" do
80
+ @consumer.send(:agent).expects(:send).with(:get, :foo, :bar)
81
+ @consumer.send(:request, :get, :foo, :bar)
82
+ end
83
+ it "raises an error when a Mechanize::ResponseCodeError occurs" do
84
+ agent = @consumer.send(:agent)
85
+ def agent.send(*args)
86
+ raise Mechanize::ResponseCodeError.new(Struct.new(:code).new)
87
+ end
88
+ assert_raises MagnumPI::API::Consumer::Error do
89
+ @consumer.get
90
+ end
91
+ end
92
+ end
93
+ describe "#agent" do
94
+ it "returns a memoized Mechanize agent" do
95
+ assert_equal Mechanize, @consumer.send(:agent).class
96
+ assert_equal @consumer.send(:agent).object_id, @consumer.send(:agent).object_id
97
+ end
98
+ it "is configured as expected" do
99
+ assert_equal OpenSSL::SSL::VERIFY_NONE, @consumer.send(:agent).verify_mode
100
+ assert_equal Mechanize::Download, @consumer.send(:agent).pluggable_parser.default
101
+ end
102
+ end
103
+ describe "#parse_args" do
104
+ it "derives the URL and params" do
105
+ @consumer.expects(:to_url).with({:foo => "bar"}).returns("http://foo.bar")
106
+ assert_equal ["http://foo.bar", {:foo => "bar"}], @consumer.send(:parse_args, {:foo => "bar"})
107
+ end
108
+ end
109
+ describe "#parse_resource_variables" do
110
+ it "returns an array containing arguments for making a request" do
111
+ resource = @consumer.resources[:statistics]
112
+ assert_equal [:get, "stats", nil], @consumer.send(:parse_resource_variables, resource, {})
113
+ assert_equal [:get, "stats", nil], @consumer.send(:parse_resource_variables, resource, {:DATE => "2014-03-20"})
114
+ assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, {:date => "2014-03-20"})
115
+ assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, {"date" => "2014-03-20"})
116
+ assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, ["2014-03-20"])
117
+ assert_equal [:get, "stats", "2014-03-20"], @consumer.send(:parse_resource_variables, resource, "2014-03-20")
118
+ assert_raises ArgumentError do
119
+ @consumer.send :parse_resource_variables, resource, ["2014-03-20", "foo"]
120
+ end
121
+
122
+ resource = [:post, "foobar"]
123
+ assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {})
124
+ assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {:DATE => "2014-03-20"})
125
+ assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {:date => "2014-03-20"})
126
+ assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, {"date" => "2014-03-20"})
127
+ assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, ["2014-03-20"])
128
+ assert_equal [:post, "foobar"], @consumer.send(:parse_resource_variables, resource, ["2014-03-20", "foo"])
129
+ end
130
+ end
131
+ describe "#to_url" do
132
+ it "returns api[:uri]" do
133
+ assert_equal "http://foo.bar", @consumer.send(:to_url)
134
+ end
135
+ end
136
+ describe "#to_params" do
137
+ it "raises a NotImplementedError" do
138
+ assert_raises NotImplementedError do
139
+ IncompleteConsumer.new.send :to_params, "url"
140
+ end
141
+ end
142
+ end
143
+ describe "#parse_content" do
144
+ it "parses the passed content" do
145
+ assert_equal(
146
+ {
147
+ "foo" => "bar"
148
+ }, @consumer.send(:parse_content,
149
+ <<-JSON
150
+ {"foo": "bar"}
151
+ JSON
152
+ )
153
+ )
154
+ @consumer.expects(:api).returns :format => "xml"
155
+ assert_equal(
156
+ {
157
+ "bar" => ["BAR"],
158
+ "baz" => [{"hello" => "world", "content" => "Baz!"}]
159
+ }, @consumer.send(:parse_content,
160
+ <<-XML
161
+ <xml>
162
+ <bar>BAR</bar>
163
+ <baz hello="world">Baz!</baz>
164
+ </xml>
165
+ XML
166
+ )
167
+ )
168
+ @consumer.expects(:api).returns :format => "unknown"
169
+ assert_equal("foobar", @consumer.send(:parse_content, "foobar"))
170
+ end
171
+ end
172
+ end
173
+
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,53 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module API
5
+ class TestInstance < MiniTest::Test
6
+
7
+ class Foo
8
+ include MagnumPI::API::Instance
9
+ end
10
+
11
+ describe MagnumPI::API::Instance do
12
+ describe ".initialize" do
13
+ it "defines @api and @resources" do
14
+ api = mock
15
+ api.expects(:finalize).returns(api)
16
+ resources = mock
17
+ resources.expects(:to_hash).returns(resources)
18
+
19
+ Foo.expects(:api).returns api
20
+ Foo.expects(:resources).returns resources
21
+ foo = Foo.new
22
+
23
+ assert_equal api, foo.instance_variable_get(:@api)
24
+ assert_equal resources, foo.instance_variable_get(:@resources)
25
+ end
26
+ end
27
+ describe "instances" do
28
+ before do
29
+ api = mock
30
+ api.expects(:finalize).returns(api)
31
+ Foo.expects(:api).returns api
32
+ Foo.expects(:resources).returns Hash.new
33
+ end
34
+ describe "#api" do
35
+ it "returns @api" do
36
+ foo, api = Foo.new, api
37
+ foo.instance_variable_set :@api, api
38
+ assert_equal api, foo.send(:api)
39
+ end
40
+ end
41
+ describe "#resources" do
42
+ it "returns @resources" do
43
+ foo, resources = Foo.new, resources
44
+ foo.instance_variable_set :@resources, resources
45
+ assert_equal resources, foo.send(:resources)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module API
5
+ class TestResources < MiniTest::Test
6
+
7
+ describe MagnumPI::API::Resources do
8
+ describe ".var" do
9
+ it "returns a MagnumPI::API::Resources::Variable instance" do
10
+ variable = MagnumPI::API::Resources.new.var("foo")
11
+ assert_equal MagnumPI::API::Resources::Variable, variable.class
12
+ assert_equal "foo", variable.name
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,81 @@
1
+ require_relative "../../test_helper"
2
+
3
+ module Unit
4
+ module API
5
+ class TestScheme < MiniTest::Test
6
+
7
+ describe MagnumPI::API::Scheme do
8
+ before do
9
+ @scheme = MagnumPI::API::Scheme.new
10
+ end
11
+
12
+ describe "#initialize" do
13
+ it "presets `uri` and `format`" do
14
+ assert_equal({:uri => String, :format => Symbol}, @scheme.instance_eval("_types"))
15
+ end
16
+ end
17
+
18
+ describe "#class" do
19
+ it "returns MagnumPI::API::Scheme" do
20
+ assert_equal MagnumPI::API::Scheme, @scheme.class
21
+ end
22
+ end
23
+
24
+ describe "#finalize" do
25
+ it "foo" do
26
+ @scheme.foo String
27
+ @scheme.bar "bar"
28
+ object_id = @scheme.instance_eval("_values").object_id
29
+
30
+ assert_equal({:bar => "bar"}, @scheme.finalize)
31
+ assert_equal({:foo => "foo", :bar => "bar"}, @scheme.finalize(:foo => "foo"))
32
+ assert_equal object_id, @scheme.instance_eval("_values").object_id
33
+ assert_equal({:foo => "FOO", :bar => "bar"}, @scheme.finalize(:foo => "FOO"))
34
+
35
+ assert_raises ArgumentError do
36
+ @scheme.finalize :foo => :foo
37
+ end
38
+ end
39
+ end
40
+
41
+ describe "#_types" do
42
+ it "is an internal memoized hash" do
43
+ assert_equal true, @scheme.instance_eval("_types").is_a?(Hash)
44
+ assert_equal @scheme.instance_eval("_types").object_id, @scheme.instance_eval("_types").object_id
45
+ end
46
+ end
47
+
48
+ describe "types" do
49
+ describe "defining" do
50
+ it "can only be done once" do
51
+ assert_equal({:uri => String, :format => Symbol}, @scheme.instance_eval("_types"))
52
+ @scheme.format String
53
+ assert_equal({:uri => String, :format => Symbol}, @scheme.instance_eval("_types"))
54
+ @scheme.foo String
55
+ assert_equal({:uri => String, :format => Symbol, :foo => String}, @scheme.instance_eval("_types"))
56
+ @scheme.foo Symbol
57
+ assert_equal({:uri => String, :format => Symbol, :foo => String}, @scheme.instance_eval("_types"))
58
+ end
59
+ end
60
+ describe "validating" do
61
+ describe "when setting a valid value" do
62
+ it "allows value to be set" do
63
+ assert_equal({}, @scheme.instance_eval("_values"))
64
+ @scheme.format :json
65
+ assert_equal({:format => :json}, @scheme.instance_eval("_values"))
66
+ @scheme.format :xml
67
+ assert_equal({:format => :xml}, @scheme.instance_eval("_values"))
68
+ end
69
+ end
70
+ describe "when setting an invalid value" do
71
+ it "raises an error" do
72
+ assert_equal "Invalid value for 'format': \"json\" (expected Symbol)", assert_raises(ArgumentError){ @scheme.format "json" }.message
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+ end
81
+ end