numerousapp 1.0.2 → 1.1.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 +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
|