numerousapp 1.0.2 → 1.1.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 +111 -39
- 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: 2df08c0abb56a4d38ce98c048fb814a37cb4ca37
|
4
|
+
data.tar.gz: 50b9313c710e3ab6c27a32cd895c10b03671bdfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e62912986ab1ce0f5377dd5a400c72fe749c61fa886689552cfa7c9e38c2f6747eb089d0d9f32129c669eaeb46990749d1c5dcd0051fdbd6f9b46c275b1d4c3
|
7
|
+
data.tar.gz: f124700210064a362c407fb976272aa874eced217ca947991fb0b2cdb6428dfd47782650efb455b4915eee0e32b2e58a773750d86c6b996c63c9ac796a901492
|
data/lib/numerousapp.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#
|
2
2
|
# The MIT License (MIT)
|
3
3
|
#
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) 2015 Neil Webber
|
5
5
|
#
|
6
6
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
7
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -39,12 +39,17 @@ require 'net/http'
|
|
39
39
|
require 'uri'
|
40
40
|
|
41
41
|
#
|
42
|
-
# NumerousError
|
42
|
+
# == NumerousError
|
43
|
+
#
|
44
|
+
# Exceptions indicate errors from the server
|
43
45
|
#
|
44
46
|
class NumerousError < StandardError
|
47
|
+
|
45
48
|
#
|
46
|
-
#
|
47
|
-
#
|
49
|
+
# initialize a NumerousError
|
50
|
+
#
|
51
|
+
# @param msg
|
52
|
+
# StandardError message attribute
|
48
53
|
#
|
49
54
|
# @!attribute code
|
50
55
|
# HTTP error code (e.g., 404)
|
@@ -53,7 +58,6 @@ class NumerousError < StandardError
|
|
53
58
|
# hash containing more ad-hoc information, most usefully :id which
|
54
59
|
# is the URL that was used in the request to the server
|
55
60
|
#
|
56
|
-
|
57
61
|
def initialize(msg, code, details)
|
58
62
|
super(msg)
|
59
63
|
@code = code
|
@@ -70,12 +74,28 @@ end
|
|
70
74
|
class NumerousAuthError < NumerousError
|
71
75
|
end
|
72
76
|
|
77
|
+
#
|
78
|
+
# A NumerousNetworkError occurs when there is a "somewhat normal" network
|
79
|
+
# error. The library catches the lower level exceptions that normally happen
|
80
|
+
# when the network is down or the HTTP connection times out and translates
|
81
|
+
# those into this exception so you can rescue this without being exposed to
|
82
|
+
# all the details of the lower-level networking exceptions.
|
83
|
+
#
|
84
|
+
class NumerousNetworkError < NumerousError
|
85
|
+
def initialize(xc)
|
86
|
+
super("Network Error", -1, { :netException => xc })
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
73
90
|
#
|
74
91
|
# A NumerousMetricConflictError occurs when you write to a metric
|
75
92
|
# and specified "only if changed" and your value
|
76
93
|
# was (already) the current value
|
77
94
|
#
|
78
95
|
class NumerousMetricConflictError < NumerousError
|
96
|
+
def initialize(msg, details)
|
97
|
+
super(msg, 409, details)
|
98
|
+
end
|
79
99
|
end
|
80
100
|
|
81
101
|
#
|
@@ -103,9 +123,10 @@ class NumerousClientInternals
|
|
103
123
|
# @!attribute [r] debugLevel
|
104
124
|
# @return [Fixnum] Current debugging level; use debug() method to change.
|
105
125
|
#
|
106
|
-
def initialize(apiKey, server:'api.numerousapp.com',
|
126
|
+
def initialize(apiKey=nil, server:'api.numerousapp.com',
|
107
127
|
throttle:nil, throttleData:nil)
|
108
128
|
|
129
|
+
# specifying apiKey=nil asks us to get key from various default places.
|
109
130
|
if not apiKey
|
110
131
|
apiKey = Numerous.numerousKey()
|
111
132
|
end
|
@@ -130,7 +151,8 @@ class NumerousClientInternals
|
|
130
151
|
# [ 1 ] - specific data for Proc
|
131
152
|
# [ 2 ] - "up" tuple for chained policy
|
132
153
|
#
|
133
|
-
# and the default policy uses the "data"
|
154
|
+
# and the default policy uses the "data" as a hash of parameters:
|
155
|
+
# :voluntary -- the threshold point for voluntary backoff
|
134
156
|
#
|
135
157
|
@arbitraryMaximumTries = 10
|
136
158
|
voluntary = { voluntary: 40}
|
@@ -143,7 +165,7 @@ class NumerousClientInternals
|
|
143
165
|
@throttlePolicy = [throttle, throttleData, @throttlePolicy]
|
144
166
|
end
|
145
167
|
|
146
|
-
@statistics = Hash.new { |h, k| h[k] = 0 } #
|
168
|
+
@statistics = Hash.new { |h, k| h[k] = 0 } # statistics are "infotainment"
|
147
169
|
@debugLevel = 0
|
148
170
|
|
149
171
|
end
|
@@ -151,8 +173,11 @@ class NumerousClientInternals
|
|
151
173
|
attr_reader :serverName, :debugLevel
|
152
174
|
attr_reader :statistics
|
153
175
|
|
176
|
+
# String representation of Numerous
|
177
|
+
#
|
178
|
+
# @return [String] Human-appropriate string representation.
|
154
179
|
def to_s()
|
155
|
-
oid = (2 * self.object_id).to_s(16)
|
180
|
+
oid = (2 * self.object_id).to_s(16) # XXX "2*" makes it match native to_s
|
156
181
|
return "<Numerous {#{@serverName}} @ 0x#{oid}>"
|
157
182
|
end
|
158
183
|
|
@@ -172,8 +197,13 @@ class NumerousClientInternals
|
|
172
197
|
return prev
|
173
198
|
end
|
174
199
|
|
175
|
-
#
|
176
|
-
#
|
200
|
+
#
|
201
|
+
# This is primarily for testing; control filtering of bogus duplicates
|
202
|
+
# @note If you are calling this you are probably doing something wrong.
|
203
|
+
#
|
204
|
+
# @param [Boolean] f
|
205
|
+
# New value for duplicate filtering flag.
|
206
|
+
# @return [Boolean] Previous value of duplicate filtering flag.
|
177
207
|
def setBogusDupFilter(f)
|
178
208
|
prev = @filterDuplicates
|
179
209
|
@filterDuplicates = f
|
@@ -182,7 +212,7 @@ class NumerousClientInternals
|
|
182
212
|
|
183
213
|
protected
|
184
214
|
|
185
|
-
VersionString = '
|
215
|
+
VersionString = '20150222-1.1.0'
|
186
216
|
|
187
217
|
MethMap = {
|
188
218
|
GET: Net::HTTP::Get,
|
@@ -197,7 +227,6 @@ class NumerousClientInternals
|
|
197
227
|
# and fills in the variable fields in URLs. It returns an "api context"
|
198
228
|
# containing all the API-specific details needed by simpleAPI.
|
199
229
|
#
|
200
|
-
|
201
230
|
def makeAPIcontext(info, whichOp, kwargs={})
|
202
231
|
rslt = {}
|
203
232
|
rslt[:httpMethod] = whichOp
|
@@ -238,8 +267,9 @@ class NumerousClientInternals
|
|
238
267
|
BCharsLen = BChars.length
|
239
268
|
|
240
269
|
def makeboundary(s)
|
241
|
-
# Just try something fixed, and if it is no good extend it with random
|
242
|
-
|
270
|
+
# Just try something fixed, and if it is no good extend it with random.
|
271
|
+
# For amusing porpoises make it this way so we don't also contain it.
|
272
|
+
b = "RoLlErCaSeDbOuNdArY867".b + "5309".b
|
243
273
|
while s.include? b
|
244
274
|
b += BChars[rand(BCharsLen)]
|
245
275
|
end
|
@@ -342,7 +372,16 @@ class NumerousClientInternals
|
|
342
372
|
|
343
373
|
@statistics[:serverRequests] += 1
|
344
374
|
t0 = Time.now
|
345
|
-
|
375
|
+
begin
|
376
|
+
resp = @http.request(rq)
|
377
|
+
rescue StandardError => e
|
378
|
+
# it's PDB (pretty bogus) that we have to rescue
|
379
|
+
# StandardError but the underlying http library can just throw
|
380
|
+
# too many exceptions to know what they all are; it really
|
381
|
+
# should have encapsulated them into an HTTPNetError class...
|
382
|
+
# so, we'll just assume any "standard error" is a network issue
|
383
|
+
raise NumerousNetworkError.new(e)
|
384
|
+
end
|
346
385
|
et = Time.now - t0
|
347
386
|
# We report the elapsed round-trip time, either as a scalar (default)
|
348
387
|
# OR if you preset the :serverResponseTimes to be an array of length N
|
@@ -377,7 +416,10 @@ class NumerousClientInternals
|
|
377
416
|
:resultCode=> resp.code.to_i,
|
378
417
|
:resp=> resp,
|
379
418
|
:statistics=> @statistics,
|
380
|
-
:request=> { :httpMethod => api[:httpMethod],
|
419
|
+
:request=> { :httpMethod => api[:httpMethod],
|
420
|
+
:url => path,
|
421
|
+
:jdict => jdict }
|
422
|
+
}
|
381
423
|
|
382
424
|
td = @throttlePolicy[1]
|
383
425
|
up = @throttlePolicy[2]
|
@@ -529,9 +571,7 @@ class NumerousClientInternals
|
|
529
571
|
# return true to force a retry or false to accept this response as-is.
|
530
572
|
#
|
531
573
|
# The policy this implements:
|
532
|
-
# if
|
533
|
-
#
|
534
|
-
# if we are "getting close" to our limit, arbitrarily delay ourselves
|
574
|
+
# if we are "getting close" to our limit, arbitrarily delay ourselves.
|
535
575
|
#
|
536
576
|
# if we truly got spanked with "Too Many Requests"
|
537
577
|
# then delay the amount of time the server told us to delay.
|
@@ -568,9 +608,7 @@ class NumerousClientInternals
|
|
568
608
|
# sort of rate-limit failure and should be discarded. The original
|
569
609
|
# request will be sent again. Obviously it's a very bad idea to
|
570
610
|
# return true in cases where the server might have done some
|
571
|
-
# anything non-idempotent.
|
572
|
-
# a 500 ("Too Busy") response from the server means the server didn't
|
573
|
-
# actually do anything (so a retry, timed appropriately, is ok)
|
611
|
+
# anything non-idempotent.
|
574
612
|
#
|
575
613
|
# All of this seems overly general for what basically amounts to "sleep sometimes"
|
576
614
|
#
|
@@ -596,13 +634,6 @@ class NumerousClientInternals
|
|
596
634
|
next false # too many tries
|
597
635
|
end
|
598
636
|
|
599
|
-
# if the server actually has failed with too busy, sleep and try again
|
600
|
-
if tparams[:resultCode] == 500
|
601
|
-
stats[:throttle500] += 1
|
602
|
-
sleep(backoff)
|
603
|
-
next true
|
604
|
-
end
|
605
|
-
|
606
637
|
# if we weren't told to back off, no need to retry
|
607
638
|
if tparams[:resultCode] != 429
|
608
639
|
# but if we are closing in on the limit then slow ourselves down
|
@@ -844,6 +875,11 @@ class Numerous < NumerousClientInternals
|
|
844
875
|
return NumerousMetric.new(id, self)
|
845
876
|
end
|
846
877
|
|
878
|
+
# just a DRY shorthand for use in metricByLabel
|
879
|
+
RaiseConflict = lambda { |s1, s2|
|
880
|
+
raise NumerousMetricConflictError.new("Multiple matches", [s1, s2])
|
881
|
+
}
|
882
|
+
private_constant :RaiseConflict
|
847
883
|
|
848
884
|
#
|
849
885
|
# Version of metric() that accepts a name (label)
|
@@ -853,9 +889,6 @@ class Numerous < NumerousClientInternals
|
|
853
889
|
# @param [String] matchType 'FIRST','BEST','ONE','STRING' or 'ID'
|
854
890
|
#
|
855
891
|
def metricByLabel(labelspec, matchType:'FIRST')
|
856
|
-
def raiseConflict(s1,s2)
|
857
|
-
raise NumerousMetricConflictError.new("Multiple matches", 409, [s1, s2])
|
858
|
-
end
|
859
892
|
|
860
893
|
bestMatch = [ nil, 0 ]
|
861
894
|
|
@@ -889,7 +922,7 @@ class Numerous < NumerousClientInternals
|
|
889
922
|
if matchType == 'STRING' # exact full match required
|
890
923
|
if m['label'] == labelspec
|
891
924
|
if bestMatch[0]
|
892
|
-
|
925
|
+
RaiseConflict.call(bestMatch[0]['label'], m['label'])
|
893
926
|
end
|
894
927
|
bestMatch = [ m, 1 ] # the length is irrelevant with STRING
|
895
928
|
end
|
@@ -899,7 +932,7 @@ class Numerous < NumerousClientInternals
|
|
899
932
|
if matchType == 'FIRST'
|
900
933
|
return self.metric(m['id'])
|
901
934
|
elsif (matchType == 'ONE') and (bestMatch[1] > 0)
|
902
|
-
|
935
|
+
RaiseConflict.call(bestMatch[0]['label'], m['label'])
|
903
936
|
end
|
904
937
|
if mm[0].length > bestMatch[1]
|
905
938
|
bestMatch = [ m, mm[0].length ]
|
@@ -935,6 +968,12 @@ class Numerous < NumerousClientInternals
|
|
935
968
|
# ignore it or delete from their own tree :)
|
936
969
|
#
|
937
970
|
|
971
|
+
# find an apikey from various default places
|
972
|
+
# @param [String] s
|
973
|
+
# See documentation for details; a file name or a key or a "readable" object.
|
974
|
+
# @param [String] credsAPIKey
|
975
|
+
# Key to use in accessing json dict if one is found.
|
976
|
+
# @return [String] the API key.
|
938
977
|
def self.numerousKey(s:nil, credsAPIKey:'NumerousAPIKey')
|
939
978
|
|
940
979
|
if not s
|
@@ -1069,7 +1108,15 @@ class NumerousMetric < NumerousClientInternals
|
|
1069
1108
|
# though it's handy sometimes in cut/paste interactive testing/use.
|
1070
1109
|
#
|
1071
1110
|
|
1072
|
-
def initialize(id, nr)
|
1111
|
+
def initialize(id, nr=nil)
|
1112
|
+
|
1113
|
+
# If you don't specify a Numerous we'll make one for you.
|
1114
|
+
# For this to work, NUMEROUSAPIKEY environment variable must exist.
|
1115
|
+
# m = NumerousMetric.new('234234234') is ok for simple one-shots
|
1116
|
+
# but note it makes a private Numerous for every metric.
|
1117
|
+
|
1118
|
+
nr ||= Numerous.new(nil)
|
1119
|
+
|
1073
1120
|
actualId = nil
|
1074
1121
|
begin
|
1075
1122
|
fields = id.split('/')
|
@@ -1112,7 +1159,9 @@ class NumerousMetric < NumerousClientInternals
|
|
1112
1159
|
# read/update/delete a metric
|
1113
1160
|
metric: {
|
1114
1161
|
path: '/v1/metrics/%{metricId}' ,
|
1115
|
-
#
|
1162
|
+
PUT: { # note that PUT has a /v2 interface but GET does not (yet?).
|
1163
|
+
path: '/v2/metrics/%{metricId}'
|
1164
|
+
},
|
1116
1165
|
DELETE: {
|
1117
1166
|
successCodes: [ 204 ]
|
1118
1167
|
}
|
@@ -1449,6 +1498,17 @@ class NumerousMetric < NumerousClientInternals
|
|
1449
1498
|
# causes the server to ADD newval to the current metric value.
|
1450
1499
|
# Note that this IS atomic at the server. Two clients doing
|
1451
1500
|
# simultaneous ADD operations will get the correct (serialized) result.
|
1501
|
+
#
|
1502
|
+
# @param [String] updated
|
1503
|
+
# updated allows you to specify the timestamp associated with the value
|
1504
|
+
# -- it must be a string in the format described in the NumerousAPI
|
1505
|
+
# documentation. Example: '2015-02-08T15:27:12.863Z'
|
1506
|
+
# NOTE: The server API implementation REQUIRES the fractional
|
1507
|
+
# seconds be EXACTLY 3 digits. No other syntax will work.
|
1508
|
+
# You will get 400/BadRequest if your format is incorrect.
|
1509
|
+
# In particular a direct strftime won't work; you will have
|
1510
|
+
# to manually massage it to conform to the above syntax.
|
1511
|
+
#
|
1452
1512
|
# @param [Boolean] dictionary
|
1453
1513
|
# If true the entire metric will be returned as a string-key Hash;
|
1454
1514
|
# else (false/default) the bare number (Fixnum or Float) for the
|
@@ -1457,7 +1517,7 @@ class NumerousMetric < NumerousClientInternals
|
|
1457
1517
|
# value of the metric is returned as a bare number.
|
1458
1518
|
# @return [Hash] if dictionary is true the entire new metric is returned.
|
1459
1519
|
#
|
1460
|
-
def write(newval, onlyIf:false, add:false, dictionary:false)
|
1520
|
+
def write(newval, onlyIf:false, add:false, dictionary:false, updated:nil)
|
1461
1521
|
j = { 'value' => newval }
|
1462
1522
|
if onlyIf
|
1463
1523
|
j['onlyIfChanged'] = true
|
@@ -1465,6 +1525,9 @@ class NumerousMetric < NumerousClientInternals
|
|
1465
1525
|
if add
|
1466
1526
|
j['action'] = 'ADD'
|
1467
1527
|
end
|
1528
|
+
if updated
|
1529
|
+
j['updated'] = updated
|
1530
|
+
end
|
1468
1531
|
|
1469
1532
|
@cachedHash = nil # will need to refresh cache on next access
|
1470
1533
|
api = getAPI(:events, :POST)
|
@@ -1475,7 +1538,7 @@ class NumerousMetric < NumerousClientInternals
|
|
1475
1538
|
# if onlyIf was specified and the error is "conflict"
|
1476
1539
|
# (meaning: no change), raise ConflictError specifically
|
1477
1540
|
if onlyIf and e.code == 409
|
1478
|
-
raise NumerousMetricConflictError.new("No Change",
|
1541
|
+
raise NumerousMetricConflictError.new("No Change", e.details)
|
1479
1542
|
else
|
1480
1543
|
raise e # never mind, plain NumerousError is fine
|
1481
1544
|
end
|
@@ -1645,6 +1708,15 @@ class NumerousMetric < NumerousClientInternals
|
|
1645
1708
|
return v['links']['web']
|
1646
1709
|
end
|
1647
1710
|
|
1711
|
+
# the phone application generates a nmrs:// URL as a way to link to
|
1712
|
+
# the application view of a metric (vs a web view). This makes
|
1713
|
+
# one of those for you so you don't have to "know" the format of it.
|
1714
|
+
#
|
1715
|
+
# @return [String] nmrs:// style URL
|
1716
|
+
def appURL
|
1717
|
+
return "nmrs://metric/" + @id
|
1718
|
+
end
|
1719
|
+
|
1648
1720
|
# Delete a metric (permanently). Be 100% you want this, because there
|
1649
1721
|
# is absolutely no undo.
|
1650
1722
|
#
|
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: 1.0
|
4
|
+
version: 1.1.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: 2015-02-
|
11
|
+
date: 2015-02-22 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
|