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
Public Class Methods
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
access cached copy of metric via [ ]
# File lib/numerousapp.rb, line 1385 def [](idx) ensureCache() return @cachedHash[idx] end
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 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
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 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
enumerator metric.each { |k, v| … }
# File lib/numerousapp.rb, line 1391 def each() ensureCache() @cachedHash.each { |k, v| yield(k, v) } end
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
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
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
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
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
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
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
produce the keys of a metric as an array
# File lib/numerousapp.rb, line 1397 def keys() ensureCache() return @cachedHash.keys end
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” 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
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
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
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
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 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
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 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
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 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
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
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
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 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” 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
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 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
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
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
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