ruby-nuggets 0.9.1 → 0.9.2

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,165 @@
1
+ # encoding: utf-8
2
+
3
+ #--
4
+ ###############################################################################
5
+ # #
6
+ # A component of ruby-nuggets, some extensions to the Ruby programming #
7
+ # language. #
8
+ # #
9
+ # Copyright (C) 2007-2011 Jens Wille #
10
+ # #
11
+ # Authors: #
12
+ # Jens Wille <jens.wille@gmail.com> #
13
+ # #
14
+ # ruby-nuggets is free software; you can redistribute it and/or modify it #
15
+ # under the terms of the GNU Affero General Public License as published by #
16
+ # the Free Software Foundation; either version 3 of the License, or (at your #
17
+ # option) any later version. #
18
+ # #
19
+ # ruby-nuggets is distributed in the hope that it will be useful, but WITHOUT #
20
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
21
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License #
22
+ # for more details. #
23
+ # #
24
+ # You should have received a copy of the GNU Affero General Public License #
25
+ # along with ruby-nuggets. If not, see <http://www.gnu.org/licenses/>. #
26
+ # #
27
+ ###############################################################################
28
+ #++
29
+
30
+ module Nuggets
31
+ module I18n
32
+
33
+ DIACRITICS = {
34
+ 'À' => 'A', # LATIN CAPITAL LETTER A WITH GRAVE
35
+ 'Á' => 'A', # LATIN CAPITAL LETTER A WITH ACUTE
36
+ 'Â' => 'A', # LATIN CAPITAL LETTER A WITH CIRCUMFLEX
37
+ 'Ã' => 'A', # LATIN CAPITAL LETTER A WITH TILDE
38
+ 'Ä' => 'AE', # LATIN CAPITAL LETTER A WITH DIAERESIS
39
+ 'Å' => 'A', # LATIN CAPITAL LETTER A WITH RING ABOVE
40
+ 'Æ' => 'AE', # LATIN CAPITAL LETTER AE
41
+ 'Ç' => 'C', # LATIN CAPITAL LETTER C WITH CEDILLA
42
+ 'È' => 'E', # LATIN CAPITAL LETTER E WITH GRAVE
43
+ 'É' => 'E', # LATIN CAPITAL LETTER E WITH ACUTE
44
+ 'Ê' => 'E', # LATIN CAPITAL LETTER E WITH CIRCUMFLEX
45
+ 'Ë' => 'E', # LATIN CAPITAL LETTER E WITH DIAERESIS
46
+ 'Ì' => 'I', # LATIN CAPITAL LETTER I WITH GRAVE
47
+ 'Í' => 'I', # LATIN CAPITAL LETTER I WITH ACUTE
48
+ 'Î' => 'I', # LATIN CAPITAL LETTER I WITH CIRCUMFLEX
49
+ 'Ï' => 'I', # LATIN CAPITAL LETTER I WITH DIAERESIS
50
+ 'Ð' => 'DH', # LATIN CAPITAL LETTER ETH
51
+ 'Ñ' => 'N', # LATIN CAPITAL LETTER N WITH TILDE
52
+ 'Ò' => 'O', # LATIN CAPITAL LETTER O WITH GRAVE
53
+ 'Ó' => 'O', # LATIN CAPITAL LETTER O WITH ACUTE
54
+ 'Ô' => 'O', # LATIN CAPITAL LETTER O WITH CIRCUMFLEX
55
+ 'Õ' => 'O', # LATIN CAPITAL LETTER O WITH TILDE
56
+ 'Ö' => 'OE', # LATIN CAPITAL LETTER O WITH DIAERESIS
57
+ 'Ø' => 'O', # LATIN CAPITAL LETTER O WITH STROKE
58
+ 'Ù' => 'U', # LATIN CAPITAL LETTER U WITH GRAVE
59
+ 'Ú' => 'U', # LATIN CAPITAL LETTER U WITH ACUTE
60
+ 'Û' => 'U', # LATIN CAPITAL LETTER U WITH CIRCUMFLEX
61
+ 'Ü' => 'UE', # LATIN CAPITAL LETTER U WITH DIAERESIS
62
+ 'Ý' => 'Y', # LATIN CAPITAL LETTER Y WITH ACUTE
63
+ 'Þ' => 'TH', # LATIN CAPITAL LETTER THORN
64
+ 'ß' => 'ss', # LATIN SMALL LETTER SHARP S
65
+ 'à' => 'a', # LATIN SMALL LETTER A WITH GRAVE
66
+ 'á' => 'a', # LATIN SMALL LETTER A WITH ACUTE
67
+ 'â' => 'a', # LATIN SMALL LETTER A WITH CIRCUMFLEX
68
+ 'ã' => 'a', # LATIN SMALL LETTER A WITH TILDE
69
+ 'ä' => 'ae', # LATIN SMALL LETTER A WITH DIAERESIS
70
+ 'å' => 'a', # LATIN SMALL LETTER A WITH RING ABOVE
71
+ 'æ' => 'ae', # LATIN SMALL LETTER AE
72
+ 'ç' => 'c', # LATIN SMALL LETTER C WITH CEDILLA
73
+ 'è' => 'e', # LATIN SMALL LETTER E WITH GRAVE
74
+ 'é' => 'e', # LATIN SMALL LETTER E WITH ACUTE
75
+ 'ê' => 'e', # LATIN SMALL LETTER E WITH CIRCUMFLEX
76
+ 'ë' => 'e', # LATIN SMALL LETTER E WITH DIAERESIS
77
+ 'ì' => 'i', # LATIN SMALL LETTER I WITH GRAVE
78
+ 'í' => 'i', # LATIN SMALL LETTER I WITH ACUTE
79
+ 'î' => 'i', # LATIN SMALL LETTER I WITH CIRCUMFLEX
80
+ 'ï' => 'i', # LATIN SMALL LETTER I WITH DIAERESIS
81
+ 'ð' => 'dh', # LATIN SMALL LETTER ETH
82
+ 'ñ' => 'n', # LATIN SMALL LETTER N WITH TILDE
83
+ 'ò' => 'o', # LATIN SMALL LETTER O WITH GRAVE
84
+ 'ó' => 'o', # LATIN SMALL LETTER O WITH ACUTE
85
+ 'ô' => 'o', # LATIN SMALL LETTER O WITH CIRCUMFLEX
86
+ 'õ' => 'o', # LATIN SMALL LETTER O WITH TILDE
87
+ 'ö' => 'oe', # LATIN SMALL LETTER O WITH DIAERESIS
88
+ 'ø' => 'o', # LATIN SMALL LETTER O WITH STROKE
89
+ 'ù' => 'u', # LATIN SMALL LETTER U WITH GRAVE
90
+ 'ú' => 'u', # LATIN SMALL LETTER U WITH ACUTE
91
+ 'û' => 'u', # LATIN SMALL LETTER U WITH CIRCUMFLEX
92
+ 'ü' => 'ue', # LATIN SMALL LETTER U WITH DIAERESIS
93
+ 'ý' => 'y', # LATIN SMALL LETTER Y WITH ACUTE
94
+ 'þ' => 'th', # LATIN SMALL LETTER THORN
95
+ 'ÿ' => 'y' # LATIN SMALL LETTER Y WITH DIAERESIS
96
+ }
97
+
98
+ def self.args_for_map_diacritics
99
+ @args_for_map_diacritics ||= begin
100
+ map = ::Hash.new { |h, k| h[k] = [] }
101
+
102
+ DIACRITICS.each { |a| a.each { |i| map[i].concat(a) } }
103
+ map.each { |k, v| v.uniq!; map[k] = "(#{::Regexp.union(*v).source})" }
104
+
105
+ [::Regexp.union(*map.keys.sort_by { |k| -k.length }), map.method(:[])]
106
+ end
107
+ end
108
+
109
+ end
110
+ end
111
+
112
+ class String
113
+
114
+ # call-seq:
115
+ # str.replace_diacritics => new_str
116
+ #
117
+ # Substitutes any diacritics in _str_ with their replacements as per
118
+ # Nuggets::I18n::DIACRITICS.
119
+ def replace_diacritics
120
+ (_dup = dup).replace_diacritics! || _dup
121
+ end
122
+
123
+ # call-seq:
124
+ # str.replace_diacritics! => str or +nil+
125
+ #
126
+ # Destructive version of #replace_diacritics.
127
+ def replace_diacritics!
128
+ diacritics = ::Nuggets::I18n::DIACRITICS
129
+
130
+ gsub!(/#{::Regexp.union(*diacritics.keys)}/) { |m|
131
+ s = diacritics[m]
132
+
133
+ # Try to adjust case:
134
+ # 'Äh' => 'AEh' => 'Aeh'
135
+ #
136
+ # But:
137
+ # 'SÖS' => 'SOES' (not 'SOeS'!)
138
+ if s.length > 1
139
+ t = $'[0..0]
140
+ s[1..-1] = s[1..-1].downcase if t == t.downcase
141
+ end
142
+
143
+ s
144
+ }
145
+ end
146
+
147
+ def map_diacritics
148
+ (_dup = dup).map_diacritics! || _dup
149
+ end
150
+
151
+ def map_diacritics!
152
+ re, block = ::Nuggets::I18n.args_for_map_diacritics
153
+ gsub!(re, &block)
154
+ end
155
+
156
+ end
157
+
158
+ if $0 == __FILE__
159
+ s = 'Äh, Rüby iß sö cüül, nö? SÖS!'
160
+ p s
161
+ p s.replace_diacritics
162
+
163
+ s.replace_diacritics!
164
+ p s
165
+ end
@@ -0,0 +1,45 @@
1
+ #--
2
+ ###############################################################################
3
+ # #
4
+ # A component of ruby-nuggets, some extensions to the Ruby programming #
5
+ # language. #
6
+ # #
7
+ # Copyright (C) 2007-2012 Jens Wille #
8
+ # #
9
+ # Authors: #
10
+ # Jens Wille <jens.wille@gmail.com> #
11
+ # #
12
+ # ruby-nuggets is free software; you can redistribute it and/or modify it #
13
+ # under the terms of the GNU Affero General Public License as published by #
14
+ # the Free Software Foundation; either version 3 of the License, or (at your #
15
+ # option) any later version. #
16
+ # #
17
+ # ruby-nuggets is distributed in the hope that it will be useful, but WITHOUT #
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License #
20
+ # for more details. #
21
+ # #
22
+ # You should have received a copy of the GNU Affero General Public License #
23
+ # along with ruby-nuggets. If not, see <http://www.gnu.org/licenses/>. #
24
+ # #
25
+ ###############################################################################
26
+ #++
27
+
28
+ module Nuggets
29
+
30
+ module LazyAttr
31
+
32
+ private
33
+
34
+ def lazy_attr(attr, freeze = true)
35
+ class << self; self; end.class_eval { attr_reader attr }
36
+
37
+ value = instance_variable_get(name = "@#{attr}") ||
38
+ instance_variable_set(name, yield)
39
+
40
+ freeze ? value.freeze : value
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,102 @@
1
+ #--
2
+ ###############################################################################
3
+ # #
4
+ # A component of ruby-nuggets, some extensions to the Ruby programming #
5
+ # language. #
6
+ # #
7
+ # Copyright (C) 2007-2012 Jens Wille #
8
+ # #
9
+ # Authors: #
10
+ # Jens Wille <jens.wille@gmail.com> #
11
+ # #
12
+ # ruby-nuggets is free software; you can redistribute it and/or modify it #
13
+ # under the terms of the GNU Affero General Public License as published by #
14
+ # the Free Software Foundation; either version 3 of the License, or (at your #
15
+ # option) any later version. #
16
+ # #
17
+ # ruby-nuggets is distributed in the hope that it will be useful, but WITHOUT #
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License #
20
+ # for more details. #
21
+ # #
22
+ # You should have received a copy of the GNU Affero General Public License #
23
+ # along with ruby-nuggets. If not, see <http://www.gnu.org/licenses/>. #
24
+ # #
25
+ ###############################################################################
26
+ #++
27
+
28
+ require 'nuggets/log_parser'
29
+
30
+ module Nuggets
31
+ module LogParser
32
+ module Apache
33
+
34
+ extend self
35
+
36
+ DEFAULT_RE = %r{(.*?)}
37
+
38
+ DIRECTIVES = {
39
+ 'h' => [:ip, %r{(\d+(?:\.\d+){3}|[\w.-]+)}],
40
+ 'l' => [:auth, DEFAULT_RE],
41
+ 'u' => [:username, DEFAULT_RE],
42
+ 't' => [:datetime, %r{\[(.*?)\]}],
43
+ 'r' => [:request, DEFAULT_RE],
44
+ 'R' => [:request, %r{(.*?)(?:"|\z)}],
45
+ 's' => [:status, %r{(\d+)}],
46
+ 'b' => [:bytecount, %r{(-|\d+)}],
47
+ 'v' => [:domain, DEFAULT_RE],
48
+ 'i' => [nil, DEFAULT_RE],
49
+ }
50
+
51
+ DIRECTIVES_RE = %r{%.*?(?:\{(.*?)\})?([#{DIRECTIVES.keys.join}])([\s\\"]*)}
52
+
53
+ ORDER = []
54
+
55
+ class << self
56
+
57
+ def register(name, format)
58
+ base = const_set(name, Module.new)
59
+
60
+ re, items = parse_format(format)
61
+ base.const_set(:RE, re)
62
+ base.const_set(:ITEMS, items)
63
+
64
+ ORDER << base
65
+ LogParser.register(base, self)
66
+ end
67
+
68
+ def parse_format(format)
69
+ re, items = '\A', []
70
+
71
+ format.scan(DIRECTIVES_RE) { |h, c, e|
72
+ i, r = DIRECTIVES[c]
73
+ re << r.source << e.gsub(/\s/, '\\s')
74
+ items << i ||= h.downcase.tr('-', '_').to_sym
75
+ }
76
+
77
+ [Regexp.new(re), items]
78
+ end
79
+
80
+ def detect_type(line)
81
+ ORDER.find { |type| line =~ type::RE }
82
+ end
83
+
84
+ end
85
+
86
+ [ [:Combined, '%h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i"'],
87
+ [:Common, '%h %l %u %t "%r" %>s %b'],
88
+ [:Minimal, '%h %l %u %t "%R']
89
+ ].each { |name, format| register(name, format) }
90
+
91
+ def parse_line(line, entry = {})
92
+ if md = self::RE.match(line)
93
+ self::ITEMS.each_with_index { |k, i| entry[k] = md[i + 1] }
94
+ yield if block_given?
95
+ end
96
+
97
+ entry
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,220 @@
1
+ #--
2
+ ###############################################################################
3
+ # #
4
+ # A component of ruby-nuggets, some extensions to the Ruby programming #
5
+ # language. #
6
+ # #
7
+ # Copyright (C) 2007-2012 Jens Wille #
8
+ # #
9
+ # Authors: #
10
+ # Jens Wille <jens.wille@gmail.com> #
11
+ # #
12
+ # ruby-nuggets is free software; you can redistribute it and/or modify it #
13
+ # under the terms of the GNU Affero General Public License as published by #
14
+ # the Free Software Foundation; either version 3 of the License, or (at your #
15
+ # option) any later version. #
16
+ # #
17
+ # ruby-nuggets is distributed in the hope that it will be useful, but WITHOUT #
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License #
20
+ # for more details. #
21
+ # #
22
+ # You should have received a copy of the GNU Affero General Public License #
23
+ # along with ruby-nuggets. If not, see <http://www.gnu.org/licenses/>. #
24
+ # #
25
+ ###############################################################################
26
+ #++
27
+
28
+ require 'nuggets/log_parser'
29
+
30
+ module Rails
31
+ module LogParser
32
+ module Rails
33
+
34
+ LogParser.register(self)
35
+
36
+ # Log line prefix
37
+ PREFIX_RE = %r{
38
+ \A
39
+ (?:
40
+ \[
41
+ (\d+) # pid
42
+ :
43
+ (.*?) # host
44
+ \]
45
+ \s
46
+ )?
47
+ \s*
48
+ }x
49
+
50
+ # Log entry separator
51
+ SEPARATOR_RE = %r{
52
+ \s+\|\s+
53
+ }x
54
+
55
+ # Log line patterns
56
+ ITEMS = [
57
+ [:processing, {
58
+ :re => %r{
59
+ #{PREFIX_RE} # pid, host
60
+ Processing\s+
61
+ (\w+) # controller
62
+ \#
63
+ (\w+) # action
64
+ \s+
65
+ \(
66
+ for\s+
67
+ (.*?) # ip
68
+ \s+at\s+
69
+ (.*?) # datetime
70
+ \)
71
+ \s+
72
+ \[
73
+ (\w+) # request_method
74
+ \]
75
+ }xo,
76
+ :keys => [:controller, :action, :ip, :datetime, :request_method]
77
+ }],
78
+ [:session_id, {
79
+ :re => %r{
80
+ #{PREFIX_RE} # pid, host
81
+ Session\sID:\s+
82
+ (\S+) # sid
83
+ }xo,
84
+ :keys => [:sid]
85
+ }],
86
+ [:parameters, {
87
+ :re => %r{
88
+ #{PREFIX_RE} # pid, host
89
+ Parameters:\s+
90
+ (\{.*\}) # params
91
+ }xo, #}
92
+ :proc => lambda { |entry, md|
93
+ entry[:params_hash] = md[3].hash
94
+ entry[:params] = begin
95
+ eval("$SAFE = 3\n#{md[3].gsub(/#<.*?>/, '%q{\&}')}", nil, __FILE__, __LINE__) # !!!
96
+ rescue SyntaxError, SecurityError
97
+ {}
98
+ end
99
+ }
100
+ }],
101
+ [:client_info, {
102
+ :re => %r{
103
+ #{PREFIX_RE} # pid, host
104
+ Client\sinfo:\s+
105
+ UA\s+=\s+
106
+ (.*?) # user_agent
107
+ #{SEPARATOR_RE}
108
+ LANG\s+=\s+
109
+ (.*) # accept_language
110
+ }xo,
111
+ :keys => [:user_agent, :accept_language]
112
+ }],
113
+ [:referer, {
114
+ :re => %r{
115
+ #{PREFIX_RE} # pid, host
116
+ Referer:\s+
117
+ (.*) # referer
118
+ }xo,
119
+ :keys => [:referer]
120
+ }],
121
+ [:meta, {
122
+ :re => %r{
123
+ #{PREFIX_RE} # pid, host
124
+ Meta:\s+
125
+ User\s+=\s+
126
+ (.*?) # user_id
127
+ #{SEPARATOR_RE}
128
+ Institution\s+=\s+
129
+ (.*) # institution_id
130
+ }xo,
131
+ :keys => [:user_id, :institution_id]
132
+ }],
133
+ [:stats, {
134
+ :re => %r{
135
+ #{PREFIX_RE} # pid, host
136
+ Stats:\s+
137
+ (.*) # flags
138
+ }xo,
139
+ :proc => lambda { |entry, md|
140
+ entry[:flags] = md[3].split(SEPARATOR_RE)
141
+ }
142
+ }],
143
+ [:oauth, {
144
+ :re => %r{
145
+ #{PREFIX_RE} # pid, host
146
+ OAuth:\s+
147
+ Token\s+=\s+
148
+ (.*?)#(.*?) # token_type, token
149
+ #{SEPARATOR_RE}
150
+ User\s+=\s+
151
+ (.*?) # user_id
152
+ #{SEPARATOR_RE}
153
+ Client\s+=\s+
154
+ (.*) # client_id
155
+ }xo,
156
+ :keys => [:token_type, :token, :user_id, :client_id]
157
+ }],
158
+ [:benchmark, {
159
+ :re => %r{
160
+ #{PREFIX_RE} # pid, host
161
+ Completed\sin\s+
162
+ (\S+) # runtime
163
+ .*?
164
+ (?: #- OPTIONAL
165
+ #{SEPARATOR_RE}
166
+ Rendering:\s+
167
+ (\S+) # rendering_runtime
168
+ .*?
169
+ )?
170
+ (?: #- OPTIONAL
171
+ #{SEPARATOR_RE}
172
+ DB:\s+
173
+ (\S+) # db_runtime
174
+ .*?
175
+ )?
176
+ (?: #- OPTIONAL
177
+ #{SEPARATOR_RE}
178
+ Mem:\s+
179
+ \S+
180
+ .*?
181
+ )?
182
+ #{SEPARATOR_RE}
183
+ (.*?) # status
184
+ \s+
185
+ \[
186
+ (.*) # request_uri
187
+ \]
188
+ }xo,
189
+ :keys => [:runtime, :rendering_runtime, :db_runtime, :status, :request_uri]
190
+ }]
191
+ ]
192
+
193
+ ITEMS.each { |_, item|
194
+ item[:proc] ||= lambda { |entry, md|
195
+ item[:keys].each_with_index { |k, i|
196
+ entry[k] = md[i + 3] # 1 = pid, 2 = host
197
+ }
198
+ }
199
+ }
200
+
201
+ def parse_line(line, entry = {})
202
+ ITEMS.each { |key, item|
203
+ if md = item[:re].match(line)
204
+ if key == :processing
205
+ yield if block_given?
206
+ entry[:pid], entry[:host] = md[1], md[2]
207
+ end
208
+
209
+ item[:proc][entry, md]
210
+
211
+ break
212
+ end
213
+ }
214
+
215
+ entry
216
+ end
217
+
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,71 @@
1
+ #--
2
+ ###############################################################################
3
+ # #
4
+ # A component of ruby-nuggets, some extensions to the Ruby programming #
5
+ # language. #
6
+ # #
7
+ # Copyright (C) 2007-2012 Jens Wille #
8
+ # #
9
+ # Authors: #
10
+ # Jens Wille <jens.wille@gmail.com> #
11
+ # #
12
+ # ruby-nuggets is free software; you can redistribute it and/or modify it #
13
+ # under the terms of the GNU Affero General Public License as published by #
14
+ # the Free Software Foundation; either version 3 of the License, or (at your #
15
+ # option) any later version. #
16
+ # #
17
+ # ruby-nuggets is distributed in the hope that it will be useful, but WITHOUT #
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License #
20
+ # for more details. #
21
+ # #
22
+ # You should have received a copy of the GNU Affero General Public License #
23
+ # along with ruby-nuggets. If not, see <http://www.gnu.org/licenses/>. #
24
+ # #
25
+ ###############################################################################
26
+ #++
27
+
28
+ require 'zlib'
29
+
30
+ module Nuggets
31
+ module LogParser
32
+
33
+ extend self
34
+
35
+ GZ_EXT_RE = %r{\.gz\z}
36
+
37
+ def self.register(base, *modules)
38
+ base.send(:include, *modules << self)
39
+ base.extend(base)
40
+ end
41
+
42
+ def parse(input)
43
+ entry = {}
44
+
45
+ input.each { |line| parse_line(line, entry) {
46
+ unless entry.empty?
47
+ yield entry.dup
48
+ entry.clear
49
+ end
50
+ } }
51
+
52
+ yield entry unless entry.empty?
53
+ end
54
+
55
+ def parse_line(line, entry = {})
56
+ # Yield when entry complete. Preferrably return +entry+.
57
+ raise NotImplementedError, 'must be implemented by type'
58
+ end
59
+
60
+ def parse_file(file, &block)
61
+ block ||= (entries = []; lambda { |entry| entries << entry })
62
+
63
+ (file =~ GZ_EXT_RE ? Zlib::GzipReader : File).open(file) { |f|
64
+ block.arity == 1 ? parse(f, &block) : block[f, method(:parse)]
65
+ }
66
+
67
+ entries
68
+ end
69
+
70
+ end
71
+ end