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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/numerousapp.rb +111 -39
  3. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5bc753ba4ac5af22bec9d355a0450327a4c0415c
4
- data.tar.gz: e5e47a6f1f2a339338bdb5fe60a66366f33fa610
3
+ metadata.gz: 2df08c0abb56a4d38ce98c048fb814a37cb4ca37
4
+ data.tar.gz: 50b9313c710e3ab6c27a32cd895c10b03671bdfb
5
5
  SHA512:
6
- metadata.gz: aba6414701c7c0ddf0e33379f3dc35a3b48070587e535be8e57ceea8247f564cc2a8a9429ac67c32dd0a028faf2ee313d548c41f18a1dbb3943ff17e9df775d1
7
- data.tar.gz: bfbae46781b680556c3cec93a0f380d7a765443c54b9d672a6d0ca54bf4c71b4ca6bce24241a431e0b626a1f2522a31f49f0ef8f9aaf0136f853d65df149cdf1
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) 2014 Neil Webber
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 exceptions indicate errors from the server
42
+ # == NumerousError
43
+ #
44
+ # Exceptions indicate errors from the server
43
45
  #
44
46
  class NumerousError < StandardError
47
+
45
48
  #
46
- # @!attribute msg
47
- # human-targeted error message as a string
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" (40) as the voluntary backoff point
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 } # just useful debug/testing info
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
- # XXX This is primarily for testing; control filtering of bogus duplicates
176
- # If you are calling this you are probably doing something wrong.
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 = '20150215-1.0.2'
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
- b = "RoLlErCaSeDbOuNdArY8675309".b
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
- resp = @http.request(rq)
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], :url => path } }
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 the server failed with too busy, do backoff based on attempt number
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. We assume that a 429 ("Too Many") or
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
- raiseConflict(bestMatch[0]['label'], m['label'])
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
- raiseConflict(bestMatch[0]['label'], m['label'])
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
- # no entries needed for GET/PUT because no special codes etc
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", 0, e.details)
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.2
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-15 00:00:00.000000000 Z
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