numerousapp 0.9.2 → 1.0.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.
- 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
|