dogapi 1.8.1 → 1.9.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.
- data/.rspec +2 -0
- data/.tailor +106 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +7 -0
- data/README.rdoc +4 -1
- data/Rakefile +31 -2
- data/lib/capistrano/datadog.rb +6 -10
- data/lib/dogapi/common.rb +1 -1
- data/lib/dogapi/event.rb +6 -6
- data/lib/dogapi/facade.rb +34 -45
- data/lib/dogapi/v1/alert.rb +1 -1
- data/lib/dogapi/v1/comment.rb +3 -3
- data/lib/dogapi/v1/dash.rb +1 -2
- data/lib/dogapi/v1/event.rb +1 -1
- data/lib/dogapi/v1/metric.rb +3 -3
- data/lib/dogapi/v1/user.rb +1 -1
- data/lib/dogapi/version.rb +1 -1
- data/spec/alerts_spec.rb +33 -0
- data/spec/common_spec.rb +16 -0
- data/spec/facade_spec.rb +122 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/support/cassettes/Alerts/create/returns_HTTP_code_201.yml +90 -0
- data/spec/support/cassettes/Alerts/create/returns_a_valid_event_ID.yml +90 -0
- data/spec/support/cassettes/Alerts/create/returns_the_same_query_as_sent.yml +90 -0
- data/spec/support/cassettes/Facade/Client/emit_point_can_pass_nil_host.yml +32 -0
- data/spec/support/cassettes/Facade/Client/emit_point_passes_data.yml +32 -0
- data/spec/support/cassettes/Facade/Client/emit_point_uses_localhost_default.yml +32 -0
- data/spec/support/cassettes/Facade/Client/emits_point_with_localhost.yml +32 -0
- data/spec/support/cassettes/Facade/Events/emits_aggregate_events.yml +131 -0
- data/spec/support/cassettes/Facade/Events/emits_events_and_retrieves_them.yml +67 -0
- data/spec/support/cassettes/Facade/Events/emits_events_with_specified_priority.yml +67 -0
- data/spec/support/cassettes/Facade/Tags/adds_updates_and_detaches_tags.yml +352 -0
- data/tests/test_alerts.rb +3 -3
- data/tests/test_client.rb +0 -114
- data/tests/test_dashes.rb +3 -2
- metadata +38 -7
data/.rspec
ADDED
data/.tailor
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
#------------------------------------------------------------------------------
|
2
|
+
# Horizontal Whitespace
|
3
|
+
#------------------------------------------------------------------------------
|
4
|
+
# allow_hard_tabs True to let hard tabs be considered a single space.
|
5
|
+
# Default: false
|
6
|
+
#
|
7
|
+
# allow_trailing_line_spaces
|
8
|
+
# True to skip detecting extra spaces at the ends of
|
9
|
+
# lines.
|
10
|
+
# Default: false
|
11
|
+
#
|
12
|
+
# indentation_spaces The number of spaces to consider a proper indent.
|
13
|
+
# Default: 2
|
14
|
+
#
|
15
|
+
# max_line_length The maximum number of characters in a line before
|
16
|
+
# tailor complains.
|
17
|
+
# Default: 80
|
18
|
+
# spaces_after_comma Number of spaces to expect after a comma.
|
19
|
+
# Default: 1
|
20
|
+
#
|
21
|
+
# spaces_before_comma Number of spaces to expect before a comma.
|
22
|
+
# Default: 0
|
23
|
+
#
|
24
|
+
# spaces_after_lbrace The number of spaces to expect after an lbrace ('{').
|
25
|
+
# Default: 1
|
26
|
+
#
|
27
|
+
# spaces_before_lbrace The number of spaces to expect before an lbrace ('{').
|
28
|
+
# Default: 1
|
29
|
+
#
|
30
|
+
# spaces_before_rbrace The number of spaces to expect before an rbrace ('}').
|
31
|
+
# Default: 1
|
32
|
+
#
|
33
|
+
# spaces_in_empty_braces The number of spaces to expect between braces when
|
34
|
+
# there's nothing in the braces (i.e. {}).
|
35
|
+
# Default: 0
|
36
|
+
#
|
37
|
+
# spaces_after_lbracket The number of spaces to expect after an
|
38
|
+
# lbracket ('[').
|
39
|
+
# Default: 0
|
40
|
+
#
|
41
|
+
# spaces_before_rbracket The number of spaces to expect before an
|
42
|
+
# rbracket (']').
|
43
|
+
# Default: 0
|
44
|
+
#
|
45
|
+
# spaces_after_lparen The number of spaces to expect after an
|
46
|
+
# lparen ('(').
|
47
|
+
# Default: 0
|
48
|
+
#
|
49
|
+
# spaces_before_rparen The number of spaces to expect before an
|
50
|
+
# rbracket (')').
|
51
|
+
# Default: 0
|
52
|
+
#
|
53
|
+
#------------------------------------------------------------------------------
|
54
|
+
# Naming
|
55
|
+
#------------------------------------------------------------------------------
|
56
|
+
# allow_camel_case_methods
|
57
|
+
# Setting to true skips detection of camel-case method
|
58
|
+
# names (i.e. def myMethod).
|
59
|
+
# Default: false
|
60
|
+
#
|
61
|
+
# allow_screaming_snake_case_classes
|
62
|
+
# Setting to true skips detection of screaming
|
63
|
+
# snake-case class names (i.e. My_Class).
|
64
|
+
# Default: false
|
65
|
+
#
|
66
|
+
#------------------------------------------------------------------------------
|
67
|
+
# Vertical Whitespace
|
68
|
+
#------------------------------------------------------------------------------
|
69
|
+
# max_code_lines_in_class The number of lines of code in a class to allow before
|
70
|
+
# tailor will warn you.
|
71
|
+
# Default: 300
|
72
|
+
#
|
73
|
+
# max_code_lines_in_method
|
74
|
+
# The number of lines of code in a method to allow
|
75
|
+
# before tailor will warn you.
|
76
|
+
# Default: 30
|
77
|
+
#
|
78
|
+
# trailing_newlines The number of newlines that should be at the end of
|
79
|
+
# the file.
|
80
|
+
# Default: 1
|
81
|
+
#
|
82
|
+
Tailor.config do |config|
|
83
|
+
config.formatters "text"
|
84
|
+
config.file_set 'lib/**/*.rb' do |style|
|
85
|
+
style.allow_camel_case_methods false, level: :error
|
86
|
+
style.allow_hard_tabs false, level: :error
|
87
|
+
style.allow_screaming_snake_case_classes false, level: :error
|
88
|
+
style.allow_trailing_line_spaces false, level: :error
|
89
|
+
style.allow_invalid_ruby false, level: :warn
|
90
|
+
style.indentation_spaces 2, level: :error
|
91
|
+
style.max_code_lines_in_class 300, level: :error
|
92
|
+
style.max_code_lines_in_method 33, level: :error
|
93
|
+
style.max_line_length 160, level: :warn
|
94
|
+
style.spaces_after_comma 1, level: :error
|
95
|
+
style.spaces_after_lbrace 1, level: :error
|
96
|
+
style.spaces_after_lbracket 0, level: :error
|
97
|
+
style.spaces_after_lparen 0, level: :error
|
98
|
+
style.spaces_before_comma 0, level: :error
|
99
|
+
style.spaces_before_lbrace 1, level: :error
|
100
|
+
style.spaces_before_rbrace 1, level: :error
|
101
|
+
style.spaces_before_rbracket 0, level: :error
|
102
|
+
style.spaces_before_rparen 0, level: :error
|
103
|
+
style.spaces_in_empty_braces 0, level: :error
|
104
|
+
style.trailing_newlines 1, level: :error
|
105
|
+
end
|
106
|
+
end
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
= Ruby
|
1
|
+
= Ruby Client for Datadog API
|
2
|
+
|
3
|
+
{<img src="https://badge.fury.io/rb/dogapi.png" alt="Gem Version" />}[http://badge.fury.io/rb/dogapi]
|
4
|
+
|
2
5
|
{<img src="https://travis-ci.org/DataDog/dogapi-rb.png?branch=master" alt="Build Status" />}[https://travis-ci.org/DataDog/dogapi-rb]
|
3
6
|
|
4
7
|
The Ruby client is a library suitable for inclusion in existing Ruby projects or for development of standalone scripts. It provides an abstraction on top of Datadog's raw HTTP interface for reporting events and metrics.
|
data/Rakefile
CHANGED
@@ -1,12 +1,36 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
2
|
require 'rake/testtask'
|
3
3
|
require 'rdoc/task'
|
4
|
+
require 'rspec/core/rake_task'
|
4
5
|
|
5
6
|
# Assign some test keys if they aren't already set.
|
6
7
|
ENV["DATADOG_API_KEY"] ||= '9775a026f1ca7d1c6c5af9d94d9595a4'
|
7
8
|
ENV["DATADOG_APP_KEY"] ||= '87ce4a24b5553d2e482ea8a8500e71b8ad4554ff'
|
8
9
|
|
9
|
-
|
10
|
+
default_tests = [:spec, :test]
|
11
|
+
|
12
|
+
case RbConfig::CONFIG['ruby_version']
|
13
|
+
when '1.8'
|
14
|
+
# do nothing
|
15
|
+
else
|
16
|
+
# Since Tailor uses methods that don't exist in Ruby 1.8.7
|
17
|
+
require 'tailor/rake_task'
|
18
|
+
default_tests.unshift(:tailor)
|
19
|
+
|
20
|
+
Tailor::RakeTask.new do |task|
|
21
|
+
task.file_set 'lib/**/*.rb', :code do |style|
|
22
|
+
style.max_line_length 160, :level => :warn
|
23
|
+
style.max_code_lines_in_method 40, :level => :warn
|
24
|
+
end
|
25
|
+
task.file_set 'spec/**/*.rb', :tests do |style|
|
26
|
+
style.max_line_length 160, :level => :warn
|
27
|
+
style.max_code_lines_in_method 40, :level => :warn
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
task :default => default_tests
|
10
34
|
|
11
35
|
Rake::TestTask.new(:test) do |test|
|
12
36
|
test.libs.push 'lib'
|
@@ -23,4 +47,9 @@ RDoc::Task.new do |rd|
|
|
23
47
|
rd.title = 'DogAPI -- DataDog Client'
|
24
48
|
end
|
25
49
|
|
26
|
-
|
50
|
+
RSpec::Core::RakeTask.new(:spec)
|
51
|
+
|
52
|
+
desc "Find notes in code"
|
53
|
+
task :notes do
|
54
|
+
puts `grep --exclude=Rakefile -r 'OPTIMIZE:\\|FIXME:\\|TODO:' .`
|
55
|
+
end
|
data/lib/capistrano/datadog.rb
CHANGED
@@ -13,16 +13,16 @@ module Capistrano
|
|
13
13
|
class Configuration
|
14
14
|
module Execution
|
15
15
|
# Attempts to locate the task at the given fully-qualified path, and
|
16
|
-
# execute it. If no such task exists, a Capistrano::NoSuchTaskError
|
16
|
+
# execute it. If no such task exists, a Capistrano::NoSuchTaskError
|
17
17
|
# will be raised.
|
18
|
-
# Also, capture the time the task took to execute, and the logs it
|
18
|
+
# Also, capture the time the task took to execute, and the logs it
|
19
19
|
# outputted for submission to Datadog
|
20
|
-
def find_and_execute_task(path, hooks={})
|
20
|
+
def find_and_execute_task(path, hooks = {})
|
21
21
|
task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist"
|
22
22
|
result = nil
|
23
23
|
reporter = Capistrano::Datadog.reporter
|
24
24
|
timing = Benchmark.measure(task.fully_qualified_name) do
|
25
|
-
# Set the current task so that the logger knows which task to
|
25
|
+
# Set the current task so that the logger knows which task to
|
26
26
|
# associate the logs with
|
27
27
|
reporter.current_task = task.fully_qualified_name
|
28
28
|
trigger(hooks[:before], task) if hooks[:before]
|
@@ -75,7 +75,7 @@ module Capistrano
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def record_log(message)
|
78
|
-
if not @logging_output[@current_task]
|
78
|
+
if not @logging_output[@current_task]
|
79
79
|
@logging_output[@current_task] = []
|
80
80
|
end
|
81
81
|
@logging_output[@current_task] << message
|
@@ -128,7 +128,7 @@ module Capistrano
|
|
128
128
|
namespace :datadog do
|
129
129
|
desc "Submit the tasks that have run to Datadog as events"
|
130
130
|
task :submit do |ns|
|
131
|
-
begin
|
131
|
+
begin
|
132
132
|
api_key = variables[:datadog_api_key]
|
133
133
|
if api_key
|
134
134
|
dog = Dogapi::Client.new(api_key)
|
@@ -147,8 +147,4 @@ module Capistrano
|
|
147
147
|
end
|
148
148
|
end
|
149
149
|
|
150
|
-
|
151
150
|
end
|
152
|
-
|
153
|
-
|
154
|
-
|
data/lib/dogapi/common.rb
CHANGED
@@ -116,7 +116,7 @@ module Dogapi
|
|
116
116
|
resp = nil
|
117
117
|
connect do |conn|
|
118
118
|
if params and params.size > 0
|
119
|
-
qs_params = params.map { |k,v| k.to_s + "=" + v.to_s }
|
119
|
+
qs_params = params.map { |k, v| k.to_s + "=" + v.to_s }
|
120
120
|
qs = "?" + qs_params.join("&")
|
121
121
|
url = url + qs
|
122
122
|
end
|
data/lib/dogapi/event.rb
CHANGED
@@ -26,7 +26,7 @@ module Dogapi
|
|
26
26
|
# :event_type => String
|
27
27
|
# :source_type_name => String
|
28
28
|
# :aggregation_key => String
|
29
|
-
def initialize(msg_text, options={})
|
29
|
+
def initialize(msg_text, options = {})
|
30
30
|
defaults = {
|
31
31
|
:date_happened => Time.now.to_i,
|
32
32
|
:msg_title => '',
|
@@ -48,13 +48,13 @@ module Dogapi
|
|
48
48
|
@event_type = options[:event_type]
|
49
49
|
@source_type_name = options[:source_type_name]
|
50
50
|
end
|
51
|
-
|
52
|
-
# Copy and pasted from the internets
|
51
|
+
|
52
|
+
# Copy and pasted from the internets
|
53
53
|
# http://stackoverflow.com/a/5031637/25276
|
54
54
|
def to_hash
|
55
|
-
|
56
|
-
|
57
|
-
|
55
|
+
hash = {}
|
56
|
+
instance_variables.each { |var| hash[var.to_s[1..-1].to_sym] = instance_variable_get(var) }
|
57
|
+
hash
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
data/lib/dogapi/facade.rb
CHANGED
@@ -23,7 +23,8 @@ module Dogapi
|
|
23
23
|
|
24
24
|
@datadog_host = Dogapi.find_datadog_host()
|
25
25
|
|
26
|
-
@host = host
|
26
|
+
@host = host ||= Dogapi.find_localhost()
|
27
|
+
|
27
28
|
@device = device
|
28
29
|
|
29
30
|
@metric_svc = Dogapi::V1::MetricService.new(@api_key, @application_key, silent)
|
@@ -53,13 +54,15 @@ module Dogapi
|
|
53
54
|
#
|
54
55
|
# options[:type] = "counter" to specify a counter metric
|
55
56
|
# options[:tags] = ["tag1", "tag2"] to tag the point
|
56
|
-
def emit_point(metric, value, options={})
|
57
|
-
defaults = {:timestamp => Time.now
|
57
|
+
def emit_point(metric, value, options = {})
|
58
|
+
defaults = { :timestamp => Time.now }
|
58
59
|
options = defaults.merge(options)
|
59
60
|
|
60
|
-
self.emit_points(
|
61
|
-
|
62
|
-
|
61
|
+
self.emit_points(
|
62
|
+
metric,
|
63
|
+
[[options[:timestamp], value]],
|
64
|
+
options
|
65
|
+
)
|
63
66
|
end
|
64
67
|
|
65
68
|
# Record a set of points of metric data
|
@@ -73,11 +76,8 @@ module Dogapi
|
|
73
76
|
#
|
74
77
|
# options[:type] = "counter" to specify a counter metric
|
75
78
|
# options[:tags] = ["tag1", "tag2"] to tag the point
|
76
|
-
def emit_points(metric, points, options={})
|
77
|
-
|
78
|
-
options = defaults.merge(options)
|
79
|
-
|
80
|
-
scope = override_scope options[:host], options[:device]
|
79
|
+
def emit_points(metric, points, options = {})
|
80
|
+
scope = override_scope options
|
81
81
|
|
82
82
|
points.each do |p|
|
83
83
|
p[0].kind_of? Time or raise "Not a Time"
|
@@ -96,11 +96,8 @@ module Dogapi
|
|
96
96
|
# Optional arguments:
|
97
97
|
# :host => String
|
98
98
|
# :device => String
|
99
|
-
def emit_event(event, options={})
|
100
|
-
|
101
|
-
options = defaults.merge(options)
|
102
|
-
|
103
|
-
scope = override_scope(options[:host], options[:device])
|
99
|
+
def emit_event(event, options = {})
|
100
|
+
scope = override_scope options
|
104
101
|
|
105
102
|
@event_svc.post(event, scope)
|
106
103
|
end
|
@@ -122,18 +119,18 @@ module Dogapi
|
|
122
119
|
# :priority => "normal" or "low"
|
123
120
|
# :sources => String, see https://github.com/DataDog/dogapi/wiki/Event for a current list of sources
|
124
121
|
# :tags => Array of Strings
|
125
|
-
def stream(start, stop, options={})
|
122
|
+
def stream(start, stop, options = {})
|
126
123
|
@event_svc.stream(start, stop, options)
|
127
124
|
end
|
128
125
|
|
129
126
|
# <b>DEPRECATED:</b> Recording events with a duration has been deprecated.
|
130
127
|
# The functionality will be removed in a later release.
|
131
|
-
def start_event(event, options={})
|
128
|
+
def start_event(event, options = {})
|
132
129
|
warn "[DEPRECATION] Dogapi::Client.start_event() is deprecated. Use `emit_event` instead."
|
133
|
-
defaults = {
|
130
|
+
defaults = { :source_type => nil }
|
134
131
|
options = defaults.merge(options)
|
135
132
|
|
136
|
-
scope = override_scope options
|
133
|
+
scope = override_scope options
|
137
134
|
|
138
135
|
@legacy_event_svc.start(@api_key, event, scope, options[:source_type]) do
|
139
136
|
yield
|
@@ -145,12 +142,12 @@ module Dogapi
|
|
145
142
|
#
|
146
143
|
|
147
144
|
# Post a comment
|
148
|
-
def comment(message, options={})
|
145
|
+
def comment(message, options = {})
|
149
146
|
@comment_svc.comment(message, options)
|
150
147
|
end
|
151
148
|
|
152
149
|
# Post a comment
|
153
|
-
def update_comment(comment_id, options={})
|
150
|
+
def update_comment(comment_id, options = {})
|
154
151
|
@comment_svc.update_comment(comment_id, options)
|
155
152
|
end
|
156
153
|
|
@@ -173,14 +170,14 @@ module Dogapi
|
|
173
170
|
#
|
174
171
|
|
175
172
|
# Get all tags and their associated hosts at your org
|
176
|
-
def all_tags(source=nil)
|
173
|
+
def all_tags(source = nil)
|
177
174
|
@tag_svc.get_all(source)
|
178
175
|
end
|
179
176
|
|
180
177
|
# Get all tags for the given host
|
181
178
|
#
|
182
179
|
# +host_id+ can be the host's numeric id or string name
|
183
|
-
def host_tags(host_id, source=nil, by_source=false)
|
180
|
+
def host_tags(host_id, source = nil, by_source = false)
|
184
181
|
@tag_svc.get(host_id, source, by_source)
|
185
182
|
end
|
186
183
|
|
@@ -189,7 +186,7 @@ module Dogapi
|
|
189
186
|
# +host_id+ can be the host's numeric id or string name
|
190
187
|
#
|
191
188
|
# +tags+ is and Array of Strings
|
192
|
-
def add_tags(host_id, tags, source=nil)
|
189
|
+
def add_tags(host_id, tags, source = nil)
|
193
190
|
@tag_svc.add(host_id, tags, source)
|
194
191
|
end
|
195
192
|
|
@@ -198,7 +195,7 @@ module Dogapi
|
|
198
195
|
# +host_id+ can be the host's numeric id or string name
|
199
196
|
#
|
200
197
|
# +tags+ is and Array of Strings
|
201
|
-
def update_tags(host_id, tags, source=nil)
|
198
|
+
def update_tags(host_id, tags, source = nil)
|
202
199
|
@tag_svc.update(host_id, tags, source)
|
203
200
|
end
|
204
201
|
|
@@ -211,7 +208,7 @@ module Dogapi
|
|
211
208
|
# Remove all tags from the given host
|
212
209
|
#
|
213
210
|
# +host_id+ can be the host's numeric id or string name
|
214
|
-
def detach_tags(host_id, source=nil)
|
211
|
+
def detach_tags(host_id, source = nil)
|
215
212
|
@tag_svc.detach(host_id, source)
|
216
213
|
end
|
217
214
|
|
@@ -220,12 +217,12 @@ module Dogapi
|
|
220
217
|
#
|
221
218
|
|
222
219
|
# Create a dashboard.
|
223
|
-
def create_dashboard(title, description, graphs, template_variables=nil)
|
220
|
+
def create_dashboard(title, description, graphs, template_variables = nil)
|
224
221
|
@dash_service.create_dashboard(title, description, graphs, template_variables)
|
225
222
|
end
|
226
223
|
|
227
224
|
# Update a dashboard.
|
228
|
-
def update_dashboard(dash_id, title, description, graphs, template_variables=nil)
|
225
|
+
def update_dashboard(dash_id, title, description, graphs, template_variables = nil)
|
229
226
|
@dash_service.update_dashboard(dash_id, title, description, graphs, template_variables)
|
230
227
|
end
|
231
228
|
|
@@ -248,11 +245,11 @@ module Dogapi
|
|
248
245
|
# ALERTS
|
249
246
|
#
|
250
247
|
|
251
|
-
def alert(query, options={})
|
248
|
+
def alert(query, options = {})
|
252
249
|
@alert_svc.alert(query, options)
|
253
250
|
end
|
254
251
|
|
255
|
-
def update_alert(alert_id, query, options={})
|
252
|
+
def update_alert(alert_id, query, options = {})
|
256
253
|
@alert_svc.update_alert(alert_id, query, options)
|
257
254
|
end
|
258
255
|
|
@@ -277,12 +274,12 @@ module Dogapi
|
|
277
274
|
end
|
278
275
|
|
279
276
|
# User invite
|
280
|
-
def invite(emails, options={})
|
277
|
+
def invite(emails, options = {})
|
281
278
|
@user_svc.invite(emails, options)
|
282
279
|
end
|
283
280
|
|
284
281
|
# Graph snapshot
|
285
|
-
def graph_snapshot(metric_query, start_ts, end_ts, event_query=nil)
|
282
|
+
def graph_snapshot(metric_query, start_ts, end_ts, event_query = nil)
|
286
283
|
@snapshot_svc.snapshot(metric_query, start_ts, end_ts, event_query)
|
287
284
|
end
|
288
285
|
|
@@ -311,18 +308,10 @@ module Dogapi
|
|
311
308
|
|
312
309
|
private
|
313
310
|
|
314
|
-
def override_scope(
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
h = @host
|
319
|
-
end
|
320
|
-
if device
|
321
|
-
d = device
|
322
|
-
else
|
323
|
-
d = @device
|
324
|
-
end
|
325
|
-
Scope.new(h, d)
|
311
|
+
def override_scope(options= {})
|
312
|
+
defaults = { :host => @host, :device => @device }
|
313
|
+
options = defaults.merge(options)
|
314
|
+
Scope.new(options[:host], options[:device])
|
326
315
|
end
|
327
316
|
end
|
328
317
|
end
|