class NumerousMetric

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.

Constants

APIInfo

Attributes

id[R]
nr[R]

Public Class Methods

new(id, nr=nil) click to toggle source

Constructor for a NumerousMetric

@param [String] id The metric ID string. @param [Numerous] nr

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

“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

or the “embed” (/e/ vs /m/) variant, 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.

# File lib/numerousapp.rb, line 1194
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 [ "m", "e" ].include? fields[-2]
            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     # raises 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

Public Instance Methods

[](idx) click to toggle source

access cached copy of metric via [ ]

# File lib/numerousapp.rb, line 1385
def [](idx)
    ensureCache()
    return @cachedHash[idx]
end
appURL() click to toggle source

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.

@return [String] nmrs:// style URL

# File lib/numerousapp.rb, line 1915
def appURL
    return "nmrs://metric/" + @id
end
comment(ctext) click to toggle source

Comment on a metric

@param [String] ctext The comment text to write. @return [String] The ID of the resulting interaction (the “comment”)

# File lib/numerousapp.rb, line 1809
def comment(ctext)
    j = { 'kind' => 'comment' , 'commentBody' => ctext }
    return writeInteraction(j)
end
crushKillDestroy() click to toggle source

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

@return [nil]

# File lib/numerousapp.rb, line 1923
def crushKillDestroy
    @cachedHash = nil
    api = getAPI(:metric, :DELETE)
    v = @nr.simpleAPI(api)
    return nil
end
delete_permission(userId) click to toggle source

Delete a permission resource for the given user

@param [String] userId The specific user ID

# File lib/numerousapp.rb, line 1625
def delete_permission(userId)
    api = getAPI(:permission, :DELETE, {userId: userId})
    ignored = @nr.simpleAPI(api)
    return nil
end
each() { |k, v| ... } click to toggle source

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

# File lib/numerousapp.rb, line 1391
def each()
    ensureCache()
    @cachedHash.each { |k, v| yield(k, v) }
end
event(eId=nil, before:nil) click to toggle source

Obtain a specific metric event from the server

@param [String] eId The specific event ID @param [String] before

Timestamp. Do not specify eId with this. You can also
also provide this as a strftime-able date/time object.

@return [Hash] The string-key hash of the event @raise [NumerousError] Not found (.code will be 404)

# File lib/numerousapp.rb, line 1547
def event(eId=nil, before:nil)
    if eId and before
        raise ArgumentError
    elsif eId
        api = getAPI(:event, :GET, {eventID:eId})
    else         # the "before" variant
        # if you gave us a formattable time try converting it
        begin
            ts = before.strftime('%Y-%m-%dT%H:%M:%S.')
            # note: we truncate, rather than round, the microseconds
            # for simplicity (in case usec is 999900 for example).
            begin
                ts += ("%03dZ" % (before.usec/1000))
            rescue NoMethodError   # in case no usec method (?)
                ts += '000Z'
            end
        rescue NoMethodError      # just take your argument
            ts = before           # which should be a string already
        end
        api = getAPI(:events, :at, {timestr:ts})
    end

    return @nr.simpleAPI(api)
end
eventDelete(evID) click to toggle source

Delete an event (a value update) @note Deleting an event that isn’t there will raise a NumerousError

but the error code will be 200/OK.

@param [String] evID ID (string) of the event to be deleted. @return [nil]

# File lib/numerousapp.rb, line 1847
def eventDelete(evID)
    api = getAPI(:event, :DELETE, {eventID:evID})
    v = @nr.simpleAPI(api)
    return nil
end
events(&block) click to toggle source

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

@yield [e] events @yieldparam e [Hash] String-key representation of one metric. @return [NumerousMetric] self

# File lib/numerousapp.rb, line 1491
def events(&block)
    @nr.chunkedIterator(APIInfo[:events], {metricId:@id}, block)
    return self
end
get_permission(userId=nil) click to toggle source

Obtain a specific permission resource for the given user

@param [String] userId The specific user ID @return [Hash] The string-key hash of the permission @raise [NumerousError] Not found (.code will be 404)

# File lib/numerousapp.rb, line 1601
def get_permission(userId=nil)
    api = getAPI(:permission, :GET, {userId: userId})
    return @nr.simpleAPI(api)
end
interaction(iId) click to toggle source

Obtain a specific metric interaction from the server

@param [String] iId The specific interaction ID @return [Hash] The string-key hash of the interaction @raise [NumerousError] Not found (.code will be 404)

# File lib/numerousapp.rb, line 1578
def interaction(iId)
    api = getAPI(:interaction, :GET, {item:iId})
    return @nr.simpleAPI(api)
end
interactionDelete(interID) click to toggle source

Delete an interaction (a like/comment/error) @note Deleting an interaction that isn’t there will raise a NumerousError

but the error code will be 200/OK.

@param [String] interID ID (string) of the interaction to be deleted. @return [nil]

# File lib/numerousapp.rb, line 1858
def interactionDelete(interID)
    api = getAPI(:interaction, :DELETE, {item:interID})
    v = @nr.simpleAPI(api)
    return nil
end
interactions(&block) click to toggle source

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

@yield [i] interactions @yieldparam i [Hash] String-key representation of one interaction. @return [NumerousMetric] self

# File lib/numerousapp.rb, line 1512
def interactions(&block)
    @nr.chunkedIterator(APIInfo[:interactions], {metricId:@id}, block)
    return self
end
keys() click to toggle source

produce the keys of a metric as an array

# File lib/numerousapp.rb, line 1397
def keys()
    ensureCache()
    return @cachedHash.keys
end
label() click to toggle source

Get the label of a metric.

@return [String] The metric label.

# File lib/numerousapp.rb, line 1897
def label
    v = read(dictionary:true)
    return v['label']
end
like() click to toggle source

“Like” a metric

@return [String] The ID of the resulting interaction (the “like”)

# File lib/numerousapp.rb, line 1785
def like
    # a like is written as an interaction
    return writeInteraction({ 'kind' => 'like' })
end
permissions(&block) click to toggle source

Enumerate the permissions of a metric.

@yield [p] permissions @yieldparam p [Hash] String-key representation of one permission @return [NumerousMetric] self

# File lib/numerousapp.rb, line 1532
def permissions(&block)
    @nr.chunkedIterator(APIInfo[:permissionsCollection], {metricId:@id}, block)
    return self
end
photo(imageDataOrReadable, mimeType:'image/jpeg') click to toggle source

set the background image for a metric @note the server enforces an undocumented maximum data size.

Exceeding the limit will raise a NumerousError (HTTP 413 / Too Large)

@param [String,#read] imageDataOrReadable

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

@param [String] mimeType

Optional(keyword arg). Mime type.

@return [Hash] updated metric representation (string-key hash)

# File lib/numerousapp.rb, line 1824
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
photoDelete() click to toggle source

Delete the metric’s photo @note Deleting a photo that isn’t there will raise a NumerousError

but the error code will be 200/OK.

@return [nil]

# File lib/numerousapp.rb, line 1835
def photoDelete
    @cachedHash = nil   # I suppose we could have just deleted the photoURL
    api = getAPI(:photo, :DELETE)
    v = @nr.simpleAPI(api)
    return nil
end
photoURL() click to toggle source

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.

@note Fetches (and discards) the entire underlying photo,

because that was the easiest way to find the target URL using net/http

@return [String, nil] URL. If there is no photo returns nil.

# File lib/numerousapp.rb, line 1878
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) click to toggle source

Read the current value of a metric @param [Boolean] dictionary

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

@return [Fixnum|Float] if dictionary is false (or defaulted). @return [Hash] if dictionary is true.

# File lib/numerousapp.rb, line 1435
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) click to toggle source

Write an error to a metric

@param [String] errText The error text to write. @return [String] The ID of the resulting interaction (the “error”)

# File lib/numerousapp.rb, line 1796
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
set_permission(perms, userId=nil) click to toggle source

Set a permission for the given user @param [Hash] perms

string-key hash of subscription parameters

@param [String] userId

Optional (keyword arg). UserId (defaults to you)
# File lib/numerousapp.rb, line 1611
def set_permission(perms, userId=nil)
    # if you don't specify a userId but DO have a userId
    # in the perms, use that one
    if (not userId) and perms.key? 'userId'
        userId = perms['userId']
    end
    api = getAPI(:permission, :PUT, {userId: userId})
    return @nr.simpleAPI(api, jdict:perms)
end
stream(&block) click to toggle source

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

@yield [s] stream @yieldparam s [Hash] String-key representation of one stream item. @return [NumerousMetric] self

# File lib/numerousapp.rb, line 1502
def stream(&block)
    @nr.chunkedIterator(APIInfo[:stream], {metricId:@id}, block)
    return self
end
subscribe(dict, userId:nil, overwriteAll:false) click to toggle source

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. @param [Hash] dict

string-key hash of subscription parameters

@param [String] userId

Optional (keyword arg). UserId to subscribe.

@param [Boolean] overwriteAll

Optional (keyword arg). If true, dict is sent without reading
the current parameters and merging them.
# File lib/numerousapp.rb, line 1647
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) click to toggle source

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. @param [String] userId @return [Hash] your subscription attributes

# File lib/numerousapp.rb, line 1590
def subscription(userId=nil)
    api = getAPI(:subscription, :GET, {userId: userId})
    return @nr.simpleAPI(api)
end
subscriptions(&block) click to toggle source

Enumerate the subscriptions of a metric.

@yield [s] subscriptions @yieldparam s [Hash] String-key representation of one subscription. @return [NumerousMetric] self

# File lib/numerousapp.rb, line 1522
def subscriptions(&block)
    @nr.chunkedIterator(APIInfo[:subscriptions], {metricId:@id}, block)
    return self
end
to_s() click to toggle source

string representation of a metric

# File lib/numerousapp.rb, line 1403
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
       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) click to toggle source

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

@param [Hash] dict

string-key Hash of the parameters to be updated.

@param [Boolean] overwriteAll

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.

@return [Hash] string-key Hash of the new metric parameters.

# File lib/numerousapp.rb, line 1763
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
validate() click to toggle source

“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.

@example

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

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). @return [Boolean] validity of the metric

# File lib/numerousapp.rb, line 1461
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
webURL() click to toggle source

Get the URL for the metric’s web representation

@return [String] URL.

# File lib/numerousapp.rb, line 1905
def webURL
    v = read(dictionary:true)
    return v['links']['web']
end
write(newval, onlyIf:false, add:false, dictionary:false, updated:nil) click to toggle source

Write a value to a metric.

@param [Fixnum|Float] newval Required. Value to be written.

@param [Boolean|String] onlyIf

Optional (keyword arg). Default is false.
If this is true or the string 'IGNORE' then the server only creates
a metric event if the newval is different from the current value.
If onlyIf=true then this RaisesNumerousMetricConflictError if there
is no change in value. If onlyIf is 'IGNORE' then the "conflict" error
is silently ignored (probably the more common usage case).

@param [Boolean] add

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.

@param [String] updated

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.

@param [Boolean] dictionary

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.

@return [Fixnum|Float] if dictionary is false (or defaulted). The new

value of the metric is returned as a bare number.

@return [Hash] if dictionary is true the entire new metric is returned.

# File lib/numerousapp.rb, line 1696
def write(newval, onlyIf:false, add:false, dictionary:false, updated:nil)
    j = { 'value' => newval }
    if onlyIf != false
        if not [ true, 'IGNORE' ].include? onlyIf
            # onlyIf must be false, true, or "IGNORE"
            raise ArgumentError, 'onlyIf must be false, true, or "IGNORE"'
        end
        j['onlyIfChanged'] = true
    end
    if add
        j['action'] = 'ADD'
    end
    if updated
        # if you gave us a formattable time try converting it
        begin
            ts = updated.strftime('%Y-%m-%dT%H:%M:%S.')
            # note: we truncate, rather than round, the microseconds
            # for simplicity (in case usec is 999900 for example).
            begin
                ts += ("%03dZ" % (updated.usec/1000))
            rescue NoMethodError   # in case no usec method (?)
                ts += '000Z'
            end
        rescue NoMethodError      # just take your argument
            ts = updated          # which should be a string already
        end
        j['updated'] = ts
    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
        # or ignore it if you specified onlyIf="IGNORE"
        if onlyIf and e.code == 409
            if onlyIf != 'IGNORE'
                raise NumerousMetricConflictError.new("No Change", e.details)
            else
                # forge a pseudo-result because you asked for it
                v = { 'value'=>newval, 'unchanged'=>true }
            end
        else
            raise e        # never mind, plain NumerousError is fine
        end
    end

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

Private Instance Methods

ensureCache() click to toggle source

for things, such as [ ], that need a cached copy of the metric’s values

# File lib/numerousapp.rb, line 1368
def ensureCache()
    begin
        if not @cachedHash
            ignored = read()    # just reading brings cache into being
        end
    rescue NumerousError => x
        raise x             # definitely pass these along
    rescue => x             # anything else turn into a NumerousError
        # this is usually going to be all sorts of random low-level
        # network problems (if the network fails at the exact wrong time)
        details = { exception: x }
        raise NumerousError.new("Could not obtain metric state", 0, details)
    end
end
getAPI(a, mx, args={}) click to toggle source

small wrapper to always supply the metricId substitution

# File lib/numerousapp.rb, line 1362
def getAPI(a, mx, args={})
    return @nr.makeAPIcontext(APIInfo[a], mx, args.merge({ metricId: @id }))
end
writeInteraction(dict) click to toggle source

common code for writing interactions

# File lib/numerousapp.rb, line 1773
def writeInteraction(dict)
    api = getAPI(:interactions, :POST)
    v = @nr.simpleAPI(api, jdict:dict)
    return v['id']
end