Operation basics¶
Pyhaystack’s API is written around the concept of finite state machines. At the base level, a simple state machine is used to retrieve the response from one of the Project Haystack server operations.
When one of the low-level functions is called, it takes the arguments, does a little processing then returns a state machine object. Depending on the implementation of the HTTP client being used, it either executes synchronously, and will be returned to you in a “finalised” state, or it may execute asynchronously in the background.
Operation state machine interface¶
The base class for all operations is the
pyhaystack.util.state.HaystackOperation
. The following methods
and properties are significant for client use:
pyhaystack.util.state.HaystackOperation.result
: The result of the state machine. If the result was an exception, the exception will be re-raised.pyhaystack.util.state.HaystackOperation.is_done
: Returns True if the operation is complete, False otherwise.pyhaystack.util.state.HaystackOperation.is_failed
: Returns True if the operation failed, False otherwise.pyhaystack.util.state.HaystackOperation.wait()
: This blocks the current thread until the operation completes (or if timeout is specified, until that number of seconds expires).pyhaystack.util.state.HaystackOperation.done_sig
: This is asignalslot.Signal
class that is “emitted” when the operation completes.
Synchronous usage¶
If you are using an asynchronous HTTP client running in a separate thread, you can optionally block your local thread either temporarily or indefinitely using the wait method.
When using the synchronous HTTP client, the wait is a no-op, since the state machine is returned to the caller in a resolved state. Thus, in synchronous code, it is recommended to do the following:
op = session.someoperation(arg1, arg2, arg3)
op.wait()
res = op.result
# do something with res
This ensures that the operation is complete prior to retrieving its result.
Operation states¶
The individual states of an operation depends on the type of state machine being inspected, however all have a final state that can be checked by inspecting the is_done property. An operation is “done” if:
- the operation succeeded, in which case see the result property to retrieve the return value.
- the operation failed, in which case reading result will re-raise the exception.
Signals¶
Pyhaystack uses the :py:module:`signalslot` module to provide a signal-based interface using the observer pattern. If you’ve ever worked with Qt, you’ll be familiar with how this works.
def _on_op_done(operation, **kwargs):
assert op.is_done # <- should not fire
# Operation is done, do something with result.
op = session.someoperation(arg1, arg2, arg3)
op.done_sig.connect(_on_op_done)
The signal object has a single method, signalslot.Signal.connect()
,
which takes a method or function as an argument. The passed-in method or
function needs to accept keyword arguments, and will receive a single
argument, operation, which will point to the instance of the
HaystackOperation that emitted it.
Asynchronous Exceptions¶
When using signals, the behaviour is undefined if your “slot” throws an
exception, thus you should catch exceptions in your slots and handle those
elsewhere. One helper class you can use for doing this is
pyhaystack.util.asyncexc.AsynchronousException
:
from pyhaystack.asyncexc import AsynchronousException
def async_func(callback):
try:
res = do_something()
except:
# Whoopsie!
res = AsynchronousException()
callback(res)
In the callback function, you can do something like this:
def callback_from_async_func(result):
try:
if isinstance(result, AsynchronousException):
result.reraise()
except:
# Handle your exception
If result is an exception, it’ll be re-raised, allowing you to handle it in your code.
Your first request¶
You defined a session, now you want to connect to the server. The first request you could make is called “about”.
About
The about op queries basic information about the server.
Request: empty grid
Response: single row grid with following columns:
- haystackVersion: Str version of REST implementation, must be “2.0”
- tz: Str of server’s default timezone
- serverName: Str name of the server or project database
- serverTime: current DateTime of server’s clock
- serverBootTime: DateTime when server was booted up
- productName: Str name of the server software product
- productUri: Uri of the product’s web site
- productVersion: Str version of the server software product
- moduleName: module which implements Haystack server protocol if its a plug-in to the product
- moduleVersion: Str version of moduleName
Using a synchronous request, you would use
op = session.about()
op.wait()
The output of op.result would print
<Grid>
Columns:
productName
moduleName
productVersion
serverTime
tz
moduleUri
serverName
productUri
serverBootTime
haystackVersion
moduleVersion
Row 0: productName='Niagara AX', moduleName='nhaystack', productVersion='3.8.41.2', serverTime=datetime.datetime(2016, 4, 28, 21, 31, 33, 882000, tzinfo=<DstTzInfo 'America/Montreal' EDT-1 day, 20:00:00 DST>), tz='Montreal', moduleUri=Uri('https://bitbucket.org/jasondbriggs/nhaystack'), serverName='Servisys', productUri=Uri('http://www.tridium.com/'), serverBootTime=datetime.datetime(2016, 4, 5, 15, 9, 8, 119000, tzinfo=<DstTzInfo 'America/Montreal' EDT-1 day, 20:00:00 DST>), haystackVersion='2.0', moduleVersion='1.2.5.18.1'
</Grid>
The return response is a hszinc.Grid
instance.
Higher Level Interface¶
The session instance also provides a higher-level interface that exposes the entities within Project Haystack as Python objects. The two functions that retrieve these entities are:
pyhaystack.client.session.HaystackSession.get_entity()
andpyhaystack.client.session.HaystackSession.find_entity()
Both are wrappers around the read operation that retrieve
pyhaystack.client.entity.entity.Entity
instances for the entities
returned.
get_entity expects a list of one or more fully qualified identifiers, and will perform a read query listing those identifiers as given.
find_entity expects a filter expression, and performs a read specifying
the given string as the filter argument. (Note: find_entity takes an
argument named filter_expr to avoid a clash with the built-in function
filter()
.)
In both cases, a dict
is returned, where the keys are the
identifiers of matching entities and the values are the Entity instances
themselves. Depending on the tags present, and the tagging_model passed to
the session, these Entity instances may include other mix-in classes as
well.
Building a filter string¶
As a convenience, it is possible to build up a filter string using Python objects, then take a string representation of that composite object to generate a filter string.
The classes are in :py:module:`pyhaystack.util.filterbuilder`. An example:
- ::
from pyhaystack.util import filterbuilder as fb # for brevity
- op = session.find_entity(fb.Field(‘site’) &
- ((fb.Field(‘tz’) == fb.Scalar(‘Brisbane’))
- (fb.Field(‘tz’) == fb.Scalar(‘Montreal’))))
op.wait() sites_in_brisbane_and_montreal = op.result
would return all sites that are in the Brisbane or Montreal timezones.
This is helpful in scenarios where you have to construct a filter programmatically and wish to avoid the possibility of unsanitised data corrupting your filter string.
Querying Sites¶
The site is
“A site entity models a single facility using the site tag. A good rule of thumb is to model any building with its own street address as its own site. For example a campus is better modeled with each building as a site, versus treating the entire campus as one site.”
—project-haystack
To browse a site you will use
op = session.find_entity(filter_expr='site')
op.wait()
site = op.result
and get a dict containing all the information provided
{'S.site': <@S.site: {area=BasicQuantity(0.0, 'ft²'), axSlotPath='slot:/site', axType='nhaystack:HSite', dis='site', geoAddr='2017', geoCity='thisTown', geoCountry='myCountry', geoLat=0.0, geoLon=0.0, geoPostalCode='', geoState='myState', geoStreet='myStreet', navName='site', site, tz='New_York'}>}
Using the default tagging model, because the entity has a site tag and a tz tag, the resulting Entity class returned here will be subclasses of the following:
pyhaystack.client.entity.entity.Entity
(base class)pyhaystack.client.entity.mixins.site.SiteMixin
(mixin class)pyhaystack.client.entity.mixins.site.TzMixin
(mixin class)
A session have typically one site attached to it, but there could be more. As a shortcut, pyhaystack provides properties on session to get the site:
# Target the first site (returns a SiteTzEntity)
session.site
# Get a dict with all sites
session.sites