numerousapp 0.9.2 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/numerousapp.rb +416 -15
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bdd91b07bfb89bd80ad1a1ab27e1ca922b50e43
|
4
|
+
data.tar.gz: c94f71b9997951d8ad71f906bd1410905ef90f10
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6eef3a14e6f0342a39062943b155e7bca3222747ad4d9f15049f3194ce0c464379da64c81a87b76cdf9196cf77c1282dc337996fa5495e962223bd705503a310
|
7
|
+
data.tar.gz: 3222957cbdcea1304bad84c199cf2aa3767066eb4b52f048494bc65207018c78f98a9b92a914ba12aa26bdeb352a88c29c8b97dd94e15c1ece41d1b97050df00
|
data/lib/numerousapp.rb
CHANGED
@@ -88,9 +88,13 @@ end
|
|
88
88
|
#
|
89
89
|
class NumerousClientInternals
|
90
90
|
|
91
|
+
|
92
|
+
|
91
93
|
#
|
92
94
|
# @param apiKey [String] API authentication key
|
93
95
|
# @param server [String] Optional (keyword arg). Server name.
|
96
|
+
# @param throttle [Proc] Optional throttle policy
|
97
|
+
# @param throttleData [Any] Optional data for throttle
|
94
98
|
#
|
95
99
|
# @!attribute agentString
|
96
100
|
# @return [String] User agent string sent to the server.
|
@@ -101,8 +105,12 @@ class NumerousClientInternals
|
|
101
105
|
# @!attribute [r] debugLevel
|
102
106
|
# @return [Fixnum] Current debugging level; use debug() method to change.
|
103
107
|
#
|
104
|
-
def initialize(apiKey, server:'api.numerousapp.com'
|
108
|
+
def initialize(apiKey, server:'api.numerousapp.com',
|
109
|
+
throttle:nil, throttleData:nil)
|
105
110
|
|
111
|
+
if not apiKey
|
112
|
+
apiKey = Numerous.numerousKey()
|
113
|
+
end
|
106
114
|
|
107
115
|
@serverName = server
|
108
116
|
@auth = { user: apiKey, password: "" }
|
@@ -113,10 +121,32 @@ class NumerousClientInternals
|
|
113
121
|
@agentString = "NW-Ruby-NumerousClass/" + VersionString +
|
114
122
|
" (Ruby #{RUBY_VERSION}) NumerousAPI/v2"
|
115
123
|
|
124
|
+
@filterDuplicates = true # see discussion elsewhere
|
125
|
+
|
126
|
+
# throttling.
|
127
|
+
# The arbitraryMaximum is just that: under no circumstances will we retry
|
128
|
+
# any particular request more than that. Tough noogies.
|
129
|
+
#
|
130
|
+
# the throttlePolicy "tuple" is:
|
131
|
+
# [ 0 ] - Proc
|
132
|
+
# [ 1 ] - specific data for Proc
|
133
|
+
# [ 2 ] - "up" tuple for chained policy
|
134
|
+
#
|
135
|
+
# and the default policy uses the "data" (40) as the voluntary backoff point
|
136
|
+
#
|
137
|
+
@arbitraryMaximumTries = 10
|
138
|
+
@throttlePolicy = [ThrottleDefault, 40, nil]
|
139
|
+
if throttle
|
140
|
+
@throttlePolicy = [throttle, throttleData, @throttlePolicy]
|
141
|
+
end
|
142
|
+
|
143
|
+
@statistics = Hash.new { |h, k| h[k] = 0 } # just useful debug/testing info
|
116
144
|
@debugLevel = 0
|
145
|
+
|
117
146
|
end
|
118
147
|
attr_accessor :agentString
|
119
148
|
attr_reader :serverName, :debugLevel
|
149
|
+
attr_reader :statistics
|
120
150
|
|
121
151
|
# Set the debug level
|
122
152
|
#
|
@@ -134,9 +164,17 @@ class NumerousClientInternals
|
|
134
164
|
return prev
|
135
165
|
end
|
136
166
|
|
167
|
+
# XXX This is primarily for testing; control filtering of bogus duplicates
|
168
|
+
# If you are calling this you are probably doing something wrong.
|
169
|
+
def setBogusDupFilter(f)
|
170
|
+
prev = @filterDuplicates
|
171
|
+
@filterDuplicates = f
|
172
|
+
return prev
|
173
|
+
end
|
174
|
+
|
137
175
|
protected
|
138
176
|
|
139
|
-
VersionString = '
|
177
|
+
VersionString = '20150123-1.0.0'
|
140
178
|
|
141
179
|
MethMap = {
|
142
180
|
GET: Net::HTTP::Get,
|
@@ -228,6 +266,8 @@ class NumerousClientInternals
|
|
228
266
|
#
|
229
267
|
def simpleAPI(api, jdict:nil, multipart:nil, url:nil)
|
230
268
|
|
269
|
+
@statistics[:simpleAPI] += 1
|
270
|
+
|
231
271
|
# take the base url if you didn't give us an override
|
232
272
|
url ||= api[:basePath]
|
233
273
|
|
@@ -279,14 +319,40 @@ class NumerousClientInternals
|
|
279
319
|
end
|
280
320
|
end
|
281
321
|
|
282
|
-
resp =
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
resp
|
287
|
-
|
322
|
+
resp = nil # ick, is there a better way to get this out of the block?
|
323
|
+
@arbitraryMaximumTries.times do |attempt|
|
324
|
+
|
325
|
+
@statistics[:serverRequests] += 1
|
326
|
+
resp = @http.request(rq)
|
327
|
+
|
328
|
+
if @debugLevel > 0
|
329
|
+
puts "Response headers:\n"
|
330
|
+
resp.each do | k, v |
|
331
|
+
puts "k: " + k + " :: " + v + "\n"
|
332
|
+
end
|
333
|
+
puts "Code: " + resp.code + "/" + resp.code.class.to_s + "/\n"
|
334
|
+
end
|
335
|
+
|
336
|
+
# invoke the rate-limiting policy
|
337
|
+
rateRemain = resp['x-rate-limit-remaining'].to_i
|
338
|
+
rateReset = resp['x-rate-limit-reset'].to_i
|
339
|
+
@statistics[:rateRemaining] = rateRemain
|
340
|
+
@statistics[:rateReset] = rateReset
|
341
|
+
|
342
|
+
tp = { :debug=> @debug,
|
343
|
+
:attempt=> attempt,
|
344
|
+
:rateRemaining=> rateRemain,
|
345
|
+
:rateReset=> rateReset,
|
346
|
+
:resultCode=> resp.code.to_i,
|
347
|
+
:resp=> resp,
|
348
|
+
:statistics=> @statistics,
|
349
|
+
:request=> { :httpMethod => api[:httpMethod], :url => path } }
|
350
|
+
|
351
|
+
td = @throttlePolicy[1]
|
352
|
+
up = @throttlePolicy[2]
|
353
|
+
if not @throttlePolicy[0].call(self, tp, td, up)
|
354
|
+
break
|
288
355
|
end
|
289
|
-
puts "Code: " + resp.code + "/" + resp.code.class.to_s + "/\n"
|
290
356
|
end
|
291
357
|
|
292
358
|
goodCodes = api[:successCodes] || [200]
|
@@ -352,6 +418,14 @@ class NumerousClientInternals
|
|
352
418
|
api = makeAPIcontext(info, :GET, subs)
|
353
419
|
list = []
|
354
420
|
nextURL = api[:basePath]
|
421
|
+
firstTime = true
|
422
|
+
|
423
|
+
# see discussion about duplicate filtering below
|
424
|
+
if @filterDuplicates and api[:dupFilter]
|
425
|
+
filterInfo = { prev: {}, current: {} }
|
426
|
+
else
|
427
|
+
filterInfo = nil
|
428
|
+
end
|
355
429
|
|
356
430
|
while nextURL
|
357
431
|
# get a chunk from the server
|
@@ -362,16 +436,167 @@ class NumerousClientInternals
|
|
362
436
|
# But here we're just letting the whatever-exceptions filter up
|
363
437
|
v = simpleAPI(api, url:nextURL)
|
364
438
|
|
439
|
+
# statistics, helpful for testing/debugging. Algorithmically
|
440
|
+
# we don't really care about first time or not, just for the stats
|
441
|
+
if firstTime
|
442
|
+
@statistics[:firstChunks] += 1
|
443
|
+
firstTime = false
|
444
|
+
else
|
445
|
+
@statistics[:additionalChunks] += 1
|
446
|
+
end
|
447
|
+
|
448
|
+
if filterInfo
|
449
|
+
filterInfo[:prev] = filterInfo[:current]
|
450
|
+
filterInfo[:current] = {}
|
451
|
+
end
|
452
|
+
|
365
453
|
list = v[api[:list]]
|
366
454
|
nextURL = v[api[:next]]
|
367
455
|
|
368
456
|
# hand them out
|
369
457
|
if list # can be nil for a variety of reasons
|
370
|
-
list.each
|
458
|
+
list.each do |i|
|
459
|
+
|
460
|
+
# A note about duplicate filtering
|
461
|
+
#
|
462
|
+
# There is a bug in the NumerousApp server which can
|
463
|
+
# cause collections to show duplicates of certain events
|
464
|
+
# (or interactions/stream items). Explaining the bug in great
|
465
|
+
# detail is beyond the scope here; suffice to say it only
|
466
|
+
# happens for events that were recorded nearly-simultaneously
|
467
|
+
# and happen to be getting reported right at a chunking boundary.
|
468
|
+
#
|
469
|
+
# So we are filtering them out here. For a more involved
|
470
|
+
# discussion of this, see the python implementation. This
|
471
|
+
# filtering "works" because it knows pragmatically how/where
|
472
|
+
# the bug can show up
|
473
|
+
#
|
474
|
+
# Turning off duplicate filtering is really meant only for testing.
|
475
|
+
#
|
476
|
+
# Not all API's require dupfiltering, hence the APIInfo test
|
477
|
+
#
|
478
|
+
if (not filterInfo) # the easy case, not filtering
|
479
|
+
block.call i
|
480
|
+
else
|
481
|
+
thisId = i[api[:dupFilter]]
|
482
|
+
if filterInfo[:prev].include? thisId
|
483
|
+
@statistics[:duplicatesFiltered] += 1
|
484
|
+
else
|
485
|
+
filterInfo[:current][thisId] = 1
|
486
|
+
block.call i
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
371
490
|
end
|
372
491
|
end
|
373
492
|
return nil # the subclasses return (should return) their own self
|
374
493
|
end
|
494
|
+
|
495
|
+
#
|
496
|
+
# The default throttle policy.
|
497
|
+
# Invoked after the response has been received and we are supposed to
|
498
|
+
# return true to force a retry or false to accept this response as-is.
|
499
|
+
#
|
500
|
+
# The policy this implements:
|
501
|
+
# if the server failed with too busy, do backoff based on attempt number
|
502
|
+
#
|
503
|
+
# if we are "getting close" to our limit, arbitrarily delay ourselves
|
504
|
+
#
|
505
|
+
# if we truly got spanked with "Too Many Requests"
|
506
|
+
# then delay the amount of time the server told us to delay.
|
507
|
+
#
|
508
|
+
# The arguments supplied to us are:
|
509
|
+
# nr is the Numerous (handled explicitly so you can write external funcs too)
|
510
|
+
# tparams is a Hash containing:
|
511
|
+
# :attempt : the attempt number. Zero on the very first try
|
512
|
+
# :rateRemaining : X-Rate-Limit-Remaining reported by the server
|
513
|
+
# :rateReset : time (in seconds) until fresh rate granted
|
514
|
+
# :resultCode : HTTP code from the server (e.g., 409, 200, etc)
|
515
|
+
# :resp : the full-on response object if you must have it
|
516
|
+
# :request : information about the original request
|
517
|
+
# :statistics : place to record stats (purely informational stats)
|
518
|
+
# :debug : current debug level
|
519
|
+
#
|
520
|
+
# td is the data you supplied as "throttleData" to the Numerous() constructor
|
521
|
+
# up is a tuple useful for calling the original system throttle policy:
|
522
|
+
# up[0] is the function pointer
|
523
|
+
# up[1] is the td for *that* function
|
524
|
+
# up[2] is the "up" for calling *that* function
|
525
|
+
# ... so after you do your own thing if you then want to defer to the
|
526
|
+
# built-in throttle policy you can
|
527
|
+
# return send(up[0], nr, tparams, up[1], up[2])
|
528
|
+
#
|
529
|
+
# It's really (really really) important to understand the return value and
|
530
|
+
# the fact that we are invoked AFTER each request:
|
531
|
+
# false : simply means "don't do more retries". It does not imply anything
|
532
|
+
# about the success or failure of the request; it simply means that
|
533
|
+
# this most recent request (response) is the one to "take" as
|
534
|
+
# the final answer
|
535
|
+
#
|
536
|
+
# true : means that the response is, indeed, to be interpreted as some
|
537
|
+
# sort of rate-limit failure and should be discarded. The original
|
538
|
+
# request will be sent again. Obviously it's a very bad idea to
|
539
|
+
# return true in cases where the server might have done some
|
540
|
+
# anything non-idempotent. We assume that a 429 ("Too Many") or
|
541
|
+
# a 500 ("Too Busy") response from the server means the server didn't
|
542
|
+
# actually do anything (so a retry, timed appropriately, is ok)
|
543
|
+
#
|
544
|
+
# All of this seems overly general for what basically amounts to "sleep sometimes"
|
545
|
+
#
|
546
|
+
|
547
|
+
ThrottleDefault = Proc.new do |nr, tparams, td, up|
|
548
|
+
rateleft = tparams[:rateRemaining]
|
549
|
+
attempt = tparams[:attempt] # note: is zero on very first try
|
550
|
+
stats = tparams[:statistics]
|
551
|
+
|
552
|
+
if attempt > 0
|
553
|
+
stats[:throttleMultipleAttempts] += 1
|
554
|
+
end
|
555
|
+
|
556
|
+
backarray = [ 2, 5, 15, 30, 60 ]
|
557
|
+
if attempt < backarray.length
|
558
|
+
backoff = backarray[attempt]
|
559
|
+
else
|
560
|
+
stats[:throttleMaxed] += 1
|
561
|
+
next false # too many tries
|
562
|
+
end
|
563
|
+
|
564
|
+
# if the server actually has failed with too busy, sleep and try again
|
565
|
+
if tparams[:resultCode] == 500
|
566
|
+
stats[:throttle500] += 1
|
567
|
+
sleep(backoff)
|
568
|
+
next true
|
569
|
+
end
|
570
|
+
|
571
|
+
# if we weren't told to back off, no need to retry
|
572
|
+
if tparams[:resultCode] != 429
|
573
|
+
# but if we are closing in on the limit then slow ourselves down
|
574
|
+
# note that some errors don't communicate rateleft so we have to
|
575
|
+
# check for that as well (will be -1 here if wasn't sent to us)
|
576
|
+
#
|
577
|
+
# at constructor time our "throttle data" (td) was set up as the
|
578
|
+
# voluntary arbitrary limit
|
579
|
+
if rateleft >= 0 and rateleft < td
|
580
|
+
stats[:throttleVoluntaryBackoff] += 1
|
581
|
+
# arbitrary .. 1 second if more than half left, 3 seconds if less
|
582
|
+
if (rateleft*2) > td
|
583
|
+
sleep(1)
|
584
|
+
else
|
585
|
+
sleep(3)
|
586
|
+
end
|
587
|
+
end
|
588
|
+
next false # no retry
|
589
|
+
end
|
590
|
+
|
591
|
+
# decide how long to delay ... we just wait for as long as the
|
592
|
+
# server told us to (plus "backoff" seconds slop to really be sure we
|
593
|
+
# aren't back too soon)
|
594
|
+
stats[:throttle429] += 1
|
595
|
+
sleep(tparams[:rateReset] + backoff)
|
596
|
+
next true
|
597
|
+
end
|
598
|
+
private_constant :ThrottleDefault
|
599
|
+
|
375
600
|
end
|
376
601
|
|
377
602
|
#
|
@@ -584,6 +809,145 @@ class Numerous < NumerousClientInternals
|
|
584
809
|
return NumerousMetric.new(id, self)
|
585
810
|
end
|
586
811
|
|
812
|
+
|
813
|
+
#
|
814
|
+
# Version of metric() that accepts a name (label)
|
815
|
+
# instead of an ID, and can even process it as a regexp.
|
816
|
+
#
|
817
|
+
# @param [String] labelspec The name (label) or regexp
|
818
|
+
# @param [String] matchType 'FIRST' or 'BEST' or 'ONE' or 'STRING' (not regexp)
|
819
|
+
# @param [Numerous] nr
|
820
|
+
# The {Numerous} object that will be used to access this metric.
|
821
|
+
def metricByLabel(labelspec, matchType:'FIRST')
|
822
|
+
def raiseConflict(s1,s2)
|
823
|
+
raise NumerousMetricConflictError.new("Multiple matches", 409, [s1, s2])
|
824
|
+
end
|
825
|
+
|
826
|
+
bestMatch = [ nil, 0 ]
|
827
|
+
|
828
|
+
if not matchType
|
829
|
+
matchType = 'FIRST'
|
830
|
+
end
|
831
|
+
if not ['FIRST', 'BEST', 'ONE', 'STRING'].include?(matchType)
|
832
|
+
raise ArgumentError
|
833
|
+
end
|
834
|
+
|
835
|
+
self.metrics do |m|
|
836
|
+
if matchType == 'STRING' # exact full match required
|
837
|
+
if m['label'] == labelspec
|
838
|
+
if bestMatch[0]
|
839
|
+
raiseConflict(bestMatch[0]['label'], m['label'])
|
840
|
+
end
|
841
|
+
bestMatch = [ m, 1 ] # the length is irrelevant with STRING
|
842
|
+
end
|
843
|
+
else
|
844
|
+
mm = labelspec.match(m['label'])
|
845
|
+
if mm
|
846
|
+
if matchType == 'FIRST'
|
847
|
+
return self.metric(m['id'])
|
848
|
+
elsif (matchType == 'ONE') and (bestMatch[1] > 0)
|
849
|
+
raiseConflict(bestMatch[0]['label'], m['label'])
|
850
|
+
end
|
851
|
+
if mm[0].length > bestMatch[1]
|
852
|
+
bestMatch = [ m, mm[0].length ]
|
853
|
+
end
|
854
|
+
end
|
855
|
+
end
|
856
|
+
end
|
857
|
+
rv = nil
|
858
|
+
if bestMatch[0]
|
859
|
+
rv = self.metric(bestMatch[0]['id'])
|
860
|
+
end
|
861
|
+
end
|
862
|
+
|
863
|
+
|
864
|
+
#
|
865
|
+
# I found this a good way to handle supplying the API key so it's here
|
866
|
+
# as a class method you may find useful. What this function does is
|
867
|
+
# return you an API Key from a supplied string or "readable" object:
|
868
|
+
#
|
869
|
+
# a "naked" API key (in which case this function is a no-op)
|
870
|
+
# @- :: meaning "get it from stdin"
|
871
|
+
# @blah :: meaning "get it from the file "blah"
|
872
|
+
# /blah :: get it from file /blah
|
873
|
+
# .blah :: get it from file .blah (could be ../ etc)
|
874
|
+
# /readable/ :: if it has a .read method, get it that way
|
875
|
+
# None :: get it from environment variable NUMEROUSAPIKEY
|
876
|
+
#
|
877
|
+
# Where the "it" that is being gotten from any of those sources can be:
|
878
|
+
# a "naked" API key
|
879
|
+
# a JSON object, from which the credsAPIKey will be used to get the key
|
880
|
+
#
|
881
|
+
# Arguably this doesn't belong here, but it's helpful. Purists are free to
|
882
|
+
# ignore it or delete from their own tree :)
|
883
|
+
#
|
884
|
+
|
885
|
+
def self.numerousKey(s:nil, credsAPIKey:'NumerousAPIKey')
|
886
|
+
|
887
|
+
if not s
|
888
|
+
# try to get from environment
|
889
|
+
s = ENV['NUMEROUSAPIKEY']
|
890
|
+
if not s
|
891
|
+
return nil
|
892
|
+
end
|
893
|
+
end
|
894
|
+
|
895
|
+
closeThis = nil
|
896
|
+
|
897
|
+
if s == "@-" # creds coming from stdin
|
898
|
+
s = STDIN
|
899
|
+
|
900
|
+
# see if they are in a file
|
901
|
+
else
|
902
|
+
begin
|
903
|
+
if s.length() > 0 # is it a string or a file object?
|
904
|
+
# it's stringy - if it looks like a file open it or fail
|
905
|
+
begin
|
906
|
+
if s.length() > 1 and s[0] == '@'
|
907
|
+
s = open(s[1..-1])
|
908
|
+
closeThis = s
|
909
|
+
elsif s[0] == '/' or s[0] == '.'
|
910
|
+
s = open(s)
|
911
|
+
closeThis = s
|
912
|
+
end
|
913
|
+
rescue
|
914
|
+
return nil
|
915
|
+
end
|
916
|
+
end
|
917
|
+
rescue NoMethodError # it wasn't stringy, presumably it's a "readable"
|
918
|
+
end
|
919
|
+
end
|
920
|
+
|
921
|
+
# well, see if whatever it is, is readable, and go with that if it is
|
922
|
+
begin
|
923
|
+
v = s.read()
|
924
|
+
if closeThis
|
925
|
+
closeThis.close()
|
926
|
+
end
|
927
|
+
s = v
|
928
|
+
rescue NoMethodError
|
929
|
+
end
|
930
|
+
|
931
|
+
# at this point s is either a JSON or a naked cred (or bogus)
|
932
|
+
begin
|
933
|
+
j = JSON.parse(s)
|
934
|
+
rescue TypeError, JSON::ParserError
|
935
|
+
j = {}
|
936
|
+
end
|
937
|
+
|
938
|
+
|
939
|
+
#
|
940
|
+
# This is kind of a hack and might hide some errors on your part
|
941
|
+
#
|
942
|
+
if not j.include? credsAPIKey # this is how the naked case happens
|
943
|
+
# replace() bcs there might be a trailing newline on naked creds
|
944
|
+
# (usually happens with a file or stdin)
|
945
|
+
j[credsAPIKey] = s.sub("\n",'')
|
946
|
+
end
|
947
|
+
|
948
|
+
return j[credsAPIKey]
|
949
|
+
end
|
950
|
+
|
587
951
|
end
|
588
952
|
|
589
953
|
#
|
@@ -623,12 +987,48 @@ class NumerousMetric < NumerousClientInternals
|
|
623
987
|
# @param [String] id The metric ID string.
|
624
988
|
# @param [Numerous] nr
|
625
989
|
# The {Numerous} object that will be used to access this metric.
|
990
|
+
#
|
991
|
+
# "id" should normally be the naked metric id (as a string).
|
992
|
+
#
|
993
|
+
# It can also be a nmrs: URL, e.g.:
|
994
|
+
# nmrs://metric/2733614827342384
|
995
|
+
#
|
996
|
+
# Or a 'self' link from the API:
|
997
|
+
# https://api.numerousapp.com/metrics/2733614827342384
|
998
|
+
#
|
999
|
+
# in either case we get the ID in the obvious syntactic way.
|
1000
|
+
#
|
1001
|
+
# It can also be a metric's web link, e.g.:
|
1002
|
+
# http://n.numerousapp.com/m/1x8ba7fjg72d
|
1003
|
+
#
|
1004
|
+
# in which case we "just know" that the tail is a base36
|
1005
|
+
# encoding of the ID.
|
1006
|
+
#
|
1007
|
+
# The decoding logic here makes the specific assumption that
|
1008
|
+
# the presence of a '/' indicates a non-naked metric ID. This
|
1009
|
+
# seems a reasonable assumption given that IDs have to go into URLs
|
1010
|
+
#
|
1011
|
+
|
626
1012
|
def initialize(id, nr)
|
1013
|
+
begin
|
1014
|
+
if id.include? '/'
|
1015
|
+
fields = id.split('/')
|
1016
|
+
if fields[-2] == "m"
|
1017
|
+
id = fields[-1].to_i(36)
|
1018
|
+
else
|
1019
|
+
id = fields[-1]
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
rescue NoMethodError
|
1023
|
+
id = id.to_s
|
1024
|
+
end
|
1025
|
+
|
627
1026
|
@id = id
|
628
1027
|
@nr = nr
|
629
1028
|
end
|
630
1029
|
attr_reader :id
|
631
1030
|
|
1031
|
+
|
632
1032
|
#
|
633
1033
|
# Obtain the {Numerous} server object associated with a metric.
|
634
1034
|
# @return [Numerous]
|
@@ -652,7 +1052,8 @@ class NumerousMetric < NumerousClientInternals
|
|
652
1052
|
path: '/v1/metrics/%{metricId}/events',
|
653
1053
|
GET: {
|
654
1054
|
next: 'nextURL',
|
655
|
-
list: 'events'
|
1055
|
+
list: 'events',
|
1056
|
+
dupFilter: 'id'
|
656
1057
|
},
|
657
1058
|
POST: {
|
658
1059
|
successCodes: [ 201 ]
|
@@ -672,7 +1073,8 @@ class NumerousMetric < NumerousClientInternals
|
|
672
1073
|
path: '/v2/metrics/%{metricId}/stream',
|
673
1074
|
GET: {
|
674
1075
|
next: 'next',
|
675
|
-
list: 'items'
|
1076
|
+
list: 'items',
|
1077
|
+
dupFilter: 'id'
|
676
1078
|
}
|
677
1079
|
},
|
678
1080
|
|
@@ -681,7 +1083,8 @@ class NumerousMetric < NumerousClientInternals
|
|
681
1083
|
path: '/v2/metrics/%{metricId}/interactions',
|
682
1084
|
GET: {
|
683
1085
|
next: 'nextURL',
|
684
|
-
list: 'interactions'
|
1086
|
+
list: 'interactions',
|
1087
|
+
dupFilter: 'id'
|
685
1088
|
},
|
686
1089
|
POST: {
|
687
1090
|
successCodes: [ 201 ]
|
@@ -1117,5 +1520,3 @@ class NumerousMetric < NumerousClientInternals
|
|
1117
1520
|
|
1118
1521
|
end
|
1119
1522
|
|
1120
|
-
|
1121
|
-
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: numerousapp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Neil Webber
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-23 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Classes implementing the NumerousApp REST APIs for metrics. Requires
|
14
14
|
Ruby 2.x
|