lgierth-rack-mount 0.6.13
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +36 -0
- data/lib/rack/mount.rb +32 -0
- data/lib/rack/mount/analysis/frequency.rb +60 -0
- data/lib/rack/mount/analysis/histogram.rb +74 -0
- data/lib/rack/mount/analysis/splitting.rb +159 -0
- data/lib/rack/mount/code_generation.rb +117 -0
- data/lib/rack/mount/generatable_regexp.rb +210 -0
- data/lib/rack/mount/multimap.rb +53 -0
- data/lib/rack/mount/prefix.rb +36 -0
- data/lib/rack/mount/regexp_with_named_groups.rb +69 -0
- data/lib/rack/mount/route.rb +130 -0
- data/lib/rack/mount/route_set.rb +420 -0
- data/lib/rack/mount/strexp.rb +68 -0
- data/lib/rack/mount/strexp/parser.rb +160 -0
- data/lib/rack/mount/strexp/parser.y +34 -0
- data/lib/rack/mount/strexp/tokenizer.rb +83 -0
- data/lib/rack/mount/strexp/tokenizer.rex +12 -0
- data/lib/rack/mount/utils.rb +162 -0
- data/lib/rack/mount/vendor/multimap/multimap.rb +569 -0
- data/lib/rack/mount/vendor/multimap/multiset.rb +185 -0
- data/lib/rack/mount/vendor/multimap/nested_multimap.rb +158 -0
- data/lib/rack/mount/vendor/regin/regin.rb +75 -0
- data/lib/rack/mount/vendor/regin/regin/alternation.rb +40 -0
- data/lib/rack/mount/vendor/regin/regin/anchor.rb +4 -0
- data/lib/rack/mount/vendor/regin/regin/atom.rb +54 -0
- data/lib/rack/mount/vendor/regin/regin/character.rb +51 -0
- data/lib/rack/mount/vendor/regin/regin/character_class.rb +50 -0
- data/lib/rack/mount/vendor/regin/regin/collection.rb +77 -0
- data/lib/rack/mount/vendor/regin/regin/expression.rb +126 -0
- data/lib/rack/mount/vendor/regin/regin/group.rb +85 -0
- data/lib/rack/mount/vendor/regin/regin/options.rb +55 -0
- data/lib/rack/mount/vendor/regin/regin/parser.rb +520 -0
- data/lib/rack/mount/vendor/regin/regin/tokenizer.rb +246 -0
- data/lib/rack/mount/vendor/regin/regin/version.rb +3 -0
- data/lib/rack/mount/version.rb +3 -0
- metadata +140 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Joshua Peek
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
= Rack::Mount
|
2
|
+
|
3
|
+
A stackable dynamic tree based Rack router.
|
4
|
+
|
5
|
+
Rack::Mount supports Rack's +X-Cascade+ convention to continue trying routes if the response returns +pass+. This allows multiple routes to be nested or stacked on top of each other. Since the application endpoint can trigger the router to continue matching, middleware can be used to add arbitrary conditions to any route. This allows you to route based on other request attributes, session information, or even data dynamically pulled from a database.
|
6
|
+
|
7
|
+
=== Usage
|
8
|
+
|
9
|
+
Rack::Mount provides a plugin API to build custom DSLs on top of.
|
10
|
+
|
11
|
+
The API is extremely minimal and only 3 methods are exposed as the public API.
|
12
|
+
|
13
|
+
<tt>Rack::Mount::RouteSet#add_route</tt>:: builder method for adding routes to the set
|
14
|
+
<tt>Rack::Mount::RouteSet#call</tt>:: Rack compatible recognition and dispatching method
|
15
|
+
<tt>Rack::Mount::RouteSet#generate</tt>:: generates a route condition from identifiers or significant keys
|
16
|
+
|
17
|
+
=== Example
|
18
|
+
|
19
|
+
require 'rack/mount'
|
20
|
+
|
21
|
+
Routes = Rack::Mount::RouteSet.new do |set|
|
22
|
+
# add_route takes a rack application and conditions to match with
|
23
|
+
#
|
24
|
+
# valid conditions methods are any method on Rack::Request
|
25
|
+
# the values to match against may be strings or regexps
|
26
|
+
#
|
27
|
+
# See Rack::Mount::RouteSet#add_route for more options.
|
28
|
+
set.add_route FooApp, { :request_method => 'GET', :path_info => %r{^/foo$} }, {}, :foo
|
29
|
+
end
|
30
|
+
|
31
|
+
# The route set itself is a simple rack app you mount
|
32
|
+
run Routes
|
33
|
+
|
34
|
+
|
35
|
+
# generate path for route named "foo"
|
36
|
+
Routes.generate(:path_info, :foo) #=> "/foo"
|
data/lib/rack/mount.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Rack #:nodoc:
|
4
|
+
# A stackable dynamic tree based Rack router.
|
5
|
+
#
|
6
|
+
# Rack::Mount supports Rack's Cascade style of trying several routes until
|
7
|
+
# it finds one that is not a 404. This allows multiple routes to be nested
|
8
|
+
# or stacked on top of each other. Since the application endpoint can
|
9
|
+
# trigger the router to continue matching, middleware can be used to add
|
10
|
+
# arbitrary conditions to any route. This allows you to route based on
|
11
|
+
# other request attributes, session information, or even data dynamically
|
12
|
+
# pulled from a database.
|
13
|
+
module Mount
|
14
|
+
autoload :CodeGeneration, 'rack/mount/code_generation'
|
15
|
+
autoload :GeneratableRegexp, 'rack/mount/generatable_regexp'
|
16
|
+
autoload :Multimap, 'rack/mount/multimap'
|
17
|
+
autoload :Prefix, 'rack/mount/prefix'
|
18
|
+
autoload :RegexpWithNamedGroups, 'rack/mount/regexp_with_named_groups'
|
19
|
+
autoload :Route, 'rack/mount/route'
|
20
|
+
autoload :RouteSet, 'rack/mount/route_set'
|
21
|
+
autoload :RoutingError, 'rack/mount/route_set'
|
22
|
+
autoload :Strexp, 'rack/mount/strexp'
|
23
|
+
autoload :Utils, 'rack/mount/utils'
|
24
|
+
autoload :Version, 'rack/mount/version'
|
25
|
+
|
26
|
+
module Analysis #:nodoc:
|
27
|
+
autoload :Frequency, 'rack/mount/analysis/frequency'
|
28
|
+
autoload :Histogram, 'rack/mount/analysis/histogram'
|
29
|
+
autoload :Splitting, 'rack/mount/analysis/splitting'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
module Analysis
|
5
|
+
class Frequency #:nodoc:
|
6
|
+
def initialize(*keys)
|
7
|
+
clear
|
8
|
+
keys.each { |key| self << key }
|
9
|
+
end
|
10
|
+
|
11
|
+
def clear
|
12
|
+
@raw_keys = []
|
13
|
+
@key_frequency = Analysis::Histogram.new
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(key)
|
18
|
+
raise ArgumentError unless key.is_a?(Hash)
|
19
|
+
@raw_keys << key
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def possible_keys
|
24
|
+
@possible_keys ||= begin
|
25
|
+
@raw_keys.map do |key|
|
26
|
+
key.inject({}) { |requirements, (method, requirement)|
|
27
|
+
process_key(requirements, method, requirement)
|
28
|
+
requirements
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def process_key(requirements, method, requirement)
|
35
|
+
if requirement.is_a?(Regexp)
|
36
|
+
expression = Utils.parse_regexp(requirement)
|
37
|
+
|
38
|
+
if expression.is_a?(Regin::Expression) && expression.anchored_to_line?
|
39
|
+
expression = Regin::Expression.new(expression.reject { |e| e.is_a?(Regin::Anchor) })
|
40
|
+
return requirements[method] = expression.to_s if expression.literal?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
requirements[method] = requirement
|
45
|
+
end
|
46
|
+
|
47
|
+
def report
|
48
|
+
@report ||= begin
|
49
|
+
possible_keys.each { |keys| keys.each_pair { |key, _| @key_frequency << key } }
|
50
|
+
return [] if @key_frequency.count <= 1
|
51
|
+
@key_frequency.keys_in_upper_quartile
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def expire!
|
56
|
+
@possible_keys = @report = nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Rack::Mount
|
2
|
+
module Analysis
|
3
|
+
class Histogram < Hash #:nodoc:
|
4
|
+
attr_reader :count
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@count = 0
|
8
|
+
super(0)
|
9
|
+
expire_caches!
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(value)
|
13
|
+
@count += 1
|
14
|
+
self[value] += 1 if value
|
15
|
+
expire_caches!
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def sorted_by_frequency
|
20
|
+
sort_by { |_, value| value }.reverse!
|
21
|
+
end
|
22
|
+
|
23
|
+
def max
|
24
|
+
@max ||= values.max || 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def min
|
28
|
+
@min ||= values.min || 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def mean
|
32
|
+
@mean ||= calculate_mean
|
33
|
+
end
|
34
|
+
|
35
|
+
def standard_deviation
|
36
|
+
@standard_deviation ||= calculate_standard_deviation
|
37
|
+
end
|
38
|
+
|
39
|
+
def upper_quartile_limit
|
40
|
+
@upper_quartile_limit ||= calculate_upper_quartile_limit
|
41
|
+
end
|
42
|
+
|
43
|
+
def keys_in_upper_quartile
|
44
|
+
@keys_in_upper_quartile ||= compute_keys_in_upper_quartile
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def calculate_mean
|
49
|
+
count / size
|
50
|
+
end
|
51
|
+
|
52
|
+
def calculate_variance
|
53
|
+
values.inject(0) { |sum, e| sum + (e - mean) ** 2 } / count.to_f
|
54
|
+
end
|
55
|
+
|
56
|
+
def calculate_standard_deviation
|
57
|
+
Math.sqrt(calculate_variance)
|
58
|
+
end
|
59
|
+
|
60
|
+
def calculate_upper_quartile_limit
|
61
|
+
mean + standard_deviation
|
62
|
+
end
|
63
|
+
|
64
|
+
def compute_keys_in_upper_quartile
|
65
|
+
sorted_by_frequency.select { |_, value| value >= upper_quartile_limit }.map! { |key, _| key }
|
66
|
+
end
|
67
|
+
|
68
|
+
def expire_caches!
|
69
|
+
@max = @min = @mean = @standard_deviation = nil
|
70
|
+
@keys_in_upper_quartile = nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require 'rack/mount/utils'
|
2
|
+
|
3
|
+
module Rack::Mount
|
4
|
+
module Analysis
|
5
|
+
class Splitting < Frequency
|
6
|
+
NULL = "\0".freeze
|
7
|
+
|
8
|
+
class Key < Struct.new(:method, :index, :separators)
|
9
|
+
def self.split(value, separator_pattern)
|
10
|
+
keys = value.split(separator_pattern)
|
11
|
+
keys.shift if keys[0] == ''
|
12
|
+
keys << NULL
|
13
|
+
keys
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(cache, obj)
|
17
|
+
(cache[method] ||= self.class.split(obj.send(method), separators))[index]
|
18
|
+
end
|
19
|
+
|
20
|
+
def call_source(cache, obj)
|
21
|
+
"(#{cache}[:#{method}] ||= Analysis::Splitting::Key.split(#{obj}.#{method}, #{separators.inspect}))[#{index}]"
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#{method}[#{index}].split(#{separators.inspect})"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear
|
30
|
+
@boundaries = {}
|
31
|
+
super
|
32
|
+
end
|
33
|
+
|
34
|
+
def <<(key)
|
35
|
+
super
|
36
|
+
key.each_pair do |k, v|
|
37
|
+
analyze_capture_boundaries(v, @boundaries[k] ||= Histogram.new)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def separators(key)
|
42
|
+
@separators ||= {}
|
43
|
+
@separators[key] ||= lookup_separators(key)
|
44
|
+
end
|
45
|
+
attr_writer :separators
|
46
|
+
|
47
|
+
def lookup_separators(key)
|
48
|
+
@boundaries[key].keys_in_upper_quartile
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_key(requirements, method, requirement)
|
52
|
+
separators = separators(method)
|
53
|
+
if requirement.is_a?(Regexp) && separators.any?
|
54
|
+
generate_split_keys(requirement, separators).each_with_index do |value, index|
|
55
|
+
requirements[Key.new(method, index, Regexp.union(*separators))] = value
|
56
|
+
end
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def analyze_capture_boundaries(regexp, boundaries) #:nodoc:
|
64
|
+
return boundaries unless regexp.is_a?(Regexp)
|
65
|
+
|
66
|
+
parts = Utils.parse_regexp(regexp)
|
67
|
+
parts.each_with_index do |part, index|
|
68
|
+
if part.is_a?(Regin::Group)
|
69
|
+
if index > 0
|
70
|
+
previous = parts[index-1]
|
71
|
+
if previous.is_a?(Regin::Character) && previous.literal?
|
72
|
+
boundaries << previous.to_s
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if inside = part.expression[0]
|
77
|
+
if inside.is_a?(Regin::Character) && inside.literal?
|
78
|
+
boundaries << inside.to_s
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if index < parts.length
|
83
|
+
following = parts[index+1]
|
84
|
+
if following.is_a?(Regin::Character) && following.literal?
|
85
|
+
boundaries << following.to_s
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
boundaries
|
92
|
+
end
|
93
|
+
|
94
|
+
def generate_split_keys(regexp, separators) #:nodoc:
|
95
|
+
segments = []
|
96
|
+
buf = nil
|
97
|
+
parts = Utils.parse_regexp(regexp)
|
98
|
+
parts.each_with_index do |part, index|
|
99
|
+
case part
|
100
|
+
when Regin::Anchor
|
101
|
+
if part.value == '$' || part.value == '\Z'
|
102
|
+
segments << join_buffer(buf, regexp) if buf
|
103
|
+
segments << NULL
|
104
|
+
buf = nil
|
105
|
+
break
|
106
|
+
end
|
107
|
+
when Regin::CharacterClass
|
108
|
+
break if separators.any? { |s| part.include?(s) }
|
109
|
+
buf = nil
|
110
|
+
segments << part.to_regexp(true)
|
111
|
+
when Regin::Character
|
112
|
+
if separators.any? { |s| part.include?(s) }
|
113
|
+
segments << join_buffer(buf, regexp) if buf
|
114
|
+
peek = parts[index+1]
|
115
|
+
if peek.is_a?(Regin::Character) && separators.include?(peek.value)
|
116
|
+
segments << ''
|
117
|
+
end
|
118
|
+
buf = nil
|
119
|
+
else
|
120
|
+
buf ||= Regin::Expression.new([])
|
121
|
+
buf += [part]
|
122
|
+
end
|
123
|
+
when Regin::Group
|
124
|
+
if part.quantifier == '?'
|
125
|
+
value = part.expression.first
|
126
|
+
if separators.any? { |s| value.include?(s) }
|
127
|
+
segments << join_buffer(buf, regexp) if buf
|
128
|
+
buf = nil
|
129
|
+
end
|
130
|
+
break
|
131
|
+
elsif part.quantifier == nil
|
132
|
+
break if separators.any? { |s| part.include?(s) }
|
133
|
+
buf = nil
|
134
|
+
segments << part.to_regexp(true)
|
135
|
+
else
|
136
|
+
break
|
137
|
+
end
|
138
|
+
else
|
139
|
+
break
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
while segments.length > 0 && (segments.last.nil? || segments.last == '')
|
144
|
+
segments.pop
|
145
|
+
end
|
146
|
+
|
147
|
+
segments
|
148
|
+
end
|
149
|
+
|
150
|
+
def join_buffer(parts, regexp)
|
151
|
+
if parts.literal?
|
152
|
+
parts.to_s
|
153
|
+
else
|
154
|
+
parts.to_regexp(true)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module Rack::Mount
|
2
|
+
module CodeGeneration #:nodoc:
|
3
|
+
def _expired_recognize(env) #:nodoc:
|
4
|
+
raise 'route set not finalized'
|
5
|
+
end
|
6
|
+
|
7
|
+
def rehash
|
8
|
+
super
|
9
|
+
optimize_recognize!
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def expire!
|
14
|
+
if @optimized_recognize_defined
|
15
|
+
remove_metaclass_method :recognize
|
16
|
+
|
17
|
+
class << self
|
18
|
+
alias_method :recognize, :_expired_recognize
|
19
|
+
end
|
20
|
+
|
21
|
+
@optimized_recognize_defined = false
|
22
|
+
end
|
23
|
+
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def optimize_container_iterator(container)
|
28
|
+
Utils.debug "optimizing container - size #{container.size}"
|
29
|
+
|
30
|
+
body = []
|
31
|
+
|
32
|
+
container.each_with_index { |route, i|
|
33
|
+
body << "route = self[#{i}]"
|
34
|
+
body << 'matches = {}'
|
35
|
+
body << 'params = route.defaults.dup'
|
36
|
+
|
37
|
+
conditions = []
|
38
|
+
route.conditions.each do |method, condition|
|
39
|
+
b = []
|
40
|
+
if condition.is_a?(Regexp)
|
41
|
+
b << "if m = obj.#{method}.match(#{condition.inspect})"
|
42
|
+
b << "matches[:#{method}] = m"
|
43
|
+
if (named_captures = route.named_captures[method]) && named_captures.any?
|
44
|
+
b << 'captures = m.captures'
|
45
|
+
b << 'p = nil'
|
46
|
+
b << named_captures.map { |k, j| "params[#{k.inspect}] = p if p = captures[#{j}]" }.join('; ')
|
47
|
+
end
|
48
|
+
else
|
49
|
+
b << "if m = obj.#{method} == route.conditions[:#{method}]"
|
50
|
+
end
|
51
|
+
b << 'true'
|
52
|
+
b << 'end'
|
53
|
+
conditions << "(#{b.join('; ')})"
|
54
|
+
end
|
55
|
+
|
56
|
+
body << <<-RUBY
|
57
|
+
if #{conditions.join(' && ')}
|
58
|
+
yield route, matches, params
|
59
|
+
end
|
60
|
+
RUBY
|
61
|
+
}
|
62
|
+
|
63
|
+
container.instance_eval(<<-RUBY, __FILE__, __LINE__)
|
64
|
+
def optimized_each(obj)
|
65
|
+
#{body.join("\n")}
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
RUBY
|
69
|
+
end
|
70
|
+
|
71
|
+
def optimize_recognize!
|
72
|
+
Utils.debug "optimizing recognize"
|
73
|
+
|
74
|
+
keys = @recognition_keys.map { |key|
|
75
|
+
if key.respond_to?(:call_source)
|
76
|
+
key.call_source(:cache, :obj)
|
77
|
+
else
|
78
|
+
"obj.#{key}"
|
79
|
+
end
|
80
|
+
}.join(', ')
|
81
|
+
|
82
|
+
@optimized_recognize_defined = true
|
83
|
+
|
84
|
+
remove_metaclass_method :recognize
|
85
|
+
|
86
|
+
instance_eval(<<-RUBY, __FILE__, __LINE__)
|
87
|
+
def recognize(obj)
|
88
|
+
cache = {}
|
89
|
+
container = @recognition_graph[#{keys}]
|
90
|
+
optimize_container_iterator(container) unless container.respond_to?(:optimized_each)
|
91
|
+
|
92
|
+
if block_given?
|
93
|
+
container.optimized_each(obj) do |route, matches, params|
|
94
|
+
yield route, matches, params
|
95
|
+
end
|
96
|
+
else
|
97
|
+
container.optimized_each(obj) do |route, matches, params|
|
98
|
+
return route, matches, params
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
RUBY
|
105
|
+
end
|
106
|
+
|
107
|
+
# method_defined? can't distinguish between instance
|
108
|
+
# and meta methods. So we have to rescue if the method
|
109
|
+
# has not been defined in the metaclass yet.
|
110
|
+
def remove_metaclass_method(symbol)
|
111
|
+
metaclass = class << self; self; end
|
112
|
+
Utils.silence_debug { metaclass.send(:remove_method, symbol) }
|
113
|
+
rescue NameError => e
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|