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

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.

Parameters:

  • id (String)

    The metric ID string.

  • nr (Numerous) (defaults to: nil)

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



1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
# 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

Instance Attribute Details

#idString (readonly)

Returns The metric ID string.

Returns:

  • (String)

    The metric ID string.



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

#nrObject (readonly)

Returns the value of attribute nr.



1238
1239
1240
# File 'lib/numerousapp.rb', line 1238

def nr
  @nr
end

Instance Method Details

#[](idx) ⇒ Object

access cached copy of metric via [ ]



1385
1386
1387
1388
# File 'lib/numerousapp.rb', line 1385

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



1915
1916
1917
# File 'lib/numerousapp.rb', line 1915

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”)



1809
1810
1811
1812
# File 'lib/numerousapp.rb', line 1809

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)


1923
1924
1925
1926
1927
1928
# File 'lib/numerousapp.rb', line 1923

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

#delete_permission(userId) ⇒ Object

Delete a permission resource for the given user

Parameters:

  • userId (String)

    The specific user ID



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

def delete_permission(userId)
    api = getAPI(:permission, :DELETE, {userId: userId})
    ignored = @nr.simpleAPI(api)
    return nil
end

#eachObject

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



1391
1392
1393
1394
# File 'lib/numerousapp.rb', line 1391

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

#event(eId = nil, before: nil) ⇒ Hash

Obtain a specific metric event from the server

Parameters:

  • eId (String) (defaults to: nil)

    The specific event ID

  • before (String) (defaults to: nil)

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

Returns:

  • (Hash)

    The string-key hash of the event

Raises:



1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
# 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) ⇒ 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)


1847
1848
1849
1850
1851
# File 'lib/numerousapp.rb', line 1847

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:



1491
1492
1493
1494
# File 'lib/numerousapp.rb', line 1491

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

#get_permission(userId = nil) ⇒ Hash

Obtain a specific permission resource for the given user

Parameters:

  • userId (String) (defaults to: nil)

    The specific user ID

Returns:

  • (Hash)

    The string-key hash of the permission

Raises:



1601
1602
1603
1604
# File 'lib/numerousapp.rb', line 1601

def get_permission(userId=nil)
    api = getAPI(:permission, :GET, {userId: userId})
    return @nr.simpleAPI(api)
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:



1578
1579
1580
1581
# File 'lib/numerousapp.rb', line 1578

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)


1858
1859
1860
1861
1862
# File 'lib/numerousapp.rb', line 1858

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:



1512
1513
1514
1515
# File 'lib/numerousapp.rb', line 1512

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

#keysObject

produce the keys of a metric as an array



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

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

#labelString

Get the label of a metric.

Returns:

  • (String)

    The metric label.



1897
1898
1899
1900
# File 'lib/numerousapp.rb', line 1897

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

#likeString

“Like” a metric

Returns:

  • (String)

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



1785
1786
1787
1788
# File 'lib/numerousapp.rb', line 1785

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

#permissions {|p| ... } ⇒ NumerousMetric

Enumerate the permissions of a metric.

Yields:

  • (p)

    permissions

Yield Parameters:

  • p (Hash)

    String-key representation of one permission

Returns:



1532
1533
1534
1535
# File 'lib/numerousapp.rb', line 1532

def permissions(&block)
    @nr.chunkedIterator(APIInfo[:permissionsCollection], {metricId:@id}, block)
    return self
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)



1824
1825
1826
1827
1828
1829
# 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

#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)


1835
1836
1837
1838
1839
1840
# 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

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



1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
# 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) ⇒ 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.



1435
1436
1437
1438
1439
1440
# 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) ⇒ 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”)



1796
1797
1798
1799
1800
1801
# 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) ⇒ Object

Set a permission for the given user

Parameters:

  • perms (Hash)

    string-key hash of subscription parameters

  • userId (String) (defaults to: nil)

    Optional (keyword arg). UserId (defaults to you)



1611
1612
1613
1614
1615
1616
1617
1618
1619
# 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 {|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:



1502
1503
1504
1505
# 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) ⇒ 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.



1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
# 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) ⇒ 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



1590
1591
1592
1593
# File 'lib/numerousapp.rb', line 1590

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:



1522
1523
1524
1525
# File 'lib/numerousapp.rb', line 1522

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

#to_sObject

string representation of a metric



1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
# 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) ⇒ 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.



1763
1764
1765
1766
1767
1768
1769
1770
# 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

#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



1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
# 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

#webURLString

Get the URL for the metric’s web representation

Returns:

  • (String)

    URL.



1905
1906
1907
1908
# 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) ⇒ Fixnum|Float, Hash

Write a value to a metric.

Parameters:

  • newval (Fixnum|Float)

    Required. Value to be written.

  • onlyIf (Boolean|String) (defaults to: false)

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

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



1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
# 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