numerousapp 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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