Objects#

Heedy Objects represent Heedy’s core functionality. By default, Heedy supports objects of the timeseries type, but other types can be implemented by Heedy plugins, and can have corresponding Python APIs registered with the Python client.

This page will describe how to use objects from the Python API, accessing them either from an app or a plugin.

Listing & Accessing#

Objects can be accessed using the objects property in Apps, Plugins, or Users:

app = heedy.App("your_access_token","http://localhost:1324")
objs = app.objects() # list objects that belong to the app
print(objs)
app = heedy.App("your_access_token","http://localhost:1324")
myuser = app.owner # Gets the user that owns this app
objs = myuser.objects() # list all permitted objects belonging to the user
print(objs)
p = heedy.Plugin(session="sync")
objs = p.objects() # access objects of any user on the server
print(objs)
app = heedy.App("your_access_token","http://localhost:1324",session="async")
objs = await app.objects() # list all objects that belong to the app
print(objs)
app = heedy.App("your_access_token","http://localhost:1324",session="async")
myuser = await app.owner # Gets the user that owns this app
objs = await myuser.objects() # list all objects that belong to the user
print(objs)
p = heedy.Plugin()
objs = await p.objects() # list all objects on the server
print(objs)

The list returned from the objects function can be constrained, usually by object type, tags, or app key. The tags are space-separated, and will match all objects with their superset.

app.objects(tags="steps fitbit",type="timeseries")
await app.objects(tags="steps fitbit",type="timeseries")
[Timeseries{'access': 'read',
  'app': 'acf85b01-3c87-4813-9538-3c1120ecb2a3',
  'created_date': '2021-03-12',
  'description': '',
  'id': 'e1f4d4c2-d4f0-431a-bcaf-58c512fc7564',
  'key': 'steps',
  'meta': {'schema': {'type': 'number'}},
  'modified_date': '2021-09-27',
  'name': 'Steps',
  'owner': 'test',
  'owner_scope': 'read',
  'tags': 'fitbit steps charge5',
  'type': 'timeseries'}]

Creating & Deleting#

One can create new objects of a given type by calling the create method in objects:

obj = app.objects.create("My Timeseries",
                    type="timeseries",
                    meta={"schema":{"type":"number"}},
                    tags="myts mydata",
                    key="myts")
obj = await app.objects.create("My Timeseries",
                    type="timeseries",
                    meta={"schema":{"type":"number"}},
                    tags="myts mydata",
                    key="myts")

Only the first argument, the object name, is required. If not explicitly specified, the object type will be timeseries, and all other fields will be empty.

When creating an object for an app, it is useful to give it a key. Keys are unique per-app, meaning that the app can have only one object with the given key. The object then can be retrieved using:

obj = app.objects(key="myts")[0]
obj = (await app.objects(key="myts"))[0]

Finally, to delete an object and all of its data, one can use the delete method:

obj.delete()
await obj.delete()

Note

Plugins have administrative access to the database, but to create an object belonging to the app when logged in using an App token, the app must have self.objects:create scope. Similarly, deleting an object requires the self.objects:delete scope. Finally, editing the object’s properties requires the self.objects:update scope. Writing an object’s content/data requires the self.objects:write scope. To give an app full access to manage its own objects, you can give it self.objects super-scope.

Reading & Updating Properties#

All properties available on the object can be accessed directly as properties in Python in two ways:

# Reads the key property from the server
assert obj.key == "myts"
# Uses the previously read cached data, avoiding a server query
assert obj["key"] == "myts"
# Reads the key property from the server
assert (await obj.key) == "myts"
# Uses the previously read cached data, avoiding a server query
assert obj["key"] == "myts"

When trying to access cached properties for an object that was not yet read from the server, obj["key"] will return a KeyError. To refresh the cache, it is sufficient to run:

obj.read()
assert obj["key"]=="myts"
await obj.read()
assert obj["key"]=="myts"

The read function returns a dict containing the props. Any read or update for the object will also refresh the cache with new data.

To update properties, the update method can be used, or the props can directly be set if in a sync session:

# Both of these lines do the same thing.
obj.description = "My description"
obj.update(description="My description")
#
#
await obj.update(description="My description")

Updating Metadata#

Each object has a meta property, which is defined separately for every object type. For example, the timeseries object type defines meta to contain a JSON schema describing the data type of each datapoint in the series:

[Timeseries{...
  'meta': {'schema': {'type': 'number'}},
  ...
  'type': 'timeseries'}]

Since the meta property is a dict that can contain multiple sub-properties, it has special handling in the API. For example, updating an object to set its meta property will merge the new values with the old ones:

obj.meta() # Read meta from server ={"a": 1,"b":2,"c":3}
obj.meta = {"c": 4,"b": None} # Update the b,c values
print(obj.meta) # {"a": 1,"c":4} # The update results are cached
await obj.meta() # Read meta from server ={"a": 1,"b":2,"c":3}
await obj.update(meta={"c": 4,"b": None}) # Update the b,c values
print(obj.meta) # {"a": 1,"c":4} # The update results are cached

Setting a key in meta to None resets it to its default value, so deleting a timeseries schema will simply reset the schema to {} rather than removing the key.

Finally, for simplicity, the Python API supports direct modification of the meta object:

obj.meta.schema = {"type":"string"}
del obj.meta.b
await obj.meta.update(schema={"type":"string"})
await obj.meta.delete("b")

Timeseries Data#

The timeseries object type is built into heedy by default. A timeseries can be considered an array of datapoints of the form:

{
    "t": 1234556789.0, // unix timestamp in seconds,
    "d": 2, // data associated with the timestamp,
    "dt": 60, //optional duration of the datapoint in seconds, [t,t+dt)
}

One can optionally specify a JSON schema as the schema property of object metadata, which then constrains the data portion of future datapoints to the schema (see example in the metadata section above).

Reading Data#

Suppose we have an object of type timeseries. Just like in Python arrays, heedy timeseries can be accessed by index, with negative numbers allowing indexing from the end of the series. One can also directly query data ranges and series length:

len(obj)    # Number of datapoints in the series
obj[2:5]    # Query by index range
obj[-1]     # Get most recent datapoint
await obj.length()      # Number of datapoints in the series
await obj(i1=2,i2=5)    # Query by index range
await obj(i=-1)[0]      # Get most recent datapoint

The timeseries data can also be accessed by timestamp and time range. Time ranges can be of two types: relative and absolute. A relative time range uses a string that specifies a timestamp relative to the current time (s,m,h,d,w,mo,y):

obj(t1="now-1w") # Returns the past week of data
await obj(t1="now-1w") # Returns the past week of data

The timeseries can also directly be accessed using the unix timestamp (in seconds) as the time range specifier:

# Returns the data from 2 hours ago to 1 hour ago
obj(t1=time.time()-2*60*60,t2=time.time()-60*60)
# Returns the data from 2 hours ago to 1 hour ago
await obj(t1=time.time()-2*60*60,t2=time.time()-60*60)

PipeScript Transforms#

When querying data from a timeseries, you can also specify a server-side transform of the data, allowing aggregation and direct processing:

# Returns the sum of data for the past week
obj(t1="now-1w",transform="sum")
# Returns the sum of data for the past week
await obj(t1="now-1w",transform="sum")

Output DatapointArray#

When reading timeseries data, by default the result is returned as a subclass of list with a couple useful add-ons:

data = obj(t1="now-1w")

# Returns an array of just data portions of the result
data.d()

# Returns a pandas DataFrame of the timeseries data
data.to_df()

# Writes the data to a file
data.write("myfile.json")
data = await obj(t1="now-1w")

# Returns an array of just data portions of the result
data.d()

# Returns a pandas DataFrame of the timeseries data
data.to_df()

# Writes the data to a file
data.save("myfile.json")

Timeseries objects in Python can be configured to return pandas.DataFrames directly instead (as is done in the heedy notebook plugin), or per query:

from heedy import Timeseries
Timeseries.output_type="dataframe" # Return pandas.DataFrames by default

obj(i1=-10,output_type="list") # override the global configuration for this query
from heedy import Timeseries
Timeseries.output_type="dataframe" # Return pandas.DataFrame by default

await obj(i1=-10,output_type="list") # override the global configuration for this query

Writing Data#

If we have write access (write scope), we can append a new datapoint to the timeseries directly, or write an array of data at once:

# Add a datapoint 5 with current timestamp to the series
obj.append(5)
assert obj[-1]["d"]==5

# Insert the given array of data
obj.insert_array([{"d": 6, "t": time.time()},{"d": 7, "t": time.time(), "dt": 5.3}])
# Add a datapoint 5 with current timestamp to the series
await obj.append(5)
assert (await obj(i=-1))["d"]==5

# Insert the given array of data
await obj.insert_array([{"d": 6, "t": time.time()},{"d": 7, "t": time.time(), "dt": 5.3}])

Removing Data#

Removing data from a timeseries has identical semantics to querying data. In other words, it is sufficient to specify the range:

# Remove the last month of data
obj.remove(t1="now-1mo")

# Timeseries are indexed by timestamp, so a specific datapoint can be removed
# by calling remove with its timestamp
dp = obj[-1]
obj.remove(t=dp["t"])

# This is equivalent to the above:
obj.remove(i=-1)
# Remove the last month of data
await obj.remove(t1="now-1mo")

# Timeseries are indexed by timestamp, so a specific datapoint can be removed
# by calling remove with its timestamp
dp = await obj[-1]
await obj.remove(t=dp["t"])

# This is equivalent to the above:
await obj.remove(i=-1)

API#

Objects#

Object#

ObjectMeta#

class heedy.objects.objects.ObjectMeta(obj)[source]#

Bases: object

Heedy’s objects have a metadata field which stores object type-specific information. This meta property of an object is a key-value dictionary, and can be edited by altering the meta field in sync sessions, or by calling the update method.

# A timeseries type object has a schema key in its metadata. Setting "schema" here
# does not alter any other elements of :code:`meta`, but only the "schema" key.
o.meta = {"schema": {"type": "string"}}
# A timeseries type object has a schema key in its metadata. Setting "schema" here
# does not alter any other elements of :code:`meta`, but only the "schema" key.
await o.update(meta={"schema": {"type": "string"}})

The meta property behaves like a dictionary, but has features that help in usage. For example, the above code can be written as:

o.meta.schema = {"type": "string"}
await o.meta.update(schema={"type": "string"})

The meta property also has several properties that offer syntactic sugar in synchronous code:

del o.meta.schema # Resets the schema to its default value
del o.meta["schema"] # Same as above
len(o.meta) # Returns the number of keys currently cached in the object metadata
"schema" in o.meta # Returns True if the schema key is in the cached meta field
await o.meta.delete("schema") # Resets the schema to its default value

len(o.meta) # Returns the number of keys currently cached in the object metadata
"schema" in o.meta # Returns True if the schema key is in the cached meta field

Finally, the semantics of o.meta.schema and o.meta["schema"] are the same as for standard objects, meaning that o.meta["schema"] does not query the server for the schema, but instead returns the cached values, while o.meta.schema will always query the server, and needs to be awaited in async sessions.

delete(*args)[source]#

Delete the given keys from the object metadata.

Deleting a key resets the value of that property to its default. Removes the key from metadata if it is optional.

o.meta.delete("schema")
assert o.meta["schema"] == {}
await o.meta.delete("schema")
assert o.meta["schema"] == {}
Parameters

*args (str) – The keys to delete

Returns

The updated object metadata

Raises

HeedyException – If writing fails (usually due to insufficient permissions)

update(**kwargs)[source]#

Sets the given keys in the object’s type metadata.

o.meta.update(schema={"type": "string"})
await o.meta.update(schema={"type": "string"})
Parameters

**kwargs – The keys to set and their values

Returns

The updated object metadata (as a dictionary)

Raises

HeedyException – If writing fails (usually due to insufficient permissions)

Timeseries#

DatapointArray#

Registering New Types#

heedy.objects.registry.registerObjectType(objectType, objectClass)[source]#

If implementing a plugin which creates a new object type in Heedy, it might be useful to add support for the object type in the heedy python client. This is done by creating a subclass of heedy.objects.Object, and then registering it:

from heedy.objects import Object,registerObjectType

class MyType(Object):
    def myfunction(self):
        # Returns the result of a REST API call for the object
        return self.session.get(self.uri + "/mytype/my_rest_endpoint")

registerObjectType("mytype",MyType)

Then, when reading objects, the object type is automatically detected and the correct class is used:

mtobjs = app.objects(type="mytype")

for mtobj in mtobjs:
    print(mtobj.myfunction())
mtobjs = await app.objects(type="mytype")

for mtobj in mtobjs:
    print(await mtobj.myfunction())