Class: NumerousMetric

Inherits:
NumerousClientInternals show all
Defined in:
lib/numerousapp.rb

Overview

NumerousMetric

Class for individual Numerous metrics

You instantiate these hanging off of a particular Numerous connection:

nr = Numerous.new('nmrs_3xblahblah')
m = nr.metric('754623094815712984')

For most operations the NumerousApp server returns a JSON representation of the current or modified object state. This is converted to a ruby Hash of <string-key, value> pairs and returned from the appropriate methods. A few of the methods return only one item from the Hash (e.g., read will return just the naked number unless you ask it for the entire dictionary)

For some operations the server returns only a success/failure code. In those cases there is no useful return value from the method; the method succeeds or else raises an exception (containing the failure code).

For the collection operations the server returns a JSON array of dictionary representations, possibly “chunked” into multiple request/response operations. The enumerator methods (e.g., “events”) implement lazy-fetch and hide the details of the chunking from you. They simply yield each individual item (string-key Hash) to your block.

Instance Attribute Summary collapse

Attributes inherited from NumerousClientInternals

#agentString, #debugLevel, #serverName, #statistics

Instance Method Summary collapse

Methods inherited from NumerousClientInternals

#debug, #setBogusDupFilter

Constructor Details

#initialize(id, nr = nil) ⇒ NumerousMetric

Constructor for a NumerousMetric

“id” should normally be the naked metric id (as a string).

It can also be a nmrs: URL, e.g.:

nmrs://metric/2733614827342384

Or a ‘self’ link from the API:

https://api.numerousapp.com/metrics/2733614827342384

in either case we get the ID in the obvious syntactic way.

It can also be a metric’s web link, e.g.:

http://n.numerousapp.com/m/1x8ba7fjg72d

in which case we “just know” that the tail is a base36 encoding of the ID.

The decoding logic here makes the specific assumption that the presence of a ‘/’ indicates a non-naked metric ID. This seems a reasonable assumption given that IDs have to go into URLs

“id” can be a hash representing a metric or a subscription. We will take (in order) key ‘metricId’ or key ‘id’ as the id. This is convenient when using the metrics() or subscriptions() iterators.

“id” can be an integer representing a metric ID. Not recommended though it’s handy sometimes in cut/paste interactive testing/use.

Parameters:

  • id (String)

    The metric ID string.

  • nr (Numerous) (defaults to: nil)

    The Numerous object that will be used to access this metric.



1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
# File 'lib/numerousapp.rb', line 1111

def initialize(id, nr=nil)

    # If you don't specify a Numerous we'll make one for you.
    # For this to work, NUMEROUSAPIKEY environment variable must exist.
    #   m = NumerousMetric.new('234234234') is ok for simple one-shots
    # but note it makes a private Numerous for every metric.

    nr ||= Numerous.new(nil)

    actualId = nil
    begin
        fields = id.split('/')
        if fields.length() == 1
            actualId = fields[0]
        elsif fields[-2] == "m"
            actualId = fields[-1].to_i(36)
        else
            actualId = fields[-1]
        end
    rescue NoMethodError
    end

    if not actualId
        # it's not a string, see if it's a hash
         actualId = id['metricId'] || id['id']
    end

    if not actualId
        # well, see if it looks like an int
        i = id.to_i     # allow this to raise exception if id bogus type here
        if i == id
            actualId = i.to_s
        end
    end

    if not actualId
        raise ArgumentError("invalid id")
    else
        @id = actualId.to_s    # defensive in case bad fmt in hash
        @nr = nr
        @cachedHash = nil
    end
end

Instance Attribute Details

#idString (readonly)

Returns The metric ID string.

Returns:

  • (String)

    The metric ID string.



# File 'lib/numerousapp.rb', line 1072

#nrObject (readonly)

Returns the value of attribute nr.



1155
1156
1157
# File 'lib/numerousapp.rb', line 1155

def nr
  @nr
end

Instance Method Details

#[](idx) ⇒ Object

access cached copy of metric via [ ]



1280
1281
1282
1283
# File 'lib/numerousapp.rb', line 1280

def [](idx)
    ensureCache()
    return @cachedHash[idx]
end

#appURLString

the phone application generates a nmrs:// URL as a way to link to the application view of a metric (vs a web view). This makes one of those for you so you don’t have to “know” the format of it.

Returns:

  • (String)

    nmrs:// style URL



1716
1717
1718
# File 'lib/numerousapp.rb', line 1716

def appURL
    return "nmrs://metric/" + @id
end

#comment(ctext) ⇒ String

Comment on a metric

Parameters:

  • ctext (String)

    The comment text to write.

Returns:

  • (String)

    The ID of the resulting interaction (the “comment”)



1610
1611
1612
1613
# File 'lib/numerousapp.rb', line 1610

def comment(ctext)
    j = { 'kind' => 'comment' , 'commentBody' => ctext }
    return writeInteraction(j)
end

#crushKillDestroynil

Delete a metric (permanently). Be 100% you want this, because there is absolutely no undo.

Returns:

  • (nil)


1724
1725
1726
1727
1728
1729
# File 'lib/numerousapp.rb', line 1724

def crushKillDestroy
    @cachedHash = nil
    api = getAPI(:metric, :DELETE)
    v = @nr.simpleAPI(api)
    return nil
end

#eachObject

enumerator metric.each { |k, v| … }



1286
1287
1288
1289
# File 'lib/numerousapp.rb', line 1286

def each()
    ensureCache()
    @cachedHash.each { |k, v| yield(k, v) }
end

#event(eId) ⇒ Hash

Obtain a specific metric event from the server

Parameters:

  • eId (String)

    The specific event ID

Returns:

  • (Hash)

    The string-key hash of the event

Raises:



1430
1431
1432
1433
# File 'lib/numerousapp.rb', line 1430

def event(eId)
    api = getAPI(:event, :GET, {eventID:eId})
    return @nr.simpleAPI(api)
end

#eventDelete(evID) ⇒ nil

Note:

Deleting an event that isn’t there will raise a NumerousError but the error code will be 200/OK.

Delete an event (a value update)

Parameters:

  • evID (String)

    ID (string) of the event to be deleted.

Returns:

  • (nil)


1648
1649
1650
1651
1652
# File 'lib/numerousapp.rb', line 1648

def eventDelete(evID)
    api = getAPI(:event, :DELETE, {eventID:evID})
    v = @nr.simpleAPI(api)
    return nil
end

#events {|e| ... } ⇒ NumerousMetric

Enumerate the events of a metric. Events are value updates.

Yields:

  • (e)

    events

Yield Parameters:

  • e (Hash)

    String-key representation of one metric.

Returns:



1387
1388
1389
1390
# File 'lib/numerousapp.rb', line 1387

def events(&block)
    @nr.chunkedIterator(APIInfo[:events], {metricId:@id}, block)
    return self
end

#interaction(iId) ⇒ Hash

Obtain a specific metric interaction from the server

Parameters:

  • iId (String)

    The specific interaction ID

Returns:

  • (Hash)

    The string-key hash of the interaction

Raises:



1441
1442
1443
1444
# File 'lib/numerousapp.rb', line 1441

def interaction(iId)
    api = getAPI(:interaction, :GET, {item:iId})
    return @nr.simpleAPI(api)
end

#interactionDelete(interID) ⇒ nil

Note:

Deleting an interaction that isn’t there will raise a NumerousError but the error code will be 200/OK.

Delete an interaction (a like/comment/error)

Parameters:

  • interID (String)

    ID (string) of the interaction to be deleted.

Returns:

  • (nil)


1659
1660
1661
1662
1663
# File 'lib/numerousapp.rb', line 1659

def interactionDelete(interID)
    api = getAPI(:interaction, :DELETE, {item:interID})
    v = @nr.simpleAPI(api)
    return nil
end

#interactions {|i| ... } ⇒ NumerousMetric

Enumerate the interactions (like/comment/error) of a metric.

Yields:

  • (i)

    interactions

Yield Parameters:

  • i (Hash)

    String-key representation of one interaction.

Returns:



1408
1409
1410
1411
# File 'lib/numerousapp.rb', line 1408

def interactions(&block)
    @nr.chunkedIterator(APIInfo[:interactions], {metricId:@id}, block)
    return self
end

#keysObject

produce the keys of a metric as an array



1292
1293
1294
1295
# File 'lib/numerousapp.rb', line 1292

def keys()
    ensureCache()
    return @cachedHash.keys
end

#labelString

Get the label of a metric.

Returns:

  • (String)

    The metric label.



1698
1699
1700
1701
# File 'lib/numerousapp.rb', line 1698

def label
    v = read(dictionary:true)
    return v['label']
end

#likeString

“Like” a metric

Returns:

  • (String)

    The ID of the resulting interaction (the “like”)



1586
1587
1588
1589
# File 'lib/numerousapp.rb', line 1586

def like
    # a like is written as an interaction
    return writeInteraction({ 'kind' => 'like' })
end

#photo(imageDataOrReadable, mimeType: 'image/jpeg') ⇒ Hash

Note:

the server enforces an undocumented maximum data size. Exceeding the limit will raise a NumerousError (HTTP 413 / Too Large)

set the background image for a metric

Parameters:

  • imageDataOrReadable (String, #read)

    Either a binary-data string of the image data or an object with a “read” method. The entire data stream will be read.

  • mimeType (String) (defaults to: 'image/jpeg')

    Optional(keyword arg). Mime type.

Returns:

  • (Hash)

    updated metric representation (string-key hash)



1625
1626
1627
1628
1629
1630
# File 'lib/numerousapp.rb', line 1625

def photo(imageDataOrReadable, mimeType:'image/jpeg')
    api = getAPI(:photo, :POST)
    mpart = { :f => imageDataOrReadable, :mimeType => mimeType }
    @cachedHash = @nr.simpleAPI(api, multipart: mpart)
    return @cachedHash.clone()
end

#photoDeletenil

Note:

Deleting a photo that isn’t there will raise a NumerousError but the error code will be 200/OK.

Delete the metric’s photo

Returns:

  • (nil)


1636
1637
1638
1639
1640
1641
# File 'lib/numerousapp.rb', line 1636

def photoDelete
    @cachedHash = nil   # I suppose we could have just deleted the photoURL
    api = getAPI(:photo, :DELETE)
    v = @nr.simpleAPI(api)
    return nil
end

#photoURLString?

Note:

Fetches (and discards) the entire underlying photo, because that was the easiest way to find the target URL using net/http

Obtain the underlying photoURL for a metric.

The photoURL is available in the metrics parameters so you could just read(dictionary:true) and obtain it that way. However this goes one step further … the URL in the metric itself still requires authentication to fetch (it then redirects to the “real” underlying static photo URL). This function goes one level deeper and returns you an actual, publicly-fetchable, photo URL.

Returns:

  • (String, nil)

    URL. If there is no photo returns nil.



1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
# File 'lib/numerousapp.rb', line 1679

def photoURL
    v = read(dictionary:true)
    begin
        phurl = v.fetch('photoURL')
        return @nr.getRedirect(phurl)
    rescue KeyError
        return nil
    end
    # never reached
    return nil
end

#read(dictionary: false) ⇒ Fixnum|Float, Hash

Read the current value of a metric

Parameters:

  • dictionary (Boolean) (defaults to: false)

    If true the entire metric will be returned as a string-key Hash; else (false/default) a bare number (Fixnum or Float) is returned.

Returns:

  • (Fixnum|Float)

    if dictionary is false (or defaulted).

  • (Hash)

    if dictionary is true.



1331
1332
1333
1334
1335
1336
# File 'lib/numerousapp.rb', line 1331

def read(dictionary: false)
    api = getAPI(:metric, :GET)
    v = @nr.simpleAPI(api)
    @cachedHash = v.clone
    return (if dictionary then v else v['value'] end)
end

#sendError(errText) ⇒ String

Write an error to a metric

Parameters:

  • errText (String)

    The error text to write.

Returns:

  • (String)

    The ID of the resulting interaction (the “error”)



1597
1598
1599
1600
1601
1602
# File 'lib/numerousapp.rb', line 1597

def sendError(errText)
    # an error is written as an interaction thusly:
    # (commentBody is used for the error text)
    j = { 'kind' => 'error' , 'commentBody' => errText }
    return writeInteraction(j)
end

#stream {|s| ... } ⇒ NumerousMetric

Enumerate the stream of a metric. The stream is events and interactions merged together into a time-ordered stream.

Yields:

  • (s)

    stream

Yield Parameters:

  • s (Hash)

    String-key representation of one stream item.

Returns:



1398
1399
1400
1401
# File 'lib/numerousapp.rb', line 1398

def stream(&block)
    @nr.chunkedIterator(APIInfo[:stream], {metricId:@id}, block)
    return self
end

#subscribe(dict, userId: nil, overwriteAll: false) ⇒ Object

Subscribe to a metric.

See the NumerousApp API docs for what should be in the dict. This function will fetch the current parameters and update them with the ones you supply (because the server does not like you supplying an incomplete dictionary here). You can prevent the fetch/merge via overwriteAll:true

Normal users cannot set other user’s subscriptions.

Parameters:

  • dict (Hash)

    string-key hash of subscription parameters

  • userId (String) (defaults to: nil)

    Optional (keyword arg). UserId to subscribe.

  • overwriteAll (Boolean) (defaults to: false)

    Optional (keyword arg). If true, dict is sent without reading the current parameters and merging them.



1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
# File 'lib/numerousapp.rb', line 1474

def subscribe(dict, userId:nil, overwriteAll:false)
    if overwriteAll
        params = {}
    else
        params = subscription(userId)
    end

    dict.each { |k, v| params[k] = v }
    @cachedHash = nil     # bcs the subscriptions count changes
    api = getAPI(:subscription, :PUT, { userId: userId })
    return @nr.simpleAPI(api, jdict:params)
end

#subscription(userId = nil) ⇒ Hash

Obtain your subscription parameters on a given metric

Note that normal users cannot see other user’s subscriptions. Thus the “userId” parameter is somewhat pointless; you can only ever see your own.

Parameters:

  • userId (String) (defaults to: nil)

Returns:

  • (Hash)

    your subscription attributes



1453
1454
1455
1456
# File 'lib/numerousapp.rb', line 1453

def subscription(userId=nil)
    api = getAPI(:subscription, :GET, {userId: userId})
    return @nr.simpleAPI(api)
end

#subscriptions {|s| ... } ⇒ NumerousMetric

Enumerate the subscriptions of a metric.

Yields:

  • (s)

    subscriptions

Yield Parameters:

  • s (Hash)

    String-key representation of one subscription.

Returns:



1418
1419
1420
1421
# File 'lib/numerousapp.rb', line 1418

def subscriptions(&block)
    @nr.chunkedIterator(APIInfo[:subscriptions], {metricId:@id}, block)
    return self
end

#to_sObject

string representation of a metric



1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
# File 'lib/numerousapp.rb', line 1298

def to_s()
   # there's nothing important/magic about the object id displayed; however
   # to make it match the native to_s we (believe it or not) need to multiply
   # the object_id return value by 2. This is obviously implementation specific
   # (and makes no difference to anyone; but this way it "looks right" to humans)
   objid = (2*self.object_id).to_s(16)   # XXX wow lol
   rslt = "<NumerousMetric @ 0x#{objid}: "
   begin
       ensureCache()
       lbl = self['label']
       val = self['value']
       rslt += "'#{self['label']}' [#@id] = #{val}>"
   rescue NumerousError => x
       puts(x.code)
       if x.code == 400
           rslt += "**INVALID-ID** '#@id'>"
       elsif x.code == 404
           rslt += "**ID NOT FOUND** '#@id'>"
       else
           rslt += "**SERVER-ERROR** '#{x.message}'>"
       end
   end
   return rslt
end

#update(dict, overwriteAll: false) ⇒ Hash

Update parameters of a metric (such as “description”, “label”, etc). Not to be used (won’t work) to update a metric’s value.

Parameters:

  • dict (Hash)

    string-key Hash of the parameters to be updated.

  • overwriteAll (Boolean) (defaults to: false)

    Optional (keyword arg). If false (default), this method will first read the current metric parameters from the server and merge them with your updates before writing them back. If true your supplied dictionary will become the entirety of the metric’s parameters, and any parameters you did not include in your dictionary will revert to their default values.

Returns:

  • (Hash)

    string-key Hash of the new metric parameters.



1564
1565
1566
1567
1568
1569
1570
1571
# File 'lib/numerousapp.rb', line 1564

def update(dict, overwriteAll:false)
    newParams = (if overwriteAll then {} else read(dictionary:true) end)
    dict.each { |k, v| newParams[k] = v }

    api = getAPI(:metric, :PUT)
    @cachedHash = @nr.simpleAPI(api, jdict:newParams)
    return @cachedHash.clone
end

#validateBoolean

“Validate” a metric object. There really is no way to do this in any way that carries much weight. However, if a user gives you a metricId and you’d like to know if that actually IS a metricId, this might be useful.

Realize that even a valid metric can be deleted asynchronously and thus become invalid after being validated by this method.

Reads the metric, catches the specific exceptions that occur for invalid metric IDs, and returns True/False. Other exceptions mean something else went awry (server down, bad authentication, etc).

Examples:

someId = ... get a metric ID from someone ...
m = nr.metric(someId)
if not m.validate
    puts "#{someId} is not a valid metric"
end

Returns:

  • (Boolean)

    validity of the metric



1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
# File 'lib/numerousapp.rb', line 1357

def validate
    begin
        ignored = read()
        return true
    rescue NumerousError => e
        # bad request (400) is a completely bogus metric ID whereas
        # not found (404) is a well-formed ID that simply does not exist
        if e.code == 400 or e.code == 404
            return false
        else        # anything else is a "real" error; figure out yourself
            raise e
        end
    end
    return false # this never happens
end

#webURLString

Get the URL for the metric’s web representation

Returns:

  • (String)

    URL.



1706
1707
1708
1709
# File 'lib/numerousapp.rb', line 1706

def webURL
    v = read(dictionary:true)
    return v['links']['web']
end

#write(newval, onlyIf: false, add: false, dictionary: false, updated: nil) ⇒ Fixnum|Float, Hash

Write a value to a metric.

Parameters:

  • newval (Fixnum|Float)

    Required. Value to be written.

  • onlyIf (Boolean) (defaults to: false)

    Optional (keyword arg). Only creates an event at the server if the newval is different from the current value. Raises NumerousMetricConflictError if there is no change in value.

  • add (Boolean) (defaults to: false)

    Optional (keyword arg). Sends the “action: ADD” attribute which causes the server to ADD newval to the current metric value. Note that this IS atomic at the server. Two clients doing simultaneous ADD operations will get the correct (serialized) result.

  • updated (String) (defaults to: nil)

    updated allows you to specify the timestamp associated with the value

    -- it must be a string in the format described in the NumerousAPI
       documentation. Example: '2015-02-08T15:27:12.863Z'
       NOTE: The server API implementation REQUIRES the fractional
             seconds be EXACTLY 3 digits. No other syntax will work.
             You will get 400/BadRequest if your format is incorrect.
             In particular a direct strftime won't work; you will have
             to manually massage it to conform to the above syntax.
    
  • dictionary (Boolean) (defaults to: false)

    If true the entire metric will be returned as a string-key Hash; else (false/default) the bare number (Fixnum or Float) for the resulting new value is returned.

Returns:

  • (Fixnum|Float)

    if dictionary is false (or defaulted). The new value of the metric is returned as a bare number.

  • (Hash)

    if dictionary is true the entire new metric is returned.



1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
# File 'lib/numerousapp.rb', line 1520

def write(newval, onlyIf:false, add:false, dictionary:false, updated:nil)
    j = { 'value' => newval }
    if onlyIf
        j['onlyIfChanged'] = true
    end
    if add
        j['action'] = 'ADD'
    end
    if updated
        j['updated'] = updated
    end

    @cachedHash = nil  # will need to refresh cache on next access
    api = getAPI(:events, :POST)
    begin
        v = @nr.simpleAPI(api, jdict:j)

    rescue NumerousError => e
        # if onlyIf was specified and the error is "conflict"
        # (meaning: no change), raise ConflictError specifically
        if onlyIf and e.code == 409
            raise NumerousMetricConflictError.new("No Change", e.details)
        else
            raise e        # never mind, plain NumerousError is fine
        end
    end

    return (if dictionary then v else v['value'] end)
end