typhoeus 0.5.0.pre → 0.5.0.rc
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/CHANGELOG.md +12 -0
- data/Gemfile +17 -1
- data/README.md +2 -2
- data/lib/typhoeus.rb +78 -16
- data/lib/typhoeus/config.rb +40 -4
- data/lib/typhoeus/errors.rb +9 -0
- data/lib/typhoeus/errors/no_stub.rb +12 -0
- data/lib/typhoeus/errors/typhoeus_error.rb +8 -0
- data/lib/typhoeus/expectation.rb +174 -0
- data/lib/typhoeus/hydra.rb +71 -14
- data/lib/typhoeus/hydra/before.rb +30 -0
- data/lib/typhoeus/hydra/block_connection.rb +35 -0
- data/lib/typhoeus/{hydras → hydra}/easy_factory.rb +17 -5
- data/lib/typhoeus/{hydras → hydra}/easy_pool.rb +4 -2
- data/lib/typhoeus/{hydras → hydra}/memoizable.rb +7 -5
- data/lib/typhoeus/{hydras → hydra}/queueable.rb +5 -3
- data/lib/typhoeus/{hydras → hydra}/runnable.rb +4 -1
- data/lib/typhoeus/hydra/stubbable.rb +26 -0
- data/lib/typhoeus/request.rb +117 -29
- data/lib/typhoeus/request/actions.rb +125 -0
- data/lib/typhoeus/request/before.rb +30 -0
- data/lib/typhoeus/request/block_connection.rb +52 -0
- data/lib/typhoeus/request/callbacks.rb +98 -0
- data/lib/typhoeus/{requests → request}/marshal.rb +1 -1
- data/lib/typhoeus/{requests → request}/memoizable.rb +4 -2
- data/lib/typhoeus/{requests → request}/operations.rb +25 -5
- data/lib/typhoeus/{requests → request}/responseable.rb +1 -1
- data/lib/typhoeus/request/stubbable.rb +28 -0
- data/lib/typhoeus/response.rb +30 -8
- data/lib/typhoeus/{responses → response}/header.rb +15 -11
- data/lib/typhoeus/response/informations.rb +205 -0
- data/lib/typhoeus/{responses → response}/status.rb +10 -7
- data/lib/typhoeus/version.rb +1 -1
- metadata +32 -135
- data/lib/typhoeus/requests/actions.rb +0 -17
- data/lib/typhoeus/requests/callbacks.rb +0 -48
- data/lib/typhoeus/responses/informations.rb +0 -43
- data/lib/typhoeus/responses/legacy.rb +0 -26
data/CHANGELOG.md
CHANGED
@@ -20,6 +20,7 @@ Major Changes:
|
|
20
20
|
for a description.
|
21
21
|
* The following classes were deleted because they do not seemed to be uesed at all. If that
|
22
22
|
turns out to be wrong, they will be restored: `Typhoeus::Filter`, `Typhoeus::Remote`, `Typhoeus::RemoteMethod`, `Typhoeus::RemoteProxyObject`
|
23
|
+
* `Typhoeus::Easy` and `Typhoeus::Multi` are now `Ethon::Easy` and `Ethon::Multi`
|
23
24
|
|
24
25
|
* Request shortcuts: `Typhoeus.get("www.google.de")`
|
25
26
|
* Global configuration:
|
@@ -32,6 +33,17 @@ end
|
|
32
33
|
* No more Response#headers_hash, instead response#header returning the last
|
33
34
|
header and response#redirections returning the responses with headers
|
34
35
|
generated through redirections
|
36
|
+
* Instead of defining the same callbacks on every request, you can define global callbacks:
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
Typhoeus.on_complete { p "yay" }
|
40
|
+
```
|
41
|
+
|
42
|
+
* The stubbing interface changed slightly. You now have the same syntax as for requests:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
Typhoeus.stub(url, options).and_return(response)
|
46
|
+
```
|
35
47
|
|
36
48
|
Enhancements:
|
37
49
|
|
data/Gemfile
CHANGED
@@ -1,3 +1,19 @@
|
|
1
1
|
source :rubygems
|
2
|
-
|
3
2
|
gemspec
|
3
|
+
|
4
|
+
gem "rake"
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem "rspec", "~> 2.11"
|
8
|
+
|
9
|
+
gem "sinatra", "~> 1.3"
|
10
|
+
gem "json"
|
11
|
+
|
12
|
+
if RUBY_PLATFORM == "java"
|
13
|
+
gem "spoon"
|
14
|
+
end
|
15
|
+
|
16
|
+
unless ENV["CI"]
|
17
|
+
gem "guard-rspec", "~> 0.7"
|
18
|
+
end
|
19
|
+
end
|
data/README.md
CHANGED
@@ -20,8 +20,8 @@ hydra.run
|
|
20
20
|
|
21
21
|
## Project Tracking
|
22
22
|
|
23
|
-
* [Documentation](http://rubydoc.info/github/typhoeus/typhoeus)
|
24
|
-
* [Website](http://typhoeus.github.com/)
|
23
|
+
* [Documentation](http://rubydoc.info/github/typhoeus/typhoeus) (github master)
|
24
|
+
* [Website](http://typhoeus.github.com/) (v0.4.2)
|
25
25
|
* [Mailinglist](http://groups.google.com/group/typhoeus)
|
26
26
|
|
27
27
|
## LICENSE
|
data/lib/typhoeus.rb
CHANGED
@@ -2,41 +2,103 @@ require 'digest/sha2'
|
|
2
2
|
require 'ethon'
|
3
3
|
|
4
4
|
require 'typhoeus/config'
|
5
|
+
require 'typhoeus/errors'
|
6
|
+
require 'typhoeus/expectation'
|
7
|
+
require 'typhoeus/hydra'
|
5
8
|
require 'typhoeus/request'
|
6
9
|
require 'typhoeus/response'
|
7
|
-
require 'typhoeus/hydra'
|
8
10
|
require 'typhoeus/version'
|
9
11
|
|
10
12
|
# Typhoeus is a http client library based on Ethon which
|
11
13
|
# wraps libcurl.
|
12
14
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
+
# @example (see Typhoeus::Request)
|
16
|
+
# @example (see Typhoeus::Hydra)
|
15
17
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
+
# @see Typhoeus::Request
|
19
|
+
# @see Typhoeus::Hydra
|
18
20
|
#
|
19
|
-
#
|
20
|
-
# requests = (0..9).map{ Typhoeus::Request.new("www.example.com") }
|
21
|
-
# requests.each{ |request| hydra.queue(request) }
|
22
|
-
# hydra.run
|
21
|
+
# @since 0.5.0
|
23
22
|
module Typhoeus
|
24
23
|
extend self
|
25
|
-
extend
|
26
|
-
extend
|
24
|
+
extend Hydra::EasyPool
|
25
|
+
extend Request::Actions
|
26
|
+
extend Request::Callbacks::Types
|
27
27
|
|
28
28
|
# The default typhoeus user agent.
|
29
29
|
USER_AGENT = "Typhoeus - https://github.com/typhoeus/typhoeus"
|
30
30
|
|
31
31
|
# Set the Typhoeus configuration options by passing a block.
|
32
32
|
#
|
33
|
-
# @example
|
34
|
-
#
|
35
|
-
#
|
36
|
-
# end
|
33
|
+
# @example (see Typhoeus::Config)
|
34
|
+
#
|
35
|
+
# @yield [ Typhoeus::Config ]
|
37
36
|
#
|
38
|
-
# @return [ Config ] The configuration
|
37
|
+
# @return [ Typhoeus::Config ] The configuration.
|
38
|
+
#
|
39
|
+
# @see Typhoeus::Config
|
39
40
|
def configure
|
40
41
|
yield Config
|
41
42
|
end
|
43
|
+
|
44
|
+
# Stub out specific request.
|
45
|
+
#
|
46
|
+
# @example (see Typhoeus::Expectation)
|
47
|
+
#
|
48
|
+
# @param [ String ] url The url to stub out.
|
49
|
+
# @param [ Hash ] options The options to stub out.
|
50
|
+
#
|
51
|
+
# @return [ Typhoeus::Expectation ] The expecatation.
|
52
|
+
#
|
53
|
+
# @see Typhoeus::Expectation
|
54
|
+
def stub(url, options = {})
|
55
|
+
expectation = Expectation.all.find{ |e| e.url == url && e.options == options }
|
56
|
+
return expectation if expectation
|
57
|
+
|
58
|
+
Expectation.new(url, options).tap do |new_expectation|
|
59
|
+
Expectation.all << new_expectation
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Add before callbacks.
|
64
|
+
#
|
65
|
+
# @example Add before callback.
|
66
|
+
# Typhoeus.before { |request| p request.url }
|
67
|
+
#
|
68
|
+
# @param [ Block ] block The callback.
|
69
|
+
#
|
70
|
+
# @yield [ Typhoeus::Request ]
|
71
|
+
#
|
72
|
+
# @return [ Array<Block> ] All before blocks.
|
73
|
+
def before(&block)
|
74
|
+
@before ||= []
|
75
|
+
@before << block if block_given?
|
76
|
+
@before
|
77
|
+
end
|
78
|
+
|
79
|
+
# Execute given block as if block connection is turned off.
|
80
|
+
# The old block connection state is restored afterwards.
|
81
|
+
#
|
82
|
+
# @example Make a real request, no matter if its blocked.
|
83
|
+
# Typhoeus::Config.block_connection = true
|
84
|
+
# Typhoeus.get("www.example.com").code
|
85
|
+
# #=> raise Typhoeus::Errors::NoStub
|
86
|
+
#
|
87
|
+
# Typhoeus.with_connection do
|
88
|
+
# Typhoeus.get("www.example.com").code
|
89
|
+
# #=> :ok
|
90
|
+
# end
|
91
|
+
#
|
92
|
+
# @param [ Block ] block The block to execute.
|
93
|
+
#
|
94
|
+
# @return [ Object ] Returns the return value of block.
|
95
|
+
#
|
96
|
+
# @see Typhoeus::Config#block_connection
|
97
|
+
def with_connection
|
98
|
+
old = Config.block_connection
|
99
|
+
Config.block_connection = false
|
100
|
+
result = yield if block_given?
|
101
|
+
Config.block_connection = old
|
102
|
+
result
|
103
|
+
end
|
42
104
|
end
|
data/lib/typhoeus/config.rb
CHANGED
@@ -1,11 +1,47 @@
|
|
1
1
|
module Typhoeus
|
2
2
|
|
3
3
|
# The Typhoeus configuration used to set global
|
4
|
-
# options.
|
5
|
-
#
|
6
|
-
#
|
4
|
+
# options.
|
5
|
+
# @example Set the configuration options within a block.
|
6
|
+
# Typhoeus.configure do |config|
|
7
|
+
# config.verbose = true
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# @example Set the configuration direct.
|
11
|
+
# Typhoeus::Config.verbose = true
|
7
12
|
module Config
|
8
13
|
extend self
|
9
|
-
|
14
|
+
|
15
|
+
# Defines wether the connection is blocked.
|
16
|
+
# Defaults to false. When set to true, only
|
17
|
+
# stubbed requests are allowed. A
|
18
|
+
# {Typhoeus::Errors::NoStub} error is raised,
|
19
|
+
# when trying to do a real request. Its possible
|
20
|
+
# to work around inside
|
21
|
+
# {Typhoeus#with_connection}.
|
22
|
+
#
|
23
|
+
# @return [ Boolean ]
|
24
|
+
#
|
25
|
+
# @see Typhoeus::Request::BlockConnection
|
26
|
+
# @see Typhoeus::Hydra::BlockConnection
|
27
|
+
# @see Typhoeus#with_connection
|
28
|
+
# @see Typhoeus::Errors::NoStub
|
29
|
+
attr_accessor :block_connection
|
30
|
+
|
31
|
+
# Defines wether GET requests are memoized when using the {Typhoeus::Hydra}.
|
32
|
+
#
|
33
|
+
# @return [ Boolean ]
|
34
|
+
#
|
35
|
+
# @see Typhoeus::Hydra
|
36
|
+
# @see Typhoeus::Hydra::Memoizable
|
37
|
+
attr_accessor :memoize
|
38
|
+
|
39
|
+
# Defines wether curls debug output is shown.
|
40
|
+
# Unfortunately it prints to stderr.
|
41
|
+
#
|
42
|
+
# @return [ Boolean ]
|
43
|
+
#
|
44
|
+
# @see http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTVERBOSE
|
45
|
+
attr_accessor :verbose
|
10
46
|
end
|
11
47
|
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
module Typhoeus
|
2
|
+
|
3
|
+
# This class represents an expectation. It is part
|
4
|
+
# of the stubbing mechanism. An expectation contains
|
5
|
+
# an url and options, like a request. They were compared
|
6
|
+
# to the request url and options in order to evaluate
|
7
|
+
# wether they match. If thats the case, the attached
|
8
|
+
# responses were returned one by one.
|
9
|
+
#
|
10
|
+
# @example Stub a request and get specified response.
|
11
|
+
# expected = Typhoeus::Response.new
|
12
|
+
# Typhoeus.stub("www.example.com").and_return(expected)
|
13
|
+
#
|
14
|
+
# actual = Typhoeus.get("www.example.com")
|
15
|
+
# expected == actual
|
16
|
+
# #=> true
|
17
|
+
class Expectation
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
attr_reader :url
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
attr_reader :options
|
24
|
+
|
25
|
+
# @api private
|
26
|
+
attr_reader :from
|
27
|
+
|
28
|
+
class << self
|
29
|
+
|
30
|
+
# Returns all expectations.
|
31
|
+
#
|
32
|
+
# @example Return expectations.
|
33
|
+
# Typhoeus::Expectation.all
|
34
|
+
#
|
35
|
+
# @return [ Array<Typhoeus::Expectation> ] The expectations.
|
36
|
+
def all
|
37
|
+
@expectations ||= []
|
38
|
+
end
|
39
|
+
|
40
|
+
# Clears expectations. This is handy while
|
41
|
+
# testing and you want to make sure, that
|
42
|
+
# you don't get canned responses.
|
43
|
+
#
|
44
|
+
# @example Clear expectations.
|
45
|
+
# Typhoeus::Expectation.clear
|
46
|
+
def clear
|
47
|
+
all.clear
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns expecation matching the provided
|
51
|
+
# request.
|
52
|
+
#
|
53
|
+
# @example Find expectation.
|
54
|
+
# Typhoeus::Expectation.find_by(request)
|
55
|
+
#
|
56
|
+
# @return [ Expectation ] The matching expectation.
|
57
|
+
#
|
58
|
+
# @api private
|
59
|
+
def find_by(request)
|
60
|
+
all.find do |expectation|
|
61
|
+
expectation.matches?(request)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Creates an expactation.
|
67
|
+
#
|
68
|
+
# @example Create expactation.
|
69
|
+
# Typhoeus::Expectation.new(url)
|
70
|
+
#
|
71
|
+
# @return [ Expectation ] The created expactation.
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
def initialize(url, options = {})
|
75
|
+
@url = url
|
76
|
+
@options = options
|
77
|
+
@response_counter = 0
|
78
|
+
@from = nil
|
79
|
+
end
|
80
|
+
|
81
|
+
# Set from value to mark an expecation. Useful for
|
82
|
+
# other libraries, eg. webmock.
|
83
|
+
#
|
84
|
+
# @example Mark expecation.
|
85
|
+
# expecation.from(:webmock)
|
86
|
+
#
|
87
|
+
# @param [ String ] value Value to set.
|
88
|
+
#
|
89
|
+
# @return [ Expectation ] Returns self.
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
def stubbed_from(value)
|
93
|
+
@from = value
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
# Specify what should be returned,
|
98
|
+
# when this expactation is hit.
|
99
|
+
#
|
100
|
+
# @example Add response.
|
101
|
+
# expectation.and_return(response)
|
102
|
+
#
|
103
|
+
# @return [ void ]
|
104
|
+
def and_return(response)
|
105
|
+
responses << response
|
106
|
+
end
|
107
|
+
|
108
|
+
# Checks wether this expectation matches
|
109
|
+
# the provided request.
|
110
|
+
#
|
111
|
+
# @example Check if request matches.
|
112
|
+
# expectation.matches? request
|
113
|
+
#
|
114
|
+
# @param [ Request ] request The request to check.
|
115
|
+
#
|
116
|
+
# @return [ Boolean ] True when matches, else false.
|
117
|
+
#
|
118
|
+
# @api private
|
119
|
+
def matches?(request)
|
120
|
+
url_match?(request.url) &&
|
121
|
+
(options ? options.all?{ |k,v| request.original_options[k] == v } : true)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Return canned responses.
|
125
|
+
#
|
126
|
+
# @example Return responses.
|
127
|
+
# expectation.responses
|
128
|
+
#
|
129
|
+
# @return [ Array<Typhoeus::Response> ] The responses.
|
130
|
+
#
|
131
|
+
# @api private
|
132
|
+
def responses
|
133
|
+
@responses ||= []
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return the response. When there are
|
137
|
+
# multiple responses, they were returned one
|
138
|
+
# by one.
|
139
|
+
#
|
140
|
+
# @example Return response.
|
141
|
+
# expectation.response
|
142
|
+
#
|
143
|
+
# @return [ Response ] The response.
|
144
|
+
#
|
145
|
+
# @api private
|
146
|
+
def response
|
147
|
+
response = responses.fetch(@response_counter, responses.last)
|
148
|
+
@response_counter += 1
|
149
|
+
response.mock = @from || true
|
150
|
+
response
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# Check wether the url matches the request url.
|
156
|
+
# The url can be a string, regex or nil. String and
|
157
|
+
# regexp were checked, nil is always true. Else false.
|
158
|
+
#
|
159
|
+
# Nil serves as a placeholder in case you want to match
|
160
|
+
# all urls.
|
161
|
+
def url_match?(request_url)
|
162
|
+
case url
|
163
|
+
when String
|
164
|
+
url == request_url
|
165
|
+
when Regexp
|
166
|
+
!!request_url.match(url)
|
167
|
+
when nil
|
168
|
+
true
|
169
|
+
else
|
170
|
+
false
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/typhoeus/hydra.rb
CHANGED
@@ -1,37 +1,94 @@
|
|
1
|
-
require 'typhoeus/
|
2
|
-
require 'typhoeus/
|
3
|
-
require 'typhoeus/
|
4
|
-
require 'typhoeus/
|
5
|
-
require 'typhoeus/
|
1
|
+
require 'typhoeus/hydra/before'
|
2
|
+
require 'typhoeus/hydra/block_connection'
|
3
|
+
require 'typhoeus/hydra/easy_factory'
|
4
|
+
require 'typhoeus/hydra/easy_pool'
|
5
|
+
require 'typhoeus/hydra/memoizable'
|
6
|
+
require 'typhoeus/hydra/queueable'
|
7
|
+
require 'typhoeus/hydra/runnable'
|
8
|
+
require 'typhoeus/hydra/stubbable'
|
6
9
|
|
7
10
|
module Typhoeus
|
8
11
|
|
9
12
|
# Hydra manages making parallel HTTP requests. This
|
10
|
-
# is
|
11
|
-
#
|
13
|
+
# is achieved by using libcurls multi interface:
|
14
|
+
# http://curl.haxx.se/libcurl/c/libcurl-multi.html
|
15
|
+
# The benefits are that you don't have to worry running
|
12
16
|
# the requests by yourself.
|
17
|
+
#
|
18
|
+
# Hydra will also handle how many requests you can
|
19
|
+
# make in parallel. Things will get flakey if you
|
20
|
+
# try to make too many requests at the same time.
|
21
|
+
# The built in limit is 200. When more requests than
|
22
|
+
# that are queued up, hydra will save them for later
|
23
|
+
# and start the requests as others are finished. You
|
24
|
+
# can raise or lower the concurrency limit through
|
25
|
+
# the Hydra constructor.
|
26
|
+
#
|
27
|
+
# Regarding the asynchronous behavior of the hydra,
|
28
|
+
# it is important to know that this is completely hidden
|
29
|
+
# from the developer and you are free to apply
|
30
|
+
# whatever technique you want to your code. That should not
|
31
|
+
# conflict with libcurls internal concurrency mechanism.
|
32
|
+
#
|
33
|
+
# @example Use the hydra to do multiple requests.
|
34
|
+
# hydra = Typhoeus::Hydra.new
|
35
|
+
# requests = (0..9).map{ Typhoeus::Request.new("www.example.com") }
|
36
|
+
# requests.each{ |request| hydra.queue(request) }
|
37
|
+
# hydra.run
|
13
38
|
class Hydra
|
14
|
-
include
|
15
|
-
include
|
16
|
-
include
|
17
|
-
include
|
39
|
+
include Hydra::EasyPool
|
40
|
+
include Hydra::Queueable
|
41
|
+
include Hydra::Runnable
|
42
|
+
include Hydra::Memoizable
|
43
|
+
include Hydra::BlockConnection
|
44
|
+
include Hydra::Stubbable
|
45
|
+
include Hydra::Before
|
18
46
|
|
19
|
-
|
47
|
+
# @example Set max_concurrency.
|
48
|
+
# Typhoeus::Hydra.new(:max_concurrency => 20)
|
49
|
+
attr_reader :max_concurrency
|
20
50
|
|
21
|
-
#
|
51
|
+
# @api private
|
52
|
+
attr_reader :multi
|
53
|
+
|
54
|
+
class << self
|
55
|
+
|
56
|
+
# Returns a memoized hydra instance.
|
57
|
+
#
|
58
|
+
# @example Get a hydra.
|
59
|
+
# Typhoeus::Hydra.hydra
|
60
|
+
#
|
61
|
+
# @return [Typhoeus::Hydra] A new hydra.
|
62
|
+
#
|
63
|
+
# @deprecated This is only for convenience because so
|
64
|
+
# much external code relies on it.
|
65
|
+
def hydra
|
66
|
+
@hydra ||= new
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create a new hydra. All
|
71
|
+
# {http://rubydoc.info/github/typhoeus/ethon/Ethon/Multi#initialize-instance_method Ethon::Multi#initialize}
|
72
|
+
# options are also available.
|
22
73
|
#
|
23
74
|
# @example Create a hydra.
|
24
75
|
# Typhoeus::Hydra.new
|
25
76
|
#
|
77
|
+
# @example Create a hydra with max_concurrency.
|
78
|
+
# Typhoeus::Hydra.new(:max_concurrency => 20)
|
79
|
+
#
|
26
80
|
# @param [ Hash ] options The options hash.
|
27
81
|
#
|
28
82
|
# @option options :max_concurrency [ Integer ] Number
|
29
83
|
# of max concurrent connections to create. Default is
|
30
84
|
# 200.
|
85
|
+
#
|
86
|
+
# @see http://rubydoc.info/github/typhoeus/ethon/Ethon/Multi#initialize-instance_method
|
87
|
+
# Ethon::Multi#initialize
|
31
88
|
def initialize(options = {})
|
32
89
|
@options = options
|
33
90
|
@max_concurrency = @options.fetch(:max_concurrency, 200)
|
34
|
-
@multi = Ethon::Multi.new
|
91
|
+
@multi = Ethon::Multi.new(options.reject{|k,_| k==:max_concurrency})
|
35
92
|
end
|
36
93
|
end
|
37
94
|
end
|