diff --git a/.configure b/.configure new file mode 100644 index 0000000..3f9ed7a --- /dev/null +++ b/.configure @@ -0,0 +1,6 @@ +export EXP_USERNAME=email@email.com +export EXP_PASSWORD=Password12321 +export EXP_ORGANIZATION=scala +export EXP_HOST=http://localhost:9000 +export EXP_DEVICE_UUID=bbb91e7f-0869-46e6-8afd-e0161d5d35ca +export EXP_SECRET=d221547725fd41f0242c0b85c1e7cff514a9d51764a973e2f7abb027ea62bcf2e8b73499c5626cb5b20339998ea4f060 diff --git a/.gitignore b/.gitignore index 0d20b64..2b03b36 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ *.pyc +*.log +dist +exp_sdk.egg-info + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3b8dd20 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright (c) 2016 Scala Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 106f1a4..194fa83 100644 --- a/README.md +++ b/README.md @@ -1,142 +1,622 @@ -# Summary and Example -The SDK is an importable Python module that facilitates API and event bus actions on EXP. + +# Installation + +Install the `exp-sdk` package from PyPi via your favorite python package manager. + +```bash +pip install exp-sdk +``` + +This gives your environment access to the ```exp_sdk``` module. + + +# Runtime + +## Starting the SDK + +**`exp_sdk.start(options)`** + +Starts and returns an sdk instance. Can be called multiple times to start multiple independent instances of the sdk. The sdk can be started using user, device, or consumer app credentials.`**options` supports the following keyword arguments: + +- `username=None` The username used to log in to EXP. Required user credential. +- `password=None` The password of the user. Required user credential. +- `organization=None` The organization of the user. Required user credential +- `uuid=None` The device or consumer app uuid. Required consumer app credential and required device credential unless `allow_pairing` is `True`. +- `secret=None` The device secret. Required device credential unless `allow_pairing` is `True`. +- `api_key=None` The consumer app api key. Required consumer app credential. +- `allow_pairing=False` Whether to allow authentication to fallback to pairing mode. If `True`, invalid or empty device - credentials will start the sdk in pairing mode. +- `host=https://api.goexp.io` The api host to authenticate with. +- `enable_network=True` Whether or not to establish a socket connection with the EXP. If `False`, you will not be - able to listen for broadcasts. + +```python +import exp_sdk + +# Authenticating as a user. +exp = exp_sdk.start(username='joe@scala.com', password='joeIsAwes0me', organization='joeworld') + +# Authenticating as a device. +exp = exp_sdk.start(uuid='[uuid]', secret='[secret]') + +# Authenticating as a consumer app. +exp = exp_sdk.start(uuid='[uuid]', api_key='[api-key]') +``` + + + +## Stopping the SDK + + +**`exp_sdk.stop()`** + +Stops all running instances of the sdk, cancels all listeners and stops all socket connections. + +```python +exp_1 = exp_sdk.start(**options_1) +exp_2 = exp_sdk.start(**options_2) + +exp_sdk.stop() +exp_2.create_device() # Exception. +``` + +New instances can still be created by calling `start`. + +**`exp.stop()`** + +Stops the sdk instance, cancels its listeners, and stops all socket connections. + +```python +exp = exp_sdk.start(**options) +exp.stop() +exp.get_auth() # Exception. +``` + +Sdk instances cannot be restarted and any invokation on the instance will raise an exception. + +## Exceptions + + **`exp_sdk.ExpError`** + + Base class for all EXP exceptions. + + **`exp_sdk.UnexpectedError`** + + Raised when an unexpected error occurs. + + **`exp_sdk.RuntimeError`** + + Raised when [startup options](#runtime) are incorrect or inconsistent. + + **`exp_sdk.NetworkError`** + +Raised when an error or timeout occurs when attempting to listen on the network. + + **`exp_sdk.AuthenticationError`** + + Raised when the sdk cannot authenticate due to bad credentials. + + **`exp_sdk.ApiError`** + + Raised when an API call fails. Has properties `message`, `code`, and `payload`. `payload` is the parsed JSON document received from the API. + + +## Authentication Payload + + +**`exp.get_auth()`** + +Returns the up to date authentication payload. The authentication payload may be updated when invoking this method. ```python -import exp -exp.runtime.start( - username="joe@exp.com", - password="joesmoe25", - host="http://localhost/", - port=9000, - organization="exp") -devices = exp.api.get_devices() -devices[0].document.name = "My Device" -devices[0].save() -exp.channels.organization.broadcast(name="I changed a device!") -response = exp.channels.experience.request(name="sendMeMoney", target={ - "device": devices[0].document["uuid"] }) -print response -exp.runtime.stop() +print 'My authentication token is : %s' % exp.get_auth()['token'] ``` -# exp.runtime -## exp.runtime.start() -The SDK must be initialized by calling ```exp.runtime.start()``` and passing in configuration options. This starts the event bus and automatically authenticates API calls. The start command will block until a connection is first established. +## Logging + +The EXP SDK uses the ```exp-sdk``` logger namespace. + + +# Real Time Communications + +## Status + +**`exp.is_connected`** + +Whether or not there is an active socket connection. ```python -# Authenticate with username and password. -exp.runtime.start( - username="joe@exp.com", - password="joesmoe25", - organization="exp") -# Authenticate with device uuid and secret. -exp.runtime.start(uuid="[uuid]", secret="[secret]") +# Wait for a connection. +while not exp.is_connected: + time.sleep(1) ``` -## exp.runtime.stop() -A socket connection is made to EXP that is non-blocking. To end the connection and to stop threads spawned by the SDK call ```exp.runtime.stop()```. -## exp.runtime.on() +## Channels -Can listen for when the event bus is online/offline. Triggers an asynchronous callback. +**`exp.get_channel(name, consumer=False, system=False)`** + +Returns a [channel](#channels) with the given name and flags. ```python -def on_online(): - print "Online!" -def on_offline(): - print "Offline!" -exp.runtime.on("online", callback=on_online) -exp.runtime.on("offline", callback=on_offline) +channel = exp.get_channel('my-consumer-channel', consumer=True) ``` -# exp.api -API abstraction layer. +**`channel.broadcast(name, payload=None, timeout=0.1)`** + +Sends a [broadcast](#broadcast) on the channel with the given name and payload and returns a list of responses. `timeout` is the number of seconds to hold the request open to wait for responses. -## Example ```python -devices = exp.api.find_devices(**params) # Query for device objects (url params). -device = exp.api.get_device(uuid) # Get device by UUID. -device = exp.api.create_device(document) # Create a device from a dictionary +responses = channel.broadcast('hi!', { 'test': 'nice to meet you!' }) +[print response for response in responses] ``` -Other available namespaces: experiences, locations, content, data. contents do not currently support queries or creation, only "get_content(uuid)". -## API Resources -Each resource object contains a "document" field which is a dictionary representation of the raw resource, along with "save" and "delete" methods. +**`channel.listen(name, timeout=10, max_age=60)`** + +Returns a [listener](#listener) for events on the channel. `timeout` is how many seconds to wait for the channel to open. `max_age` is the number of seconds the listener will buffer events before they are discarded. If `timeout` is reached before the channel is opened, a `NetworkError` will be raised. + ```python -device = exp.api.create_device({ "field": value }) -device.document["field"] = 123 -device.save() -print device.document["field"] -device.delete() +channel = exp.get_channel('my-consumer-channel', consumer=True) +listener = channel.listen('hi', max_age=30) +``` + +**`channel.fling(payload)`** + +Fling an app launch payload on the channel. + +```python +location = exp.get_location('[uuid]') +location.get_channel().fling({ 'appTemplate' : { 'uuid': '[uuid'} }) +``` + + +**`channel.identify()`** + +Requests that [devices](#device) listening for this event on this channel visually identify themselves. Implementation is device specific; this is simply a convience method. + + +## Listeners + +**`listener.wait(timeout=0)`** + +Wait for `timeout` seconds for broadcasts. Returns a [broadcast](#broadcasts) if a [broadcast](#broadcasts) is in the queue or if a [broadcast](#broadcasts) is received before the timeout. If timeout is reached, returns `None`. If timeout is set to 0 (the default), will return immediately. + +```python +channel = exp.get_channel('my-channel') +listener = channel.listen('my-event') + +while True: + broadcast = listener.wait(60) + if broadcast: + print 'I got a broadcast!' +``` + +[Broadcasts](#broadcasts) are returned in the order they are received. + +**`listener.cancel()`** + +Cancels the listener. The listener is unsubscribed from [broadcasts](#broadcast) and will no longer receive messages. This cannot be undone. + +## Broadcasts + +**`broadcast.payload`** + +The payload of the broadcast. Can be any JSON serializable type. + +**`broadcast.respond(response)`** + +Respond to the broadcast with a JSON serializable response. + +```python +channel = exp.get_channel('my-channel') +listener = channel.listen('my-event') + +while True: + broadcast = listener.wait(60) + if broadcast and broadcast.payload == 'hi!': + broadcast.respond('hi back at you!') + break +``` + + +# API + + +## Devices + +Devices inherit all [common resource methods and attributes](#resources). + +**`exp.get_device(uuid=None)`** + +Returns the device with the given uuid or `None` if no device could be found. + +**`exp.get_current_device()`** + +Returns the current device or `None` if not applicable. + +**`exp.create_device(document=None)`** + +Returns a device created based on the supplied document. + +```python +device = exp.create_device({ 'subtype': 'scala:device:player' }) +``` + +**`exp.find_devices(params=None)`** + +Returns an iterable of devices matching the given query parameters. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + +**`exp.delete_device(uuid=None)`** + +Deletes the device with the given uuid. + +**`device.get_location()`** + +Returns the device's [location](#locations) or `None`. + +**`device.get_zones()`** + +Returns a list of the device's [zones](#zones). + +**`device.get_experience()`** + +Returns the device's [experience](#experiences) or `None` + + + +## Things + +Things inherit all [common resource methods and attributes](#resources). + +**`exp.get_thing(uuid=None)`** + +Returns the thing with the given uuid or `None` if no things could be found. + +**`exp.create_thing(document=None)`** + +Returns a thing created based on the supplied document. + +```python +thing = exp.create_thing({ 'subtype': 'scala:thing:rfid', 'id': '[rfid]', 'name': 'my-rfid-tag' }) +``` + +**`exp.find_things(params=None)`** + +Returns an iterable of things matching the given query parameters. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + + +**`exp.delete_thing(uuid=None)`** + +Deletes the thing with the given uuid. + + +**`thing.get_location()`** + +Returns the thing's [location](#locations) or `None`. + +**`thing.get_zones()`** + +Returns a list of the thing's [#zones](#zones). + +**`thing.get_experience()`** + +Returns the device's [experience](#experiences) or `None` + + + + +## Experiences + +Experiences inherit all [common resource methods and attributes](#resources). + +**`exp.get_experience(uuid=None)`** + +Returns the experience with the given uuid or `None` if no experience could be found. + +**`exp.get_current_experience()`** + +Returns the current experience or `None`. + +**`exp.create_experience(document=None)`** + +Returns an experience created based on the supplied document. + +**`exp.delete_experience(uuid=None)`** + +Deletes the experience with the given uuid. + +**`exp.find_experiences(params=None)`** + +Returns an iterable of experiences matching the given query parameters. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + +**`experience.get_devices(params=None)`** + +Returns an iterable of [devices](#devices) that are part of this experience. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + + + +## Locations +Locations inherit all [common resource methods and attributes](#resources). + +**`exp.get_location(uuid=None)`** + +Returns the location with the given uuid or `None` if no location could be found. + +**`exp.get_current_location()`** + +Returns the current location or `None`. + +**`exp.create_location(document=None)`** + +Returns a location created based on the supplied document. + +**`exp.find_locations(params=None)`** + +Returns an iterable of locations matching the given query parameters. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + + +**`exp.delete_location(uuid=None)`** + +Deletes the location with the given uuid. + + +**`location.get_devices(params=None)`** + +Returns an iterable of [devices](#devices) that are part of this location. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + +**`location.get_things(params=None)`** + +Returns an iterable of [things](#things) that are part of this location. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + +**`location.get_zones()`** + +Returns a list of [zones](#zones) that are part of this location. + +**`location.get_layout_url()`** + +Returns a url pointing to the location's layout image. + + + +## Zones +Zones inherit the [common resource methods and attributes](#resources) `save()`, `refresh()`, and `get_channel()`. + +**`exp.get_current_zones()`** + +Returns a list of the current zones or an empty list. + +**`zone.key`** + +The zone's key. + +**`zone.name`** + +The zone's name. + +**`zone.get_devices()`** + +Returns all [devices](#devices) that are members of this zone. + +**`zone.get_things()`** + +Returns all [things](#things) that are members of this zone. + +**`zone.get_location()`** + +Returns the zone's [location](#locations) + + +## Feeds +Feeds inherit all [common resource methods and attributes](#resources). + +**`exp.get_feed(uuid=None)`** + +Returns the feed with the given uuid or `None` if no feed could be found. + +**`exp.create_feed(document=None)`** + +Returns a feed created based on the supplied document. + +```python +feed = exp.create_feed({ 'subtype': 'scala:feed:weather', 'searchValue': '16902', 'name': 'My Weather Feed' }) ``` +**`exp.find_feeds(params=None)`** + +Returns an iterable of feeds matching the given query parameters. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + +```python +feeds = exp.find_feeds({ 'subtype': 'scala:feed:facebook' }) +``` + + +**`exp.delete_feed(uuid=None)`** + +Deletes the feed with the given uuid. + + +**`feed.get_data(**params)`** + +Returns the feed's data. For dynamic feeds specify key value query params in `params`. + + + +## Data + +Data items inherit the [common resource methods and attributes](#resources) `save()`, `refresh()`, `delete()`, and `get_channel()`. +There is a limit of 16MB per data document. + +*Note that data values must be a javascript object, but can contain other primitives.* + + + +**`exp.get_data(group='default', key=None)`** + +Returns the data item with the given group or key or `None` if the data item could not be found. ```python -data = exp.api.get_data("key1", "group0") -print data.value -data.value = { "generic": 1111 } -data.save() -data.delete() +data = exp.get_data('cats', 'fluffy') +``` + +**`exp.create_data(group='default', key=None, value=None)`** -data = exp.api.create_data(key="4", group="cats", { "name": "fluffy" }) +Returns a data item created based on the supplied group, key, and value. +```python +data = exp.create_data('cats', 'fluffy', { 'color': 'brown'}) ``` -The "content" resource has a ```get_children()``` method that returns the content's children (a list of content objects). Every content object also has a ```get_url()``` and ```get_variant_url(https://github.com/name)``` method that returns a delivery url for the content. +**`exp.find_data(params=None)`** +Returns an iterable of data items matching the given query parameters. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). -# exp.channels -Parent namespace for interaction with the event bus. Available channels are: ```python -exp.channels.system # Calls to and from the system -exp.channels.experience -exp.channels.location -exp.channels.organization +items = exp.find_data({ 'group': 'cats' }) ``` -## exp.channels.[channel].fling -Fling content on a channel. +**`exp.delete_data(group=None, key=None)`** + +Deletes the data item with the given group and key. + + +**`data.key`** + +The data item's key. Settable. + +**`data.group`** + +The data item's group. Settable + +**`data.value`** + +The data item's value. Settable. + + + +## Content +Content items inherit all [common resource methods and attributes](#resources) except `save()`. + +**`exp.get_content(uuid=None)`** + +Returns the content item with the given uuid or `None` if no content item could be found. + +**`exp.find_content(params=None)`** + +Returns a list of content items matching the given query parameters. `params` is a dictionary of query parameters. + +**`content.subtype`** + +The content item's subtype. Not settable. + +**`content.get_url()`** + +Returns the delivery url for this content item. + +**`content.has_variant(name)`** + +Returns a boolean indicating whether or not this content item has a variant with the given name. + +**`content.get_variant_url(https://github.com/name)`** + +Returns the delivery url for a variant of this content item. + +**`content.get_children(params)`** + +Returns an iterable of the content items children. `params` is a dictionary of query parameters. Iterable also has attributes matching the raw API response document properties (i.e. `total` and `results`). + +## Resources + +These methods and attributes are shared by many of the abstract API resources. + +**`resource.uuid`** + +The uuid of the resource. Cannot be set. + +**`resource.name`** + +The name of the resource. Can be set directl + +**`resource.document`** + +The resource's underlying document + +**`resource.save()`** + +Saves the resource and updates the document in place. + ```python -uuid = '4abd....' -exp.channels.organization.fling(uuid) +device = exp.get_device('[uuid]') +device.name = 'my-new-name' +device.save() # device changes are now saved ``` -## exp.channels.[channel].request -Send a request on this channel. +**`resource.refresh()`** + +Refreshes the resource's underlying document in place. + ```python -response = exp.channels.location.request( - name="getSomething", - target= { "device" : "[uuid]"}, payload= { "info": 123 }) +device = exp.create_device() +device.name = 'new-name' +device_2 = exp.get_device(device.uuid) +device.save() +device_2.refresh() +print device_2.name # 'new-name' ``` -## exp.channels.[channel].respond -Attach a callback to handle requests to this device. Return value of callback is response content. Must be JSON serializable. +**`resource.get_channel(system=False, consumer=False)`** + +Returns the channel whose name is contextually associated with this resource. + ```python -def get_something(payload=None): - return "Something" -exp.channels.location.respond(name="getSomething", callback=get_something_callback) +channel = experience.get_channel() +channel.broadcast('hello?') ``` -## exp.channels.[channel].broadcast -Sends a broadcast message on the channel. +**`resource.delete()`** + +Deletes the resource. + + +## Custom Requests + +These methods all users to send custom authenticated API calls. `params` is a dictionary of url params, `payload` is a JSON serializable type, and `timeout` is the duration, in seconds, to wait for the request to complete. `path` is relative to the api host root. All methods will return a JSON serializable type. + +**`exp.get(path, params=None, timeout=10)`** + +Send a GET request. + ```python -exp.channels.experience.broadcast(name="Hi!", payload={}) +result = exp.get('/api/devices', { 'name': 'my-name' }) # Find devices by name. ``` -## exp.channels.[channel].listen -Listens for broadcasts on the channel. Non-blocking, callback is spawned in new thread. +**`exp.post(path, payload=None, params=None, timeout=10)`** + +Send a POST request. + ```python -def my_method(payload=None): - print payload -exp.channels.experience.listen(name="Hi!", callback=my_method) +document = exp.post('/api/experiences', {}) # Create a new empty experience. ``` +**`exp.patch(path, payload=None, params=None, timeout=10)`** + +Send a PATCH request. +```python +document = exp.patch('/api/experiences/[uuid]', { 'name': 'new-name' }) # Rename an experience. +``` +**`exp.put(path, payload=None, params=None, timeout=10)`** +Send a PUT request. +```python +document = exp.put('/api/data/cats/fluffy', { 'eyes': 'blue'}) # Insert a data value. +``` +**`exp.delete(path, params=None, timeout=10)`** +Send a DELETE request. +```python +exp.delete('/api/location/[uuid]') # Delete a location. +``` diff --git a/bin/setup-ci-tests.sh b/bin/setup-ci-tests.sh new file mode 100644 index 0000000..515ac59 --- /dev/null +++ b/bin/setup-ci-tests.sh @@ -0,0 +1,20 @@ + +set -e + +cd .. +git clone git@github.com:scalainc/exp-api.git +cd exp-api +git checkout develop +git pull origin develop +npm install +NODE_ENV=test npm start& +sleep 10 +cd .. +git clone git@github.com:scalainc/exp-network.git +cd exp-network +git checkout develop +git pull origin develop +npm install +npm start& +sleep 10 +cd ../exp-python2-sdk diff --git a/circle.yml b/circle.yml index 89a1dd2..9a72c94 100644 --- a/circle.yml +++ b/circle.yml @@ -1,6 +1,9 @@ machine: python: version: 2.7.10 + node: + version: 4.2.3 + dependencies: pre: @@ -9,4 +12,4 @@ dependencies: test: override: - - nosetests \ No newline at end of file + - chmod +x ./bin/setup-ci-tests.sh && ./bin/setup-ci-tests.sh && nosetests diff --git a/exp/__init__.py b/exp/__init__.py deleted file mode 100644 index 3c17b39..0000000 --- a/exp/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -""" Main module for Scala EXP SDK """ - -from . import channels -from . import runtime -from . import api - - - - - - - - - - - - - - - - - - - - - diff --git a/exp/api/__init__.py b/exp/api/__init__.py deleted file mode 100644 index 2fd62a7..0000000 --- a/exp/api/__init__.py +++ /dev/null @@ -1,103 +0,0 @@ -import urllib - -from .. lib import api_utils -from .. lib.models.device import Device -from .. lib.models.location import Location -from .. lib.models.experience import Experience -from .. lib.models.content import Content -from .. lib.models.data import Data -from .. lib.models.thing import Thing -from .. lib.models.feed import Feed - - -""" Content """ - -def get_content(uuid): - return Content( - api_utils.get("/api/content/" + uuid + "/children"), - _is_children_populated=True) - - -""" Devices """ - -def find_devices(**params): - query = api_utils.get('/api/devices', params=params) - empty = [] - return [Device(x, _new=False) for x in query.get("results", empty)] - -def get_device(uuid): - return Device(api_utils.get('/api/devices/' + uuid), _new=False) - -def create_device(document): - return Device(document).save() - - -""" Things """ - -def find_things(**params): - query = api_utils.get('/api/things', params=params) - empty = [] - return [Thing(x, _new=False) for x in query.get("results", empty)] - -def get_thing(uuid): - return Thing(api_utils.get('/api/things/' + uuid), _new=False) - -def create_thing(document): - return Thing(document).save() - - -""" Experiences """ - -def find_experiences(**params): - query = api_utils.get('/api/experiences', params=params) - empty = [] - return [Experience(x, _new=False) for x in query.get("results", empty)] - -def get_experience(uuid): - return Experience(api_utils.get('/api/experiences/' + uuid), _new=False) - -def create_experience(document): - return Experience(document).save() - - -""" Locations """ - -def find_locations(**params): - query = api_utils.get('/api/locations', params=params) - empty = [] - return [Location(x, _new=False) for x in query.get("results", empty)] - -def get_location(uuid): - return Location(api_utils.get('/api/locations/' + uuid), _new=False) - -def create_location(document): - return Location(document).save() - - -""" Data """ - -def get_data(key, group): - key = urllib.quote(key, safe='') - group = urllib.quote(group, safe='') - return Data(**api_utils.get('/api/data/' + group + '/' + key)) - -def find_data(**params): - query = api_utils.get('/api/data', params=params) - return [Data(**x) for x in query.get('results', [])] - -def create_data(**params): - return Data(**params).save() - - -""" Feeds """ - -def find_feeds(**params): - query = api_utils.get('/api/connectors/feeds', params=params) - empty = [] - return [Feed(x, _new=False) for x in query.get("results", empty)] - -def get_feed(uuid): - return Feed(api_utils.get('/api/connectors/feeds/' + uuid), _new=False) - -def create_feed(document): - return Feed(document).save() diff --git a/exp/channels.py b/exp/channels.py deleted file mode 100644 index 58a1e3a..0000000 --- a/exp/channels.py +++ /dev/null @@ -1,6 +0,0 @@ -from lib import channel - -system = channel.Channel('system') -organization = channel.Channel('organization') -location = channel.Channel('location') -experience = channel.Channel('experience') diff --git a/exp/lib/api_utils.py b/exp/lib/api_utils.py deleted file mode 100644 index a7587ec..0000000 --- a/exp/lib/api_utils.py +++ /dev/null @@ -1,63 +0,0 @@ -import requests -import urllib - -from . import config -from . import credentials - -def generate_url(https://github.com/path): - base = config.get("host") - if config.get("port"): - base = "{0}:{1}".format(base, config.get("port")) - return "{0}{1}".format(base, urllib.quote(path)) - -def authenticate(username, password, organization): - url = generate_url("https://github.com/api/auth/login") - payload = {} - payload["username"] = username - payload["password"] = password - payload["org"] = organization - response = requests.post(url, json=payload) - response.raise_for_status() - body = response.json() - return body["token"] - -def get(path, params=None): - url = generate_url(https://github.com/path) - headers = {} - headers["Authorization"] = "Bearer " + credentials.get_token() - response = requests.get(url, params=params, headers=headers) - response.raise_for_status() - return response.json() - -def post(path, payload=None, params=None): - url = generate_url(https://github.com/path) - headers = {} - headers["Authorization"] = "Bearer " + credentials.get_token() - response = requests.post(url, params=params, json=payload, headers=headers) - response.raise_for_status() - return response.json() - -def patch(path, payload=None, params=None): - url = generate_url(https://github.com/path) - headers = {} - headers["Authorization"] = "Bearer " + credentials.get_token() - response = requests.patch(url, params=params, json=payload, headers=headers) - response.raise_for_status() - return response.json() - -def put(path, payload=None, params=None): - url = generate_url(https://github.com/path) - headers = {} - headers["Authorization"] = "Bearer " + credentials.get_token() - response = requests.put(url, params=params, json=payload, headers=headers) - response.raise_for_status() - return response.json() - -def delete(path, payload=None, params=None): - url = generate_url(https://github.com/path) - headers = {} - headers["Authorization"] = "Bearer " + credentials.get_token() - response = requests.delete(url, params=params, json=payload, headers=headers) - response.raise_for_status() - return response.json() - diff --git a/exp/lib/channel.py b/exp/lib/channel.py deleted file mode 100644 index fbce471..0000000 --- a/exp/lib/channel.py +++ /dev/null @@ -1,129 +0,0 @@ -import random -import time -import threading -from . import socket - -class Timeout(Exception): - pass - -class Error(Exception): - pass - -class Channel(object): - - Timeout = Timeout - Error = Error - - def __init__(self, name): - self._name = name - self._listeners = {} - self._responses = {} - self._responders = {} - self._lock = threading.Lock() - socket.on('message', self._on_message) - - def _clean_responses(self): - """ Clean up responses that have not been processed """ - self._lock.acquire() - now = time.time() - for id, response in self._responses.iteritems(): - if now - response["time"] > 10: - self._responses.pop(id, None) - self._lock.release() - - def _on_response(self, message): - self._lock.acquire() - message["time"] = time.time() - self._responses[message["id"]] = message - self._lock.release() - - def _on_broadcast(self, message): - self._lock.acquire() - callbacks = self._listeners.get(message["name"], None) - if not callbacks: - return self._lock.release() - for callback in callbacks: - try: - callback(message['payload']) - except: - pass - self._lock.release() - - def _on_request(self, message): - self._lock.acquire() - if not self._responders.get(message["name"]): - return self._lock.release() - callback = self._responders.get(message["name"]) - try: - payload = callback(message['payload']) - except: - socket.send({ - "type": "response", - "id": message["id"], - "error": "An error occured" - }) - else: - socket.send({ - "type": "response", - "id": message["id"], - "payload": payload - }) - self._lock.release() - - def _on_message(self, message): - if message["type"] == "response": - self._clean_responses() - return self._on_response(message) - if message["channel"] != self._name: - return - if message["type"] == "broadcast": - self._on_broadcast(message) - elif message["type"] == "request": - self._on_request(message) - - def request(self, name=None, target=None, payload=None): - message = {} - message["type"] = "request" - message["id"] = str(random.random()) - message["channel"] = self._name - message["name"] = name - message["payload"] = payload - message["device"] = {} # This will be deprecated - message["device"]["target"] = target - socket.send(message) - start = time.time() - while time.time() - start < 10: - self._lock.acquire() - response = self._responses.get(message["id"], None) - if response: - self._responses.pop(message["id"], None) - self._lock.release() - if response.get('error', None): - raise Error(response.get('error')) - return response.get('payload', None) - self._lock.release() - time.sleep(.1) - self._lock.acquire() - self._responses.pop(message["id"], None) - self._lock.release() - raise self.Timeout() - - - def broadcast(self, name=None, payload=None): - socket.send({ - 'type': 'broadcast', - 'channel': self._name, - 'name': name, - 'payload': payload - }) - - def listen(self, name=None, callback=None): - if not self._listeners.get(name): - self._listeners[name] = [] - self._listeners[name].append(callback) - - def fling(self, uuid=None): - return self.broadcast(name='fling', payload={'uuid': uuid}) - - def respond(self, name=None, callback=None): - self._responders[name] = callback diff --git a/exp/lib/config.py b/exp/lib/config.py deleted file mode 100644 index c6b1d24..0000000 --- a/exp/lib/config.py +++ /dev/null @@ -1,10 +0,0 @@ -_vars = {} - -def set(**kwargs): - for key, value in kwargs.iteritems(): - _vars[key] = value - -def get(name): - return _vars.get(name, None) - - diff --git a/exp/lib/credentials.py b/exp/lib/credentials.py deleted file mode 100644 index 42ee657..0000000 --- a/exp/lib/credentials.py +++ /dev/null @@ -1,91 +0,0 @@ -import time -import requests -import json -import hmac -from hashlib import sha256 -from base64 import urlsafe_b64encode - - -_vars = {} -_vars["uuid"] = None -_vars["secret"] = None -_vars["username"] = None -_vars["password"] = None -_vars["token"] = None -_vars["time"] = 0 -_vars["networkUuid"] = None -_vars["apiKey"] = None - -def _reset(): - _vars["uuid"] = None - _vars["secret"] = None - _vars["username"] = None - _vars["password"] = None - _vars["organization"] = None - _vars["token"] = None - _vars["time"] = 0 - _vars["networkUuid"] = None - _vars["apiKey"] = None - -def _jwtHS256encode(payload, secret): - alg = urlsafe_b64encode('{"alg":"HS256","typ":"JWT"}') - if not isinstance(payload, basestring): - payload = json.dumps(payload, separators=(',', ':')) - payload = urlsafe_b64encode(payload.encode("utf-8")).rstrip('=') - sign = urlsafe_b64encode(hmac.new(secret.encode("utf-8"), '.'.join([alg, payload]), sha256).digest()).rstrip('=') - return '.'.join([alg, payload, sign]) - -def set_user_credentials(username, password, organization): - _reset() - _vars["username"] = username - _vars["password"] = password - _vars["organization"] = organization - -def set_device_credentials(uuid, secret): - _reset() - _vars["uuid"] = uuid - _vars["secret"] = secret - -def set_network_credentials(uuid, apiKey): - _reset() - _vars["networkUuid"] = uuid - _vars["apiKey"] = apiKey - -def set_token(token): - _reset() - _vars["token"] = token - _vars["time"] = float("inf") - -def get_token(): - if not _vars["token"] or time.time() - _vars["time"] < 120: - _generate_token() - return _vars["token"] - -def _generate_token(): - if _vars["uuid"] and _vars["secret"]: - _generate_device_token() - elif _vars["username"] and _vars["password"]: - _generate_user_token() - elif _vars["networkUuid"] and _vars["apiKey"]: - _generate_network_token() - else: - _vars["token"] = '' - _vars["time"] = 0 - -def _generate_user_token(): - from . import api_utils # Avoid circular import. - _vars["token"] = api_utils.authenticate(_vars["username"], _vars["password"], _vars["organization"]) - _vars["time"] = time.time() - -def _generate_device_token(): - payload = {} - payload["uuid"] = cls._uuid - _vars["token"] = _jwtHS256encode(payload, _vars["secret"]) - _vars["time"] = time.time() - -def _generate_network_token(): - payload = {} - payload["networkUuid"] = _vars["networkUuid"] - _vars["token"] = _jwtHS256encode(payload, _vars["apiKey"]) - _vars["time"] = time.time() - diff --git a/exp/lib/event_node.py b/exp/lib/event_node.py deleted file mode 100644 index 98e115f..0000000 --- a/exp/lib/event_node.py +++ /dev/null @@ -1,24 +0,0 @@ -import threading -import inspect - -class EventNode(object): - - def __init__(self): - self.callbacks = {} - - def on(self, name, callback): - if not self.callbacks.get(name): - self.callbacks[name] = [] - self.callbacks[name].append(callback) - - def trigger(self, name, payload=None): - if not self.callbacks.get(name): return - for callback in self.callbacks.get(name): - spec = inspect.getargspec(callback) - if len(spec.args) == 0: - thread = threading.Thread(target=callback) - else: - thread = threading.Thread(target=callback, args=[payload]) - thread.daemon = True - thread.start() - diff --git a/exp/lib/models/__init__.py b/exp/lib/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/exp/lib/models/content.py b/exp/lib/models/content.py deleted file mode 100644 index 7154ab5..0000000 --- a/exp/lib/models/content.py +++ /dev/null @@ -1,41 +0,0 @@ -import urllib - -from .. import api_utils - -class Content(object): - - def __init__(self, document, _is_children_populated=False): - self.document = document - self._is_children_populated = _is_children_populated - - def get_url(https://github.com/self): - subtype = self.document['subtype'] - if subtype == 'scala:content:file': - path = urllib.quote(self.document['path']) - return api_utils.generate_url('https://github.com/api/delivery') + path - elif subtype == 'scala:content:app': - path = urllib.quote(self.document['path'] + '/index.html') - return api_utils.generate_url('https://github.com/api/delivery') + path - elif subtype == 'scala:content:url': - return self.document['url'] - raise NotImplementedError('Cannot get url for this subtype.') - - def get_variant_url(https://github.com/self,%20variant_name): - subtype = self.document['subtype'] - path = self.document['path'] - variants = self.document.get('variants', {}).get(name, None) - if not variants: - raise NameError('Variant not found.') - if subtype == 'scala:content:file': - query = '?' + urllib.urlencode({ 'variant' : variant_name }) - return api_utils.generate_url('https://github.com/api/delivery'%20+%20path) + query - raise NameError('Variant not found.') - - def get_children(self): - if not self._is_children_populated: - path = '{0}{1}{2}'.format('/api/content/', self.document.get("uuid"), '/children') - self.document = api_utils.get(path) - self._is_children_populated = True - return [Content(x) for x in self.document.get("children")] - - diff --git a/exp/lib/models/data.py b/exp/lib/models/data.py deleted file mode 100644 index af3d456..0000000 --- a/exp/lib/models/data.py +++ /dev/null @@ -1,24 +0,0 @@ -import urllib - -from .. import api_utils - -class Data(object): - - def __init__(self, key=None, group=None, value=None, **kwargs): - self.key = key - self.group = group - self.value = value - encoded_key = urllib.quote_plus(key) - encoded_group = urllib.quote_plus(group) - self._path = '/api/data/{0}/{1}'.format(encoded_group, encoded_key) - - def save(self): - api_utils.put(self._path, payload=self.value) - return self - - def delete(self): - api_utils.delete(self._path) - return self - - - diff --git a/exp/lib/models/device.py b/exp/lib/models/device.py deleted file mode 100644 index ac20d18..0000000 --- a/exp/lib/models/device.py +++ /dev/null @@ -1,22 +0,0 @@ -from .. import api_utils - -class Device(object): - - def __init__(self, document, _new=True): - self.document = document - self._new = _new - - def save(self): - if self._new: - self.document = api_utils.post("/api/devices", payload=self.document) - self._new = False - else: - self.document = api_utils.patch("/api/devices/" + self.document["uuid"], payload=self.document) - return self - - def delete(self): - api_utils.delete("/api/devices/" + self.document["uuid"]) - return self - - - diff --git a/exp/lib/models/experience.py b/exp/lib/models/experience.py deleted file mode 100644 index ac0aec4..0000000 --- a/exp/lib/models/experience.py +++ /dev/null @@ -1,19 +0,0 @@ -from .. import api_utils - -class Experience(object): - - def __init__(self, document, _new=True): - self.document = document - self._new = _new - - def save(self): - if self._new: - self.experience = api_utils.post("/api/experiences", payload=self.document) - self._new = False - else: - self.document = api_utils.patch("/api/experiences/" + self.document["uuid"], payload=self.document) - return self - - def delete(self): - api_utils.delete("/api/experiences/" + self.document["uuid"]) - return self diff --git a/exp/lib/models/feed.py b/exp/lib/models/feed.py deleted file mode 100644 index 735d74b..0000000 --- a/exp/lib/models/feed.py +++ /dev/null @@ -1,26 +0,0 @@ -from .. import api_utils - -class Feed(object): - - def __init__(self, document, _new=True): - self.document = document - self._new = _new - - def save(self): - if self._new: - self.document = api_utils.post('/api/connectors/feeds', payload=self.document) - self._new = False - else: - self.document = api_utils.patch('/api/connectors/feeds' + self.document['uuid'], payload=self.document) - return self - - def get_data (self): - return api_utils.get('/api/connectors/feeds/' + self.document['uuid'] + '/data') - - - def delete(self): - api_utils.delete('/api/locations/' + self.document['uuid']) - return self - - - diff --git a/exp/lib/models/location.py b/exp/lib/models/location.py deleted file mode 100644 index a2f145e..0000000 --- a/exp/lib/models/location.py +++ /dev/null @@ -1,22 +0,0 @@ -from .. import api_utils - -class Location(object): - - def __init__(self, document, _new=True): - self.document = document - self._new = _new - - def save(self): - if self._new: - self.document = api_utils.post("/api/locations", payload=self.document) - self._new = False - else: - self.document = api_utils.patch("/api/locations/" + self.document["uuid"], payload=self.document) - return self - - def delete(self): - api_utils.delete("/api/locations/" + self.document["uuid"]) - return self - - - diff --git a/exp/lib/models/thing.py b/exp/lib/models/thing.py deleted file mode 100644 index e9eadc3..0000000 --- a/exp/lib/models/thing.py +++ /dev/null @@ -1,22 +0,0 @@ -from .. import api_utils - -class Thing(object): - - def __init__(self, document, _new=True): - self.document = document - self._new = _new - - def save(self): - if self._new: - self.document = api_utils.post("/api/things", payload=self.document) - self._new = False - else: - self.document = api_utils.patch("/api/things/" + self.document["uuid"], payload=self.document) - return self - - def delete(self): - api_utils.delete("/api/things/" + self.document["uuid"]) - return self - - - diff --git a/exp/lib/socket.py b/exp/lib/socket.py deleted file mode 100644 index 23d3367..0000000 --- a/exp/lib/socket.py +++ /dev/null @@ -1,110 +0,0 @@ -import Queue -import threading -import time -import logging -import random - -from socketIO_client import SocketIO, BaseNamespace - -from . import event_node - - -logging.getLogger('request').setLevel(logging.DEBUG) -logging.basicConfig(level=logging.DEBUG) - -_events = event_node.EventNode() - -_vars = {} -_vars["io"] = None -_vars["threadId"] = None - -_outgoing = Queue.Queue() -_lock = threading.Lock() - -on = _events.on - -class _Namespace(BaseNamespace): - - def on_message(self, message): - if type(message) != dict: return - _events.trigger('message', message) - - def on_connect(self): - _events.trigger('connected') - - def on_disconnect(self): - _events.trigger('disconnected') - - -class Timeout(Exception): - pass - - -def _disconnect(): - _lock.acquire() - if _vars["io"]: - _vars["io"].disconnect() - _vars["io"] = None - _lock.release() - -def _connect(host, port, token): - _disconnect() - _lock.acquire() - start = time.time() - while time.time() - start < 5: - if not _vars["io"]: - try: - _vars["io"] = SocketIO(host, port, params={ "token": token }, Namespace=_Namespace) - except: - _vars["io"] = None - time.sleep(1) - continue - if _vars["io"].connected: - return _lock.release() - time.sleep(.1) - _lock.release() - _disconnect() - -def send(message): - _outgoing.put(message) - -def _process_incoming(): - if not _vars["io"]: - return - try: - _vars["io"].wait(.1) - except: - time.sleep(1) - -def _process_outgoing(): - try: - message = _outgoing.get_nowait() - except Queue.Empty: - return - if not _vars["io"]: - return - _vars["io"].emit('message', message) - -def _loop(): - threadId = _vars["threadId"] - _lock.release() - while threadId == _vars["threadId"]: - _lock.acquire() - _process_incoming() - _process_outgoing() - _lock.release() - time.sleep(.1) - -def start(*args): - _lock.acquire() - _vars["threadId"] = random.random() - threading.Thread(target=_loop).start() - _connect(*args) - -def stop(): - _lock.acquire() - _vars["threadId"] = None - _lock.release() - _disconnect() - - diff --git a/exp/runtime.py b/exp/runtime.py deleted file mode 100644 index 7fec5e0..0000000 --- a/exp/runtime.py +++ /dev/null @@ -1,45 +0,0 @@ -from lib import socket -from lib import event_node -from lib import credentials -from lib import config - -_events = event_node.EventNode() -on = _events.on - -def _trigger_online(): - _events.trigger('online') - -def _trigger_offline(): - _events.trigger('offline') - -socket.on('connected', _trigger_online) -socket.on('disconnected', _trigger_offline) - -def start( - host='http://api.exp.scala.com/', - port=80, - uuid=None, - secret=None, - username=None, - password=None, - organization=None, - token=None, - networkUuid=None, - apiKey=None, - **kwargs): - config.set(host=host, port=port) - if uuid and secret: - credentials.set_device_credentials(uuid, secret) - elif networkUuid and apiKey: - credentials.set_network_credentials(networkUuid, apiKey) - elif username and password and organization: - credentials.set_user_credentials(username, password, organization) - elif token: - credentials.set_token(token) - socket.start(host, port, credentials.get_token()) - -def stop(): - socket.stop() - - - diff --git a/exp_sdk/__init__.py b/exp_sdk/__init__.py new file mode 100644 index 0000000..ccd9e80 --- /dev/null +++ b/exp_sdk/__init__.py @@ -0,0 +1,4 @@ +""" Main module for EXP Python SDK """ + +from .exp import start, stop +from .exceptions import * \ No newline at end of file diff --git a/exp_sdk/api.py b/exp_sdk/api.py new file mode 100644 index 0000000..fa78c53 --- /dev/null +++ b/exp_sdk/api.py @@ -0,0 +1,467 @@ +import urllib +import requests +import traceback + + +class Resource (object): + + _collection_path = None + + def __init__ (self, document, sdk): + self._document = document + self._sdk = sdk + + def _get_channel_name (self): + raise NotImplementedError + + def _get_resource_path (self): + raise NotImplementedError + + @property + def document (self): + if not isinstance(self._document, dict): + self._document = {} + return self._document + + def save (self): + self._document = self._sdk.api.patch(self._get_resource_path(), self.document) + + def refresh (self): + self._document = self._sdk.api.get(self._get_resource_path()) + + @classmethod + def create (cls, document, sdk): + return cls(sdk.api.post(cls._collection_path, document), sdk) + + @classmethod + def find (cls, params, sdk): + return Collection(cls, sdk.api.get(cls._collection_path, params), sdk) + + def get_channel(self, **kwargs): + return self._sdk.network.get_channel(self._get_channel_name(), **kwargs) + + +class Collection (list): + + def __init__(self, Resource, document, sdk): + list.__init__(self, [Resource(doc, sdk) for doc in document['results']]) + for key, value in document.iteritems(): + self.__dict__[key] = value + +class CommonResource (Resource): + + @property + def uuid (self): + return self.document['uuid'] + + @property + def name(self): + return self.document['name'] + + @name.setter + def name (self, value): + self.document['name'] = value + + def _get_channel_name (self): + return self.uuid + + def _get_resource_path (self): + return '{0}/{1}'.format(self._collection_path, self.uuid) + + @classmethod + def delete_ (cls, uuid, sdk): + if not uuid or not isinstance(uuid, basestring): + return None + path = '{0}/{1}'.format(cls._collection_path, uuid) + return sdk.api.delete(path) + + def delete (self): + return self._sdk.api.delete(self._get_resource_path()) + + @classmethod + def get (cls, uuid, sdk): + if not uuid or not isinstance(uuid, basestring): + return None + path = '{0}/{1}'.format(cls._collection_path, uuid) + try: + remote_document = sdk.api.get(path) + except sdk.exceptions.ApiError as exception: + if exception.status_code == 404: + return None + raise + return cls(remote_document, sdk) + + +class GetLocationMixin (object): + + def _get_location_uuid (self): + raise NotImplementedError + + def _get_zone_keys (self): + raise NotImplementedError + + def get_location (self): + uuid = self._get_location_uuid() + if not uuid: + return None + return self._sdk.api.Location.get(uuid, self._sdk) + + def get_zones (self): + location = self.get_location() + if not location: + return [] + keys = self._get_zone_keys() + return [self._sdk.api.Zone(document, self, self._sdk) for document in location.document.get('zones', []) if document.get('key') in keys] + + +class GetExperienceMixin (object): + + def _get_experience_uuid (self): + raise NotImplementedError + + def get_experience (self): + uuid = self._get_experience_uuid() + if not uuid: + return None + return self._sdk.api.Experience.get(uuid, self._sdk) + + +class GetDevicesMixin (object): + + def _get_device_query_params (self): + raise NotImplementedError + + def get_devices (self, params=None): + return self._sdk.api.Device.find(self._get_device_query_params(params), self._sdk) + + +class GetThingsMixin (object): + + def _get_thing_query_params (self): + raise NotImplementedError + + def get_things (self, params=None): + return self._sdk.api.Thing.find(self._get_thing_query_params(params), self._sdk) + + +class Device (CommonResource, GetLocationMixin, GetExperienceMixin): + + _collection_path = '/api/devices' + + def _get_experience_uuid (self): + return self.document.get('experience', {}).get('uuid') + + def _get_location_uuid (self): + return self.document.get('location', {}).get('uuid') + + def _get_zone_keys (self): + return [document.get('key') for document in self.document.get('location', {}).get('zones', []) if document.get('key')] + + @classmethod + def get_current (cls, sdk): + auth = sdk.authenticator.get_auth() + if not auth or not auth['identity']['type'] == 'device': + return None + return cls.get(auth['identity']['uuid'], sdk) + + +class Thing (CommonResource, GetLocationMixin): + + _collection_path = '/api/things' + + def _get_location_uuid (self): + return self.document.get('location', {}).get('uuid') + + def _get_zone_keys (self): + return [document.get('key') for document in self.document.get('location', {}).get('zones', []) if document.get('key')] + + +class Experience (CommonResource, GetDevicesMixin): + + _collection_path = '/api/experiences' + + def _get_device_query_params (self, params): + params = params or {} + params['experience.uuid'] = self.uuid + + @classmethod + def get_current (cls, sdk): + device = Device.get_current(sdk) + return device.get_experience() if device else None + + +class Location (CommonResource, GetDevicesMixin, GetThingsMixin): + + _collection_path = '/api/locations' + + def _get_device_query_params (self, params=None): + params = params or {} + params['location.uuid'] = self.uuid + + def _get_thing_query_params (self, params=None): + params = params or {} + params['location.uuid'] = self.uuid + + def get_zones (self): + return [self._sdk.api.Zone(document, self, self._sdk) for document in self.document.get('zones', [])] + + def get_layout_url (self): + return self._get_resource_path() + '/layout?_rt=' + self._sdk.authenticator.get_auth()['restrictedToken'] + + @classmethod + def get_current(cls, sdk): + device = Device.get_current(sdk) + return device.get_location() if device else None + + +class Feed (CommonResource): + + _collection_path = '/api/connectors/feeds' + + def get_data (self, **params): + return self._sdk.api.get(self._get_resource_path() + '/data', params=params) + + +class Zone (Resource, GetDevicesMixin, GetThingsMixin): + + def __init__ (self, document, location, sdk): + super(Zone, self).__init__(document, sdk) + self._location = location + + @property + def key(self): + return self.document['key'] + + @property + def name(self): + return self.document['name'] + + @name.setter + def name (self, value): + self.document['name'] = value + + def _get_device_query_params (self, params=None): + params = params or {} + params['location.uuid'] = self._location.uuid + params['location.zones.key'] = self.key + return params + + def _get_thing_query_params (self, params=None): + params = params or {} + params['location.uuid'] = self._location.uuid + params['location.zones.key'] = self.key + return params + + def save (self): + return self._location.save() + + def refresh (self): + self._location.refresh() + matches = [document for document in self._location.document.get('zones', []) if self.key == document['key']] + if matches: + self._document = matches[0] + + def get_location (self): + return self._location + + def _get_channel_name(self): + return self._location.uuid + ':zone:' + self.key + + @classmethod + def get_current(cls, sdk): + device = Device.get_current(sdk) + return device.get_zones() if device else [] + + +class Data (Resource): + + _collection_path = '/api/data' + + @property + def group(self): + return self.document.get('group') + + @group.setter + def group (self, value): + self.document['group'] = value + + @property + def key(self): + return self.document.get('key') + + @key.setter + def key (self, value): + self.document['key'] = value + + @property + def value (self): + return self.document.get('value') + + @value.setter + def value(self, value): + self.document['value'] = value + + def _get_resource_path(self): + return '{0}/{1}/{2}'.format(self._collection_path, self.group, self.key) + + @classmethod + def get (cls, group, key, sdk): + path = '{0}/{1}/{2}'.format(cls._collection_path, group, key) + try: + document = sdk.api.get(path) + except sdk.exceptions.ApiError as exception: + if exception.status_code == 404: + return None + raise + return cls(document, sdk) + + @classmethod + def create (cls, group, key, value, sdk): + path = '{0}/{1}/{2}'.format(cls._collection_path, group, key) + document = sdk.api.put(path, value) + data = cls(document, sdk) + return data + + def save (self): + self._document = self._sdk.api.put(self._get_resource_path(), self.value) + + def _get_channel_name (self): + return 'data:{0}:{1}'.format(self.group, self.key) + + @classmethod + def delete_ (cls, group, key, sdk): + path = '{0}/{1}/{2}'.format(cls._collection_path, group, key) + sdk.api.delete(path) + + def delete (self): + self._sdk.api.delete(self._get_resource_path()) + + +class Content (CommonResource): + + _collection_path = '/api/content' + + def save (self): + raise NotImplementedError + + @property + def subtype(self): + return self.document.get('subtype') + + def get_children (self, params=None): + params = params or {} + params['parent'] = self.uuid + return Collection(self.__class__, self._sdk.api.get(self._collection_path, params), self._sdk) + + def _get_delivery_url (self): + auth = self._sdk.authenticator.get_auth() + base = '{0}/api/delivery'.format(auth['api']['host']) + encoded_path = urllib.quote(self.document.get('path').encode('utf-8')) + return '{0}/{1}?_rt={2}'.format(base, encoded_path, auth['restrictedToken']) + + def get_url (self): + if self.subtype == 'scala:content:file': + return self._get_delivery_url() + elif self.subtype == 'scala:content:app': + return self._get_delivery_url() + elif self.subtype == 'scala:content:url': + return self.document.get('url') + + def get_variant_url (self, name): + return '{0}&variant='.format(self._get_delivery_url(), name) + + def has_variant (self, name): + return name in [variant['name'] for variant in self.document.get('variants', [])] + + + +class Api (object): + + Device = Device + Thing = Thing + Experience = Experience + Location = Location + Zone = Zone + Feed = Feed + Data = Data + Content = Content + + def __init__(self, sdk): + self._sdk = sdk + + def _get_url (self, path): + return '{0}{1}'.format(self._sdk.authenticator.get_auth()['api']['host'], urllib.quote(path)) + + def _get_headers (self): + return { 'Authorization': 'Bearer ' + self._sdk.authenticator.get_auth()['token'] } + + def _on_error(self, exception): + if hasattr(exception, 'response'): + try: + payload = exception.response.json() + except: + self._sdk.logger.warn('API call encountered an unexpected error.') + self._sdk.logger.debug('API call encountered an unexpected error: %s' % traceback.format_exc()) + raise self._sdk.exceptions.UnexpectedError('API call encountered an unexpected error.') + else: + raise self._sdk.exceptions.ApiError(code=payload.get('code'), message=payload.get('message'), status_code=exception.response.status_code, payload=payload) + else: + self._sdk.logger.warn('API call encountered an unexpected error.') + self._sdk.logger.debug('API call encountered an unexpected error: %s' % traceback.format_exc()) + raise self._sdk.exceptions.UnexpectedError('API call encountered an unexpected error.') + + + def get(self, path, params=None, timeout=10): + try: + response = requests.get(self._get_url(https://github.com/path), timeout=timeout, params=params, headers=self._get_headers()) + response.raise_for_status() + try: + return response.json() + except ValueError: + return None + except Exception as exception: + return self._on_error(exception) + + def post(self, path, payload=None, params=None, timeout=10): + try: + response = requests.post(self._get_url(https://github.com/path), timeout=timeout, params=params, json=payload, headers=self._get_headers()) + response.raise_for_status() + try: + return response.json() + except ValueError: + return None + except Exception as exception: + return self._on_error(exception) + + def patch(self, path, payload=None, params=None, timeout=10): + try: + response = requests.patch(self._get_url(https://github.com/path), timeout=timeout, params=params, json=payload, headers=self._get_headers()) + response.raise_for_status() + try: + return response.json() + except ValueError: + return None + except Exception as exception: + return self._on_error(exception) + + def put(self, path, payload=None, params=None, timeout=10): + try: + response = requests.put(self._get_url(https://github.com/path), timeout=timeout, params=params, json=payload, headers=self._get_headers()) + response.raise_for_status() + try: + return response.json() + except ValueError: + return None + except Exception as exception: + return self._on_error(exception) + + def delete(self, path, payload=None, params=None, timeout=10): + try: + response = requests.delete(self._get_url(https://github.com/path), timeout=timeout, params=params, json=payload, headers=self._get_headers()) + response.raise_for_status() + try: + return response.json() + except ValueError: + return None + except Exception as exception: + return self._on_error(exception) diff --git a/exp_sdk/authenticator.py b/exp_sdk/authenticator.py new file mode 100644 index 0000000..5973b57 --- /dev/null +++ b/exp_sdk/authenticator.py @@ -0,0 +1,146 @@ +import time +import traceback +import requests +import json +import base64 +import hmac +import hashlib +import threading + +from exp_sdk import exceptions + + +class Authenticator (object): + + def __init__(self, sdk): + self._sdk = sdk + self._auth = None + self._time = None + self._failed = False + self._lock = threading.Lock() + + def get_auth(self): + self._lock.acquire() + if self._failed: + self._lock.release() + raise exceptions.AuthenticationError('Invalid credentials. Please restart the SDK with valid credentials.') + try: + if not self._auth: + self._login() + elif self._time < int(time.time()): + self._refresh() + except: + self._lock.release() + raise + self._lock.release() + return self._auth + + def _login (self): + payload = {} + self._sdk.logger.debug('Login starting.') + if self._sdk.options.get('type') is 'user': + self._sdk.logger.debug('Login generating user payload.') + payload['username'] = self._sdk.options.get('username') + payload['password'] = self._sdk.options.get('password') + payload['organization'] = self._sdk.options.get('organization') + elif self._sdk.options.get('type') is 'device': + self._sdk.logger.debug('Login generating device payload.') + token_payload = {} + token_payload['uuid'] = self._sdk.options.get('uuid') or '_' + token_payload['type'] = 'device' + token_payload['allowPairing'] = self._sdk.options.get('allow_pairing') + payload['token'] = self.generate_jwt(token_payload, self._sdk.options.get('secret') or '_') + elif self._sdk.options.get('type') is 'consumer_app': + self._sdk.logger.debug('Login generating consumer app payload.') + token_payload = {} + token_payload['type'] = 'consumerApp' + token_payload['uuid'] = self._sdk.options.get('uuid') or '_' + payload['token'] = self.generate_jwt(token_payload, self._sdk.options.get('api_key')) + url = self._sdk.options.get('host') + '/api/auth/login' + self._sdk.logger.debug('Sending login payload: %s' % payload) + try: + response = requests.request('POST', url, json=payload) + except Exception as exception: + self._sdk.logger.warn('Login encountered an unexpected error.') + self._sdk.logger.debug('Login encountered an unexpected error: %s', traceback.format_exc()) + self._auth = None + self._time = None + raise exceptions.UnexpectedError('Login encountered an unexpected error.') + if response.status_code == 200: + self._sdk.logger.debug('Login request successful.') + self._on_success(response) + elif response.status_code == 401: + self._sdk.logger.critical('Invalid credentials.') + self._sdk.logger.debug('Invalid credentials.') + self._sdk.network.stop() + self._auth = None + self._time = None + self._failed = True + raise exceptions.AuthenticationError('Invalid credentials.') + else: + self._sdk.logger.warn('Login received an unexpected HTTP status code: %s.' % response.status_code) + self._sdk.logger.debug('Login received an unexpected HTTP status code: %s' % response.status_code) + self._auth = None + self._time = None + raise exceptions.UnexpectedError('Login received an unexpected HTTP status code: %s.' % response.status_code) + + def _refresh (self): + self._sdk.logger.debug('Authentication token refresh starting.') + url = self._sdk.options.get('host') + '/api/auth/token' + headers = { 'Authorization': 'Bearer ' + self._auth.get('token') } + self._sdk.logger.debug('Sending token refresh request.') + try: + response = requests.request('POST', url, headers=headers) + except Exception as exception: + self._sdk.logger.warn('Token refresh encountered an unexpected error.') + self._sdk.logger.debug('Token refresh encountered an unexpected error: %s.', traceback.format_exc()) + self._auth = None + self._time = None + raise UnexpectedError('Token refresh encountered an unexpected error.') + if response.status_code == 200: + self._sdk.logger.debug('Token refresh request successful.') + self._on_success(response) + elif response.status_code == 401: + self._sdk.logger.warn('Token refresh request failed due to expired or invalid token.') + self._sdk.logger.debug('Token refresh request failed due to expired or invalid token.') + self._auth = None + self._time = None + self._login() + else: + self._sdk.logger.warn('Token refresh request received an unexpected HTTP status code: %s.' % response.status_code) + self._sdk.logger.debug('Token refresh request receive an unexpected HTTP status code: %s' % response.status_code) + self._auth = None + self._time = None + raise exceptions.UnexpectedError('Token refresh request receive an unexpected HTTP status code: %d' % response.status_code) + + def _on_success (self, response): + self._sdk.logger.debug('Authentication update starting.') + try: + auth = response.json() + except Exception as exception: + self._sdk.logger.warn('Authentication update encountered an unexpected error.') + self._sdk.logger.debug('Authentication updated encountered an unexpected error:' % traceback.format_exc()) + self._auth = None + self._time = None + raise exceptions.UnexpectedError('Authentication update encountered an unexpected error.') + self._auth = auth + self._time = (int(time.time()) + auth['expiration'] * 3.0 / 1000.0 ) / 4.0 + self._sdk.logger.debug('Authentication update successful: %s' % auth) + + @staticmethod + def generate_jwt (payload, secret): + + algorithm = { 'alg': 'HS256', 'typ': 'JWT' } + algorithm_json = json.dumps(algorithm, separators=(',', ':')).decode('utf-8') + algorithm_b64 = base64.urlsafe_b64encode(algorithm_json).rstrip('=') + + payload['exp'] = (int(time.time()) + 30) * 1000 + payload_json = json.dumps(payload, separators=(',', ':')) + payload_b64 = base64.urlsafe_b64encode(payload_json).rstrip('=') + + signature = hmac.new(secret, '.'.join([algorithm_b64, payload_b64]), hashlib.sha256).digest() + signature_b64 = base64.urlsafe_b64encode(signature).rstrip('=') + + return '.'.join([algorithm_b64, payload_b64, signature_b64]) + + diff --git a/exp_sdk/exceptions.py b/exp_sdk/exceptions.py new file mode 100644 index 0000000..94af186 --- /dev/null +++ b/exp_sdk/exceptions.py @@ -0,0 +1,49 @@ + +import traceback +import logging + +logger = logging.getLogger('exp') + + +class ExpError (Exception): + + def __init__ (self, message): + self.message = message + + def __str__ (self): + return self.message + + +class AuthenticationError (ExpError): + pass + +class UnexpectedError (ExpError): + + def __init__ (self, *args, **kwargs): + logger.debug('An unexpected error occured:') + logger.debug(traceback.format_exc()) + super(UnexpectedError, self).__init__(*args, **kwargs) + +# Cannot execute desired action. +class RuntimeError(ExpError): + + def __init__ (self, message): + logger.debug('A runtime error has occured: %s' % message) + + def __str__ (self): + return self.message + + +class ApiError(ExpError): + + def __init__(self, code=None, message=None, status_code=None, payload=None): + self.message = message or 'An unknown error has occurred.' + self.code = code or 'unknown.error' + self.status_code = status_code + self.payload = payload + + def __str__(self): + return '%s: %s \n %s' % (self.code, self.message, self.payload) + + +class NetworkError(ExpError): pass diff --git a/exp_sdk/exp.py b/exp_sdk/exp.py new file mode 100644 index 0000000..620b7ee --- /dev/null +++ b/exp_sdk/exp.py @@ -0,0 +1,228 @@ +import signal +import sys +import logging +import traceback + +from logging.handlers import RotatingFileHandler + +from . import network +from . import authenticator +from . import api +from . import exceptions + + +""" List of all instances of Exp. """ +instances = [] + + +""" Terminate all running instances when Ctrl-C is pressed. """ +try: + signal.signal(signal.SIGINT, lambda signal, frame: stop()) +except: + pass + + +def start (enable_network=True, host='https://api.goexp.io/', **options): + + """ Validate SDK options """ + options['host'] = host + options['enable_network'] = enable_network + if options.get('type') is 'user' or ((options.get('username') or options.get('password') or options.get('organization')) and not options.get('type')): + options['type'] = 'user' + if not options.get('username'): + raise exceptions.RuntimeError('Please specify the username.') + if not options.get('password'): + raise exceptions.RuntimeError('Please specify the password.') + if not options.get('organization'): + raise exceptions.RuntimeError('Please specify the organization.') + elif options.get('type') is 'device' or ((options.get('secret') or options.get('allow_pairing')) and not options.get('type')): + options['type'] = 'device' + if not options.get('uuid') and not options.get('allow_pairing'): + raise exceptions.RuntimeError('Please specify the device uuid.') + if not options.get('secret') and not options.get('allow_pairing'): + raise exceptions.RuntimeError('Please specify the device secret.') + elif options.get('type') is 'consumer_app' or (options.get('api_key') and not options.get('type')): + options['type'] = 'consumer_app' + if not options.get('uuid'): + raise exceptions.RuntimeError('Please specify the consumer app uuid.') + if not options.get('api_key'): + raise exceptions.RuntimeError('Please specify the consumer app api key.') + else: + raise exceptions.RuntimeError('Please specify authentication type.') + + """ Generate wrapper and SDK instance """ + sdk = Sdk(**options) + if options['enable_network']: + sdk.network.start() + exp = Exp(sdk) + instances.append(exp) + exp.get_auth() + return exp + + +def stop (): + """ Stop all running SDK instances. """ + [exp.stop() for exp in instances[:]] + + +class Sdk (object): + """ Wrapper to hold SDK modules. """ + + def __init__(self, **options): + self.options = options + self.authenticator = authenticator.Authenticator(self) + self.api = api.Api(self) + self.network = network.Network(self) + self.logger = logging.getLogger('exp-sdk') + self.exceptions = exceptions + + +class Exp (object): + + def __init__(self, sdk): + self._sdk_ = sdk + + @property + def _sdk(self): + if not self._sdk_: + raise exceptions.RuntimeError('This SDK instance is stopped.') + return self._sdk_ + + + """ Runtime """ + + def stop (self): + if self in instances: + instances.remove(self) + self._sdk.network.stop() + self._sdk_ = None # Remove internal references to SDK modules. All calls will now throw runtime error. + + def get_auth (self): + return self._sdk.authenticator.get_auth() + + + """ Naked API """ + + def get (self, *args, **kwargs): + return self._sdk.api.get(*args, **kwargs) + + def post (self, *args, **kwargs): + return self._sdk.api.post(*args, **kwargs) + + def patch (self, *args, **kwargs): + return self._sdk.api.patch(*args, **kwargs) + + def put (self, *args, **kwargs): + return self._sdk.api.put(*args, **kwargs) + + def delete (self, *args, **kwargs): + return self._sdk.api.delete(*args, **kwargs) + + + """ Network """ + + @property + def is_connected(self): + return self._sdk.network.is_connected + + def get_channel(self, *args, **kwargs): + return self._sdk.network.get_channel(*args, **kwargs) + + + """ API Resources """ + + def get_device (self, uuid=None): + return self._sdk.api.Device.get(uuid, self._sdk) + + def get_current_device(self): + return self._sdk.api.Device.get_current(self._sdk) + + def find_devices (self, params=None): + return self._sdk.api.Device.find(params, self._sdk) + + def create_device (self, document=None): + return self._sdk.api.Device.create(document, self._sdk) + + def delete_device (self, uuid=None): + return self._sdk.api.Device.delete_(uuid, self._sdk) + + + def get_thing (self, uuid=None): + return self._sdk.api.Thing.get(uuid, self._sdk) + + def find_things (self, params=None): + return self._sdk.api.Thing.find(params, self._sdk) + + def create_thing (self, document=None): + return self._sdk.api.Thing.create(document, self._sdk) + + def delete_thing (self, uuid=None): + return self._sdk.api.Thing.delete_(uuid, self._sdk) + + + def get_experience (self, uuid=None): + return self._sdk.api.Experience.get(uuid, self._sdk) + + def get_current_experience(self): + return self._sdk.api.Experience.get_current(self._sdk) + + def find_experiences (self, params=None): + return self._sdk.api.Experience.find(params, self._sdk) + + def create_experience (self, document=None): + return self._sdk.api.Experience.create(document, self._sdk) + + def delete_experience (self, uuid=None): + return self._sdk.api.Experience.delete_(uuid, self._sdk) + + + def get_location (self, uuid=None): + return self._sdk.api.Location.get(uuid, self._sdk) + + def get_current_location(self): + return self._sdk.api.Location.get_current(self._sdk) + + def get_current_zones(self): + return self._sdk.api.Zone.get_current(self._sdk) + + def find_locations (self, params=None): + return self._sdk.api.Location.find(params, self._sdk) + + def create_location (self, document=None): + return self._sdk.api.Location.create(document, self._sdk) + + def delete_location (self, uuid=None): + return self._sdk.api.Location.delete_(uuid, self._sdk) + + + def get_feed (self, uuid=None): + return self._sdk.api.Feed.get(uuid, self._sdk) + + def find_feeds (self, params=None): + return self._sdk.api.Feed.find(params, self._sdk) + + def create_feed (self, document=None): + return self._sdk.api.Feed.create(document, self._sdk) + + def delete_feed (self, uuid=None): + return self._sdk.api.Feed.delete_(uuid, self._sdk); + + + def get_data (self, group='default', key=None): + return self._sdk.api.Data.get(group, key, self._sdk) + + def find_data (self, params=None): + return self._sdk.api.Data.find(params, self._sdk) + + def create_data (self, group=None, key=None, value=None): + return self._sdk.api.Data.create(group, key, value, self._sdk) + + def delete_data(self, group=None, key=None): + return self._sdk.api.Data.delete_(group, key, self._sdk) + + + def get_content (self, uuid=None): + return self._sdk.api.Content.get(uuid, self._sdk) + + def find_content (self, params=None): + return self._sdk.api.Content.find(params, self._sdk) diff --git a/exp_sdk/network.py b/exp_sdk/network.py new file mode 100644 index 0000000..5e7504e --- /dev/null +++ b/exp_sdk/network.py @@ -0,0 +1,305 @@ +import time +import threading +import uuid + + +import urlparse +from socketIO_client import SocketIO, BaseNamespace +import json +import base64 + +import traceback + + +class _Broadcast (object): + + def __init__(self, sdk, message): + self._sdk = sdk + self._message = message + self.time = int(time.time()) + + @property + def payload (self): + return json.loads(json.dumps(self._message['payload'])) + + def respond(self, response): + self._sdk.api.post('/api/networks/current/responses', {'id': self._message['id'], 'channel': self._message['channel'], 'payload': response }) + + +class _Listener (object): + + def __init__(self, namespace, sdk, max_age=60, **kwargs): + self._namespace = namespace + self._sdk = sdk + self._max_age = max_age + self._event = threading.Event() + self._broadcasts = [] + + def _prune (self): + now = int(time.time()) + self._broadcasts = [broadcast for broadcast in self._broadcasts if now - broadcast.time < self._max_age] + + def receive (self, message): + self._prune() + self._broadcasts.append(_Broadcast(self._sdk, message)) + self._event.set() + + def wait (self, timeout=0, **kwargs): + self._prune() + if not self._broadcasts: + self._event.clear() + self._event.wait(timeout) + if self._broadcasts: + broadcast = self._broadcasts.pop(0) + return broadcast + + def cancel(self): + self._namespace.cancel_listener(self) + + +class _Namespace (object): + + def __init__(self, sdk): + self._listeners = [] + self._sdk = sdk + + def listen (self, **kwargs): + listener = _Listener(self, self._sdk, **kwargs) + self._listeners.append(listener) + return listener + + def cancel_listener(self, listener): + if listener in self._listeners: + self._listeners.remove(listener) + + def receive (self, message): + [listener.receive(message) for listener in self._listeners] + + @property + def has_listeners (self): + return bool(self._listeners) + + +class _Channel (object): + + def __init__ (self, id, sdk): + self._id = id + self._sdk = sdk + self._namespaces = {} + self.subscription = threading.Event() + + def broadcast (self, name, payload=None, timeout=0.1): + path = '/api/networks/current/broadcasts' + payload = {'channel': self._id, 'name': name, 'payload': payload} + params = {'timeout': timeout * 1000 } + return self._sdk.api.post(path, payload, params) + + def listen (self, name, timeout=10, **kwargs): + if not self._namespaces.get(name): + self._namespaces[name] = _Namespace(self._sdk) + listener = self._namespaces[name].listen(**kwargs) + if not self.subscription.is_set(): + self._sdk.network.emit('subscribe', [self._id]) + if not self.subscription.wait(timeout): + raise self._sdk.exceptions.NetworkError('Listen timed out.') + return listener + + def receive (self, message): + if message['name'] in self._namespaces: + self._namespaces[message['name']].receive(message) + + @property + def has_listeners (self): + return any([namespace.has_listeners for name, namespace in self._namespaces.iteritems()]) + + + + + + + + + +class Network (object): + + def __init__(self, sdk): + self._sdk = sdk + self._channels = {} + self._auth = None + self._socket = None + self._abort = False + self._parent = threading.currentThread() + self._thread = threading.Thread(target=lambda: self._main_event_loop()) + self._lock = threading.Lock() + + @property + def is_connected (self): + return self._socket and self._socket.is_connected + + def start (self): + self._thread.start() + + def stop (self): + self._abort = True + + def get_channel(self, name, system=False, consumer=False): + channel_id = self._generate_channel_id(name, system, consumer) + channel = self._get_channel_by_id(channel_id) + return channel + + def emit (self, name, payload): + if not self._socket: + return False + return self._socket.emit(name, payload) + + def _get_channel_by_id (self, channel_id): + if channel_id not in self._channels: + self._channels[channel_id] = _Channel(channel_id, self._sdk) + return self._channels[channel_id] + + def _generate_channel_id (self, name, system=False, consumer=False): + organization = self._sdk.authenticator.get_auth()['identity']['organization'] + raw_id = [organization, name, 1 if system else 0, 1 if consumer else 0] + json_id = json.dumps(raw_id, separators=(',', ':')) + return base64.b64encode(json_id) + + def on_connect(self, socket): + if socket != self._socket: + return + [channel.subscription.clear() for id, channel in self._channels.iteritems()] + self._socket.emit('subscribe', [id for id, channel in self._channels.iteritems() if channel.has_listeners]) + + def on_disconnect(self, socket): + if socket != self._socket: + return + [channel.subscription.clear() for id, channel in self._channels.iteritems()] + + def on_subscribed(self, socket, ids): + if socket != self._socket: + return + [self._get_channel_by_id(id).subscription.set() for id in ids] + + def on_broadcast (self, socket, message): + if socket != self._socket: + return + self._get_channel_by_id(message['channel']).receive(message) + + + def _main_event_loop (self): + + while True: + + """ If parent thread is dead or network is inactive, disconnect and break thread. """ + if not self._parent.is_alive() or self._abort: + if self._socket: + self._socket.stop() + self._socket = None + break + + """ Attempt to retrieve current auth. """ + try: + auth = self._sdk.authenticator.get_auth() + except Exception: + self._auth = None + time.sleep(1) + continue + + """ Disconnect if auth has changed. """ + if auth != self._auth: + self._auth = auth + if self._socket: + self._socket.stop() + self._socket = None + + """ Do nothing if there is no auth. """ + if not self._auth: + time.sleep(1) + continue + + """ Start the socket if not started. """ + if not self._socket: + try: + self._socket = _Socket(self._sdk) + self._socket.start(**self._auth) + except: + self._sdk.logger.warning('Socket failed to start.') + self._sdk.logger.warning('Socket failed to start: %s', traceback.format_exc()) + self._socket = None + time.sleep(1) + continue + + """ Listen for socket events. """ + try: + self._socket.wait(1) + except: + self._sdk.logger.warning('Socket failed to wait for messages.') + self._sdk.logger.warning('Socket failed to wait for messages: %s', traceback.format_exc()) + time.sleep(1) + + + + + +class _Socket (object): + + def __init__(self, sdk): + self._sdk = sdk + self._socket = None + + def start (self, **auth): + + + class Namespace (BaseNamespace): + + def on_broadcast (self, message): + self.socket._sdk.network.on_broadcast(self.socket, message) + + def on_connect (self): + self.socket._sdk.network.on_connect(self.socket) + + def on_disconnect (self): + self.socket._sdk.network.on_disconnect(self.socket) + + def on_subscribed (self, message): + self.socket._sdk.network.on_subscribed(self.socket, message) + + Namespace.socket = self + + params = { 'token': auth['token'] } + self._socket = SocketIO(auth['network']['host'], Namespace=Namespace, params=params, wait_for_connection=False, hurry_interval_in_seconds=5) + + def stop (self): + self._sdk.logger.debug('Disconnecting from network.') + try: + self._socket.disconnect() + self._sdk.logger.debug('Disconnected from network') + except: + self._sdk.logger.warning('Failed to disconnect from network.') + self._sdk.logger.debug('Failed to disconnect from network: %s', traceback.format_exc()) + + def wait (self, seconds): + if not self.is_connected: + return time.sleep(seconds) + try: + self._socket.wait(seconds) + except: + self._sdk.logger.warning('Failed to wait for socket messages.') + self._sdk.logger.debug('Failed to wait for socket messages: %s', traceback.format_exc()) + time.sleep(seconds) + + def emit (self, name, payload): + if not self.is_connected: + self._sdk.logger.debug('Failed to emit socket message: Device is offline.') + return False + try: + self._sdk.logger.debug('Emitting socket message: %s,%s', name, payload) + self._socket.emit(name, payload) + return True + except: + self._sdk.logger.warning('Failed to emit socket message.') + self._sdk.logger.debug('Failed to emit socket message: %s', traceback.format_exc()) + return False + + @property + def is_connected (self): + return self._socket and self._socket.connected diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py index c8e95ee..6b4bb08 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,18 @@ - -from setuptools import setup, find_packages +from setuptools import setup setup( - name="scala-sdk", - version="0.0.0", - packages=find_packages(), - install_requires=["requests", "socketIO_client"] + name='exp-sdk', + packages= ['exp_sdk'], + version='1.0.7', + description='EXP Python SDK', + author='Scala', + author_email='james.dalessio@scala.com', + url='https://github.com/scalainc/exp-python2-sdk', + download_url='https://github.com/scalainc/exp-python2-sdk/tarball/1.0.7', + install_requires=["requests", "socketIO_client"], + license='MIT', + keywords=['scala', 'exp', 'sdk', 'signage'], + classifiers=[ + 'Programming Language :: Python :: 2' + ] ) - diff --git a/exp/lib/__init__.py b/tests/__init__.py similarity index 100% rename from exp/lib/__init__.py rename to tests/__init__.py diff --git a/tests/enterpriceSDKTestScript/RssReader.sca b/tests/enterpriceSDKTestScript/RssReader.sca new file mode 100644 index 0000000..3892573 --- /dev/null +++ b/tests/enterpriceSDKTestScript/RssReader.sca @@ -0,0 +1,77 @@ +!ScalaScript1100 +// Saved by Scala Designer Release 11.00.06 at 2016-04-07 17:20:01 +:"RssReader.sca" +{ + Group: + Template Integer(pageDuration(20)); + Template String(uuid("110c7cba-f12c-43cf-ab62-e525b8de7f68"), api_key("682e0a9d341b783c6856c4bf8f4f741c08f6251b641aaeec165e052efba7fbd164f0027f5ca3e310b38c247021919d64"), host("http://192.168.168.38:9000/"), feed_uuid("f14fb720-6a66-4b26-b38d-283a724695d9")); + String(info.source("http://rss.cnn.com/rss/edition_world.rss"), info.lastBuildDate("2016-04-06T14:43:54.000Z")); + Integer(info.maxResults(100)); + String(info.name("CNN.com - World")); + Boolean(item.exists[9]); + String(item.title[10]("How Reagan never left the campaign trail", "Death penalty: 2015 a troubling year", "Putin and the Panama Papers", "It moves, it glows: Vhils' ode to the neon city", "Celebrating 100 years of Bavarian beauty from BMW", "Joining the 'gold rush' with Italy's Tuscan truffle hunters", "Female Sumatran rhino dies weeks after rare sighting", "Nigeria plans to send an astronaut to space by 2030", "World's first dengue fever vaccine launched in the Philippines", "Then and now: The home of the Masters before it was famous"), item.text[10]("Ronald Reagan may have left office in 1989 but, to listen to Republican presidential candidates since, it's like he never left.", "Countries that still execute need to realize that they are on the wrong side of history, writes Salil Shetty.", "The Kremlin introduced a new word into the English language Monday: ^"Putinophobia.^"", "Whether it's blowing up a building façade in Berlin to reveal a carving of a man's face or drilling portraits into favela walls in Rio de Janeiro, raucous street artist Alexandre Farto, who goes by the tag ^"Vhils,^" has left an imprint on urban landscapes across the globe.", "This month ^"the ultimate driving machine^" passes an important milestone: BMW, aka Bavarian Motor Works, is turning 100.", "On a misty Italian morning, we crunch our way through green-yellow undergrowth as Pepe the dog darts on ahead, nose close to the sandy soil.", "The optimistic story of a Sumatran rhino took a dark turn when the mammal died weeks after a rare sighting.", "Nigeria has announced plans to send an astronaut into space by 2030, as part of its drive to develop a world-class space industry.", "Dengue fever infects 390 million people each year, and kills as many as 25,000, according to the World Health Organization.", "It's one of the most iconic venues in sport but Augusta National Golf Club -- home of the Masters -- hasn't always been a pristine golfer's paradise."), item.date[10]("2016-04-06T14:43:26.000Z", "2016-04-06T14:34:05.000Z", "2016-04-06T14:33:42.000Z", "2016-04-06T14:00:23.000Z", "2016-04-06T14:00:08.000Z", "2016-04-06T13:59:42.000Z", "2016-04-06T13:58:43.000Z", "2016-04-06T13:58:12.000Z", "2016-04-06T13:53:29.000Z", "2016-04-06T13:52:29.000Z")); + FileNameString(item.image[10]("content:\rss\121102125319-41-ronald-reagan-president-top-tease.jpg", "content:\rss\160406083051-amnesty-death-penalty-report-2015-tease-top-tease.jpg", "content:\rss\160404174156-putin-panama-papers-lklv-chance-00001517-top-tease.jpg", "content:\rss\160404090216-vhils-street-view-top-tease.jpg", "content:\rss\160404173013-bmw-slide-3-top-tease.jpg", "content:\rss\151210180650-truffles-balcony-top-tease.jpg", "content:\rss\160323212452-03-sumatran-rhino-top-tease.jpg", "content:\rss\160406100823-nigcom-sat-1-launch-top-tease.jpg", "content:\rss\150731180818-aedes-aegypti-top-tease.jpg", "content:\rss\160406114700-augusta-slider-tease-top-tease.jpg")); + Integer(count, loop, index, totalCount); + String(EXPmessage); + BackgroundSettings(Size(1920, 1080)); + Config.RecentPublishLocations(PublishLocation("localhost ContentManager", "RssReader")); + Sequence: + :"EXPlisten" + WindowsScript("RssReader\expmessage.py", Engine("Python.AXScript.2"), ShareVariable(EXPmessage), ShareVariable(api_key), ShareVariable(uuid), ShareVariable(host)); + :"start" + { + } + :"rssReader" + WindowsScript("RssReader\exprss.py", Wait(On), Engine("Python.AXScript.2"), ShareVariable(uuid), ShareVariable(api_key), ShareVariable(host), ShareVariable(feed_uuid), ShareVariable(EXPmessage)); + { + Group: + XMLFile("Content:\rss\rss_data.xml", MapData("/rss/info", DataVariable(info.source, "source"), DataVariable(info.lastBuildDate, "lastBuildDate"), DataVariable(info.maxResults, "maxResults"), DataVariable(info.name, "name")), MapRepeatingData("/rss/items/item", NumRecords(10), MaxLoops(0), StepSize(1), CurrentCountVariable(count), CurrentIndexVariable(index), CurrentLoopVariable(loop), TotalCountVariable(totalCount), DataExistsVariable(item.exists), DataVariable(item.title, "title"), DataVariable(item.text, "text"), DataVariable(item.date, "date"), DataVariable(item.image, "image"))); + Sequence: + { + Group: + Picture("RssReader\news.jpg", Wipe("Dissolve", Duration(1000), Direction(90)), Backdrop(Pen(1)), Margin(10, 10, 0, 0), UserPalette(RGBPen(1, $0, $ffffff, $999999, $555555, $712068, $df449c, $dc110e, $662200, $ff5a00, $ff9c00, $ffee00, $8800, $dd00, $cccc, $66ff, $aa, $777777, $bbbbbb, $dddddd, $465a96)), AutoScale(FillAndTrim), Operation(On)); + Text(1060, 494, info.name, Shadow(Off, Softness(5)), AntiAlias(On), OnReplay(Replace), Under(Off, Thickness(3)), Font("Calibri (Western [])", 50), Tabs(Relative(On)), Wrap(On, Width(1890))); + HardDuration(5000); + If(index<1); + } + :"newsPage" + { + Group: + Picture("RssReader\background.jpg", Wipe("Dissolve", Duration(1000), Direction(90)), Backdrop(Pen(2)), Margin(10, 10, 0, 0), UserPalette(RGBPen(1, $0, $ffffff, $999999, $555555, $712068, $df449c, $dc110e, $662200, $ff5a00, $ff9c00, $ffee00, $8800, $dd00, $cccc, $66ff, $aa, $777777, $bbbbbb, $dddddd, $465a96)), AutoScale(FillAndTrim), Operation(On)); + HardDuration(pageDuration*1000); + Sequence: + { + Group: + Text(550, 333, item.title[0], Wipe("ShorterFade", Duration(401), Direction(0), Wait(Off)), Outline(Off, Pen(2)), Shadow(Off, Softness(5), Pen(2)), AntiAlias(On), Update(None), OnReplay(Replace), Under(Off, Thickness(3)), Font("Calibri (Western [])", 50), Bold(On), Tabs(Relative(On)), Wrap(Off, Width(864))); + HardDuration(0); + } + { + Group: + Text(991, 760, "!EXPmessage", Wipe("ShorterFade", Duration(401), Direction(0), Wait(Off)), Outline(Off, Pen(2)), Shadow(Off, Softness(5), Pen(2)), AntiAlias(On), OnReplay(Replace), Under(Off, Thickness(3)), Font("Calibri (Western [])", 35), Bold(On), Tabs(Relative(On)), Wrap(On, Width(891))); + HardDuration(0); + } + { + Group: + Text(550, 760, info.name, Wipe("ShorterFade", Duration(401), Direction(0), Wait(Off)), Outline(Off, Pen(2)), Shadow(Off, Softness(5), Pen(2)), AntiAlias(On), Update(None), OnReplay(Replace), Under(Off, Thickness(3)), Font("Calibri (Western [])", 35), Bold(On), Tabs(Relative(On)), Wrap(On, Width(1678))); + HardDuration(0); + } + { + Group: + Text(860, 429, item.date[0], Wipe("ShorterFade", Duration(401), Direction(0), Wait(Off)), Outline(Off, Pen(2)), Shadow(Off, Softness(5), Pen(2)), AntiAlias(On), Update(None), OnReplay(Replace), Under(Off, Thickness(3)), Font("Calibri (Western [])", 35), Italic(On), Tabs(Relative(On)), Wrap(On, Width(1890))); + HardDuration(0); + } + { + Group: + Text(860, 473, item.text[0], Wipe("ShorterFade", Duration(401), Direction(0), Wait(Off)), Outline(Off, Pen(2)), Shadow(Off, Softness(5), Pen(2)), AntiAlias(On), Update(None), OnReplay(Replace), Under(Off, Thickness(3)), Font("Calibri (Western [])", 50), Tabs(Relative(On)), Wrap(On, Width(1003))); + HardDuration(0); + } + { + Group: + Clip(550, 429, item.image[0], Wipe("ShorterFade", Duration(401), Direction(0), Wait(Off)), Transparent(Off), Update(None), OnReplay(Replace), AutoScale(FillAndTrim), Operation(On, Resize(276, 276))); + HardDuration(0); + } + } + } + :"end" + Goto("start"); +} diff --git a/tests/enterpriceSDKTestScript/RssReader/background.jpg b/tests/enterpriceSDKTestScript/RssReader/background.jpg new file mode 100644 index 0000000..41e964e Binary files /dev/null and b/tests/enterpriceSDKTestScript/RssReader/background.jpg differ diff --git a/tests/enterpriceSDKTestScript/RssReader/expmessage.py b/tests/enterpriceSDKTestScript/RssReader/expmessage.py new file mode 100644 index 0000000..3c85791 --- /dev/null +++ b/tests/enterpriceSDKTestScript/RssReader/expmessage.py @@ -0,0 +1,40 @@ +import exp_sdk +import scala5 +import scalalib +from scalalib import sharedvars + +scalaVars = sharedvars() +scala5.ScalaPlayer.Log('Starting EXP message listen') + +try: + # authentication + exp = exp_sdk.start(uuid=scalaVars.uuid, api_key=scalaVars.api_key, host=scalaVars.host) + + # Wait for a connection. + while not exp.is_connected: + scalalib.sleep(1000) + + # setup channel + channel = exp.get_channel('scala-test-channel', consumer=True) + listener = channel.listen('my-message', max_age=30) + + # listen to message + while True: + broadcast = listener.wait() + if broadcast: + scala5.ScalaPlayer.Log('Message received') + scalaVars.EXPmessage = broadcast.payload + scala5.ScalaPlayer.Log('Received message: ' + broadcast.payload) + broadcast.respond('Message received thank you!') + scalalib.sleep(1000) + + exp.stop() +except exp_sdk.ExpError or exp_sdk.UnexpectedError: + scala5.ScalaPlayer.LogExternalError(1000, 'ExpError', 'Error opening channel to EXP') +except exp_sdk.RuntimeError: + scala5.ScalaPlayer.LogExternalError(1000, 'RuntimeError', 'Please check start options of EXP SDK') +except exp_sdk.AuthenticationError: + scala5.ScalaPlayer.LogExternalError(1000, 'AuthenticationError', + 'Unable to connect to EXP, please check credentials') +except exp_sdk.ApiError: + scala5.ScalaPlayer.LogExternalError(1000, 'ApiError', exp_sdk.ApiError.message) diff --git a/tests/enterpriceSDKTestScript/RssReader/exprss.py b/tests/enterpriceSDKTestScript/RssReader/exprss.py new file mode 100644 index 0000000..c729bcf --- /dev/null +++ b/tests/enterpriceSDKTestScript/RssReader/exprss.py @@ -0,0 +1,128 @@ +# import libraries +import exp_sdk +import scalalib +import scalatools +from scalalib import sharedvars +import scala5 +import xml.etree.ElementTree as ET +from xml.etree.ElementTree import Element, SubElement, Comment +import tempfile +import os +import re +import urllib + +scalaVars = sharedvars() +scala5.ScalaPlayer.Log('Starting EXP data sync') + + +# download image files +def download_file_from_net(url, filename): + try: + temp_dir = tempfile.gettempdir() + save_dir = os.path.join(temp_dir, filename) + image = urllib.URLopener() + image.retrieve(url, save_dir) + # install to the content folder + scalalib.install_content(save_dir, subfolder='rss', autostart=False) + scala5.ScalaPlayer.Log('Image ' + filename + ' downloaded from ' + url) + except IOError as (errno, strerror): + scala5.ScalaPlayer.LogExternalError(1000, 'I/O error({0})'.format(errno), strerror) + + +# get exp data +def get_rss_data(): + feed_data = None + + try: + # authentication + exp = exp_sdk.start(uuid=scalaVars.uuid, api_key=scalaVars.api_key, host=scalaVars.host) + # get feed data from EXP + feed = exp.get_feed(uuid=scalaVars.feed_uuid) + feed_data = feed.get_data() + # stop connection + exp.stop() + scala5.ScalaPlayer.Log('Connection to EXP successful data downloaded') + except exp_sdk.ExpError or exp_sdk.UnexpectedError: + scala5.ScalaPlayer.LogExternalError(1000, 'ExpError', 'Error downloading data from EXP') + except exp_sdk.RuntimeError: + scala5.ScalaPlayer.LogExternalError(1000, 'RuntimeError', 'Please check start options of EXP SDK') + except exp_sdk.AuthenticationError: + scala5.ScalaPlayer.LogExternalError(1000, 'AuthenticationError', + 'Unable to connect to EXP, please check credentials') + except exp_sdk.ApiError: + scala5.ScalaPlayer.LogExternalError(1000, 'ApiError', exp_sdk.ApiError.message) + + return feed_data + + +# save jSon to XML +def json2xml(json_obj): + top = Element('rss') + info = SubElement(top, 'info') + rss_source = SubElement(info, 'source') + rss_source.text = json_obj['search']['search'] + rss_build_date = SubElement(info, 'lastBuildDate') + rss_build_date.text = json_obj['details']['lastBuildDate'] + rss_max_items = SubElement(info, 'maxResults') + rss_max_items.text = str(json_obj['search']['maxResults']) + rss_source_name = SubElement(info, 'name') + rss_source_name.text = json_obj['details']['name'] + rss_items = SubElement(top, 'items') + + for item in json_obj['items']: + rss_item = SubElement(rss_items, 'item') + rss_item_title = SubElement(rss_item, 'title') + rss_item_title.text = item['raw']['title'][0] + rss_item_text = SubElement(rss_item, 'text') + rss_item_text.text = re.sub('<[^<]+?>', '', item['text']) + rss_item_text.text = re.sub('&nbps;', '', rss_item_text.text) + rss_item_date = SubElement(rss_item, 'date') + rss_item_date.text = item['date'] + + if (len(item['images']) > 0): + url = item['images'][0]['url'] + filename = item['images'][0]['url'].split('/')[-1] + rss_item_file = SubElement(rss_item, 'image') + rss_item_file.text = 'content:\\rss\\' + filename + download_file_from_net(url, filename) + elif (len(item['raw']['media:content']) > 0): + url = item['raw']['media:content'][0]['$']['url'] + filename = item['raw']['media:content'][0]['$']['url'].split('/')[-1] + rss_item_file = SubElement(rss_item, 'image') + rss_item_file.text = 'content:\\rss\\' + filename + download_file_from_net(url, filename) + elif (len(item['raw']['media:thumbnail']) > 0): + url = item['raw']['media:thumbnail'][0]['$']['url'] + filename = item['raw']['media:thumbnail'][0]['$']['url'].split('/')[-1] + rss_item_file = SubElement(rss_item, 'image') + rss_item_file.text = 'content:\\rss\\' + filename + download_file_from_net(url, filename) + else: + rss_item_file = SubElement(rss_item, 'image') + rss_item_file.text = '' + + return ET.tostring(top, 'utf-8') + + +# save xml data to file +def save_data(xml_data): + try: + temp_dir = tempfile.gettempdir() + save_dir = os.path.join(temp_dir, 'rss_data.xml') + file_ = open(save_dir, 'w') + file_.write(xml_data) + file_.close() + # install to the content folder + scalalib.install_content(save_dir, subfolder='rss', autostart=True) + scala5.ScalaPlayer.Log('XML rss data saved in player content directory') + except IOError as (errno, strerror): + scala5.ScalaPlayer.LogExternalError(1000, 'I/O error({0})'.format(errno), strerror) + + +# main program +rss_data = None +rss_data = get_rss_data() +if rss_data: + xml_data = json2xml(rss_data) + save_data(xml_data) +scala5.ScalaPlayer.Log('EXP data sync ready') diff --git a/tests/enterpriceSDKTestScript/RssReader/news.jpg b/tests/enterpriceSDKTestScript/RssReader/news.jpg new file mode 100644 index 0000000..2368ba3 Binary files /dev/null and b/tests/enterpriceSDKTestScript/RssReader/news.jpg differ diff --git a/tests/enterpriceSDKTestScript/sendMessage.py b/tests/enterpriceSDKTestScript/sendMessage.py new file mode 100644 index 0000000..2f834ae --- /dev/null +++ b/tests/enterpriceSDKTestScript/sendMessage.py @@ -0,0 +1,26 @@ +# import libraries +import exp_sdk +import time +import datetime + +exp_uuid = '110c7cba-f12c-43cf-ab62-e525b8de7f68' +exp_api_key = '682e0a9d341b783c6856c4bf8f4f741c08f6251b641aaeec165e052efba7fbd164f0027f5ca3e310b38c247021919d64' +exp_host = 'http://192.168.168.38:9000/' +now = datetime.datetime.now() + +# authentication +exp = exp_sdk.start(uuid=exp_uuid, api_key=exp_api_key, host=exp_host) + +# Wait for a connection. +while not exp.is_connected: + time.sleep(1) + +# setup channel +channel = exp.get_channel('scala-test-channel', consumer=True) +responses = channel.broadcast('my-message', 'this message is send @' + now.isoformat()) + +# print response +for response in responses: + print responses + +exp.stop() diff --git a/tests/test0.py b/tests/test0.py deleted file mode 100644 index 21eae2f..0000000 --- a/tests/test0.py +++ /dev/null @@ -1,10 +0,0 @@ -import unittest -import exp - -class TestBoilerplate(unittest.TestCase): - - def test_something(self): - self.assertEqual(0, 0) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..d2898e2 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,30 @@ +import utils +import random +import string + +class Test (utils.Device): + + def test_get (self): + self.exp.get('/api/devices/' + self.device_credentials.get('uuid')) + + def test_post (self): + self.exp.post('/api/experiences', {}) + + def test_patch (self): + document = self.exp.post('/api/experiences', {}) + self.exp.patch('/api/experiences/' + document['uuid'], {}) + + def test_put (self): + self.exp.put('/api/data/test1/test2', { 'a': 1 }) + + def test_delete (self): + document = self.exp.post('/api/experiences', {}) + self.exp.delete('/api/experiences/' + document['uuid']) + + def test_post_error (self): + name = ''.join(random.choice(string.lowercase) for i in range(10)) + self.exp.post('/api/experiences', { 'name': name }) + try: + self.exp.post('/api/experiences', { 'name': name }) + except self.exp_sdk.ApiError: + pass \ No newline at end of file diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..1379f46 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,46 @@ +import unittest +import requests +import time + +from . import utils + + +class AuthBase (object): + + + def test_authentication (self): + self.exp.get_auth() + + def test_token_refresh (self): + self.exp._sdk.authenticator._login() + self.exp._sdk.authenticator._refresh() + + def test_refresh_401 (self): + auth = self.exp.get_auth() + auth['token'] = auth['token'] + 'blah' + self.exp._sdk.options['uuid'] = 'blah' + self.exp._sdk.options['username'] = 'blah' + try: + self.exp._sdk.authenticator._refresh() + except self.exp_sdk.AuthenticationError: + pass + else: + raise Exception + + + +class TestDeviceAuth (AuthBase, utils.Device, unittest.TestCase): pass +class TestUserAuth (AuthBase, utils.User, unittest.TestCase): pass +class TestConsumerAuth (AuthBase, utils.Consumer, unittest.TestCase): pass + + +class TestDevice401 (utils.Base, unittest.TestCase): + + def test_login_401 (self): + self.device_credentials['uuid'] = 'wrong uuid' + try: + exp = self.exp_sdk.start(**self.device_credentials) + except self.exp_sdk.AuthenticationError: + pass + else: + raise Exception diff --git a/tests/test_content.py b/tests/test_content.py new file mode 100644 index 0000000..f165d74 --- /dev/null +++ b/tests/test_content.py @@ -0,0 +1,85 @@ + +import unittest + +from . import utils + +class Test(utils.Device, utils.CommonResourceBase): + + get_name = 'get_content' + find_name = 'find_content' + creatable = False + savable = False + class_ = utils.api.Content + + def create(self, _=None): + return self.exp.find_content()[0] + + def create_valid (self): + return self.create() + + def test_subtype (self): + items = self.exp.find_content() + for item in items: + if not item.subtype: + raise Exception + + def test_get_url_url (self): + items = self.exp.find_content({ 'subtype': 'scala:content:url' }) + for item in items: + if item.subtype != 'scala:content:url': + raise Exception + if not items or items.total == 0: + raise Exception('No url content items found for testing.') + if not items[0].get_url(): + raise Exception + + def test_has_variant (self): + items = self.exp.find_content() + for item in items: + variants = item.document.get('variants', []) + if variants: + for variant in variants: + if not item.has_variant(variant['name']): + raise Exception + if item.has_variant('not a variant'): + raise Exception + + def test_get_url_file (self): + items = self.exp.find_content({ 'subtype': 'scala:content:file' }) + for item in items: + if item.subtype != 'scala:content:file': + raise Exception + if not items or items.total == 0: + raise Exception('No file content items found for testing.') + if not items[0].get_url(): + raise Exception + + def test_get_url_app (self): + items = self.exp.find_content({ 'subtype': 'scala:content:app' }) + for item in items: + if item.subtype != 'scala:content:app': + raise Exception + if not items or items.total == 0: + raise Exception('No file content items found for testing.') + if not items[0].get_url(): + raise Exception + + + def test_get_variant_url (self): + items = self.exp.find_content() + for item in items: + if not item.get_variant_url('https://github.com/test_variant'): + raise Exception + + + def test_children (self): + folders = self.exp.find_content({ 'subtype': 'scala:content:folder' }) + if folders.total == 0: + raise Exception + for folder in folders: + children = folder.get_children({ 'subtype': 'scala:content:folder' }) + for child in children: + if child.subtype != 'scala:content:folder': + raise Exception + if not children.total and children.total != 0: + raise Exception diff --git a/tests/test_data.py b/tests/test_data.py new file mode 100644 index 0000000..6afd773 --- /dev/null +++ b/tests/test_data.py @@ -0,0 +1,52 @@ + +import unittest + +from . import utils + +class Test(utils.Device, utils.ResourceBase): + + get_name = 'get_data' + find_name = 'find_data' + create_name = 'create_data' + class_ = utils.api.Data + + def create_valid (self): + return self.create(group=self.generate_name(), key=self.generate_name(), value={ 'test': self.generate_name() }) + + def create (self, group=None, key=None, value=None): + return self.exp.create_data(group, key, value) + + def test_find (self): + data = self.create_valid() + [self.assert_isinstance(data) for data in self.find()] + items = self.find({ 'group': data.group }) + if not data.key in [item.key for item in items]: + raise Exception + self.assert_isinstance(items[0]) + + def test_create (self): + data = self.exp.create_data('cats', 'fluffy', { 't': 'meow1' }) + if data.key != 'fluffy' or data.group != 'cats' or data.value['t'] != 'meow1': + raise Exception + + def test_update (self): + data = self.create_valid() + data.value = { 'test2': 'a' } + data.save() + data = self.exp.get_data(data.group, data.key) + if data.value['test2'] != 'a': + raise Exception + + def test_delete (self): + data = self.create_valid() + key = data.key + group = data.group + data.delete() + if self.exp.get_data(group, key): + raise Exception + data = self.create_valid() + key = data.key + group = data.group + self.exp.delete_data(group, key) + if self.exp.get_data(group, key): + raise Exception diff --git a/tests/test_devices.py b/tests/test_devices.py new file mode 100644 index 0000000..16c62c8 --- /dev/null +++ b/tests/test_devices.py @@ -0,0 +1,61 @@ + +import unittest + +from . import utils + +class Test(utils.Device, utils.CommonResourceBase): + + get_name = 'get_device' + find_name = 'find_devices' + create_name = 'create_device' + class_ = utils.api.Device + + def test_get_location (self): + device = self.create_valid() + location = self.exp.create_location() + device.document['location'] = {} + device.document['location']['uuid'] = location.uuid + device.save() + location = device.get_location() + if not location: + raise Exception + + def test_get_experience (self): + device = self.create_valid() + experience = self.exp.create_experience() + device.document['experience'] = {} + device.document['experience']['uuid'] = experience.uuid + device.save() + experience = device.get_experience() + if not experience: + raise Exception + + def test_get_zones (self): + location = self.exp.create_location({ 'zones': [{ 'key': 'key_1' }, { 'key': 'key_2' }]}) + device = self.create({ 'location': { 'uuid': location.uuid, 'zones': [{ 'key': 'key_2'}] }}) + zones = device.get_zones() + if zones[0].key != 'key_2': + raise Exception + + def test_get_current (self): + device = self.exp.get_current_device() + if not device: + raise Exception + exp = self.exp_sdk.start(**self.consumer_credentials) + if exp.get_current_device(): + raise Exception + exp = self.exp_sdk.start(**self.user_credentials) + if exp.get_current_device(): + raise Exception() + + def test_delete (self): + device = self.create_valid() + uuid = device.uuid + device.delete() + if self.exp.get_device(uuid): + raise Exception + device = self.create_valid() + uuid = device.uuid + self.exp.delete_device(uuid) + if self.exp.get_device(uuid): + raise Exception diff --git a/tests/test_experiences.py b/tests/test_experiences.py new file mode 100644 index 0000000..2c030fa --- /dev/null +++ b/tests/test_experiences.py @@ -0,0 +1,53 @@ + +import unittest + +from . import utils + +class Test(utils.Device, utils.CommonResourceBase): + + get_name = 'get_experience' + find_name = 'find_experiences' + create_name = 'create_experience' + class_ = utils.api.Experience + + def test_get_devices (self): + experience = self.create_valid() + device = self.exp.create_device({ 'experience': { 'uuid': experience.uuid } }) + devices = experience.get_devices() + if devices.total < 1: + raise Exception + if device.uuid not in [x.uuid for x in devices]: + raise Exception + + + def test_get_current (self): + device = self.exp.get_current_device() + device.document['experience'] = {} + device.document['experience']['uuid'] = None + device.save() + if self.exp.get_current_experience(): + raise Exception + experience = self.create_valid() + device.document['experience'] = experience.document + device.save() + experience_new = self.exp.get_current_experience() + if experience_new.uuid != experience.uuid: + raise Exception + exp = self.exp_sdk.start(**self.consumer_credentials) + if exp.get_current_experience(): + raise Exception + exp = self.exp_sdk.start(**self.user_credentials) + if exp.get_current_experience(): + raise Exception() + + def test_delete (self): + experience = self.create_valid() + uuid = experience.uuid + experience.delete() + if self.exp.get_experience(uuid): + raise Exception + experience = self.create_valid() + uuid = experience.uuid + self.exp.delete_experience(uuid) + if self.exp.get_experience(uuid): + raise Exception diff --git a/tests/test_feeds.py b/tests/test_feeds.py new file mode 100644 index 0000000..a341872 --- /dev/null +++ b/tests/test_feeds.py @@ -0,0 +1,38 @@ + +import unittest + +from . import utils + +class Test(utils.Device, utils.CommonResourceBase): + + get_name = 'get_feed' + find_name = 'find_feeds' + create_name = 'create_feed' + class_ = utils.api.Feed + + def generate_valid_document (self): + return { 'subtype': 'scala:feed:weather', 'dataType': 'static', 'searchValue': '19713', 'name': self.generate_name() } + + def test_get_data (self): + feed = self.create_valid() + data = feed.get_data() + if not isinstance(data, dict): + raise Exception + + def test_dynamic (self): + feed = self.exp.create_feed({ 'subtype': 'scala:feed:weather', 'searchValue': '', 'dataType': 'dynamic', 'name': self.generate_name() }) + data = feed.get_data(searchValue='19713') + if data['search']['search'] != '19713': + raise Exception + + def test_delete (self): + feed = self.create_valid() + uuid = feed.uuid + feed.delete() + if self.exp.get_feed(uuid): + raise Exception + feed = self.create_valid() + uuid = feed.uuid + self.exp.delete_feed(uuid) + if self.exp.get_feed(uuid): + raise Exception diff --git a/tests/test_locations.py b/tests/test_locations.py new file mode 100644 index 0000000..3c343e1 --- /dev/null +++ b/tests/test_locations.py @@ -0,0 +1,68 @@ + +import unittest + +from . import utils + +class Test(utils.Device, utils.CommonResourceBase): + + get_name = 'get_location' + find_name = 'find_locations' + create_name = 'create_location' + class_ = utils.api.Location + + def test_get_devices (self): + location = self.create_valid() + device = self.exp.create_device({ 'location': { 'uuid': location.uuid } }) + devices = location.get_devices() + if devices.total < 1: + raise Exception + if device.uuid not in [x.uuid for x in devices]: + raise Exception + + def test_get_things (self): + location = self.create_valid() + thing = self.exp.create_thing({ 'location': { 'uuid': location.uuid }, 'name': self.generate_name(), 'subtype': 'scala:thing:rfid', 'id': '123'}) + things = location.get_things() + if things.total < 1: + raise Exception + if thing.uuid not in [x.uuid for x in things]: + raise Exception + + def test_layout_url (self): + location = self.create_valid() + url = location.get_layout_url() + if not url: + raise Exception + + def test_get_current (self): + device = self.exp.get_current_device() + device.document['location'] = {} + device.document['location']['uuid'] = None + device.save() + if self.exp.get_current_location(): + raise Exception + location = self.create_valid() + device.document['location'] = location.document + device.save() + location_new = self.exp.get_current_location() + if location_new.uuid != location.uuid: + raise Exception + + exp = self.exp_sdk.start(**self.consumer_credentials) + if exp.get_current_location(): + raise Exception + exp = self.exp_sdk.start(**self.user_credentials) + if exp.get_current_location(): + raise Exception() + + def test_delete (self): + location = self.create_valid() + uuid = location.uuid + location.delete() + if self.exp.get_location(uuid): + raise Exception + location = self.create_valid() + uuid = location.uuid + self.exp.delete_location(uuid) + if self.exp.get_location(uuid): + raise Exception diff --git a/tests/test_network.py b/tests/test_network.py new file mode 100644 index 0000000..188a820 --- /dev/null +++ b/tests/test_network.py @@ -0,0 +1,94 @@ +import utils +import random +import string +import threading +import time + +class Test1 (utils.Device): + + def test_simple_message_pattern (self): + channel = self.exp.get_channel('test_channel') + listener = channel.listen('test_message') + channel.broadcast('test_message', { 'a': 1 }) + broadcast = listener.wait() + if broadcast.payload['a'] != 1: + raise + + def test_queue (self): + channel = self.exp.get_channel(self.generate_name()) + listener = channel.listen('m', max_age=1) + channel.broadcast('m', 1) + time.sleep(.1) + channel.broadcast('m', 2) + time.sleep(2) + channel.broadcast('m', 3) + channel.broadcast('m', 4) + if not listener.wait(2).payload in [3, 4]: + raise Exception + if not listener.wait(2).payload in [3, 4]: + raise Exception + if listener.wait(): + raise Exception + + + def test_cloning (self): + channel = self.exp.get_channel(self.generate_name()) + listener1 = channel.listen('hi') + listener2 = channel.listen('hi'); + channel.broadcast('hi', {}); + + broadcast = listener1.wait(5) + broadcast.payload['a'] = 1 + broadcast2 = listener2.wait(5) + if 'a' in broadcast.payload and broadcast.payload['a'] == 1: + raise Exception + + + +class Test2 (utils.Base): + + def test_responding (self): + exp1 = self.exp_sdk.start(**self.consumer_credentials) + exp2 = self.exp_sdk.start(**self.consumer_credentials) + + channel1 = exp1.get_channel('test_channel_2', consumer=True) + channel2 = exp2.get_channel('test_channel_2', consumer=True) + + self.listener = channel1.listen('test_message_2') + + threading.Thread(target=lambda: self.responder()).start() + time.sleep(.5) + response = channel1.broadcast('test_message_2', { 'a': 1 }) + if response[0]['b'] != 2: + raise Exception + + def responder (self): + broadcast = self.listener.wait(60) + if broadcast.payload['a'] == 1: + broadcast.respond({ 'b': 2 }) + + + def test_listener_cancelling (self): + exp = self.exp_sdk.start(**self.consumer_credentials) + channel = exp.get_channel('test_channel_3', consumer=True) + listener = channel.listen('test_message_3') + listener.cancel() + channel.broadcast('test_message_3') + if listener.wait(.1): + raise Exception + + def test_connected (self): + exp = self.exp_sdk.start(**self.consumer_credentials) + while not exp.is_connected: + time.sleep(.1) + + + def test_listener_timeout (self): + self.consumer_credentials['enable_network'] = False + exp = self.exp_sdk.start(**self.consumer_credentials) + try: + exp.get_channel('test').listen('hello', timeout=2) + except self.exp_sdk.NetworkError: + return + raise Exception + diff --git a/tests/test_start.py b/tests/test_start.py new file mode 100644 index 0000000..d329cc8 --- /dev/null +++ b/tests/test_start.py @@ -0,0 +1,100 @@ +import unittest +import requests +import time + +from . import utils + + +class StartFailBase (object): + + def test_start (self): + try: + self.exp_sdk.start(**self.credentials) + except self.exp_sdk.RuntimeError: + pass + else: + raise Exception + +class StartSuccessBase (object): + + def test_start (self): + self.exp_sdk.start(**self.credentials) + + +class DeviceStartBase (utils.Base): + + def setUp (self): + super(DeviceStartBase, self).setUp() + self.credentials = self.device_credentials + +class UserStartBase (utils.Base): + + def setUp(self): + super(UserStartBase, self).setUp() + self.credentials = self.user_credentials + +class ConsumerStartBase (utils.Base): + + def setUp(self): + super(ConsumerStartBase, self).setUp() + self.credentials = self.consumer_credentials + + +class TestNoDeviceUuid (StartFailBase, DeviceStartBase, unittest.TestCase): + + def setUp(self): + super(TestNoDeviceUuid, self).setUp() + self.credentials['uuid'] = None + + + +class TestNoDeviceSecret (StartFailBase, DeviceStartBase, unittest.TestCase): + + def setUp(self): + super(TestNoDeviceSecret, self).setUp() + self.credentials['secret'] = None + + + +class TestAllowPairing (StartSuccessBase, DeviceStartBase, unittest.TestCase): + + def setUp(self): + super(TestAllowPairing, self).setUp() + self.credentials['uuid'] = None + self.credentials['allow_pairing'] = True + self.credentials['secret'] = None + + +class TestNoUsername (StartFailBase, UserStartBase, unittest.TestCase): + + def setUp(self): + super(TestNoUsername, self).setUp() + self.credentials['username'] = None + + +class TestNoPassword (StartFailBase, UserStartBase, unittest.TestCase): + + def setUp(self): + super(TestNoPassword, self).setUp() + self.credentials['password'] = None + + +class TestNoOrganization (StartFailBase, UserStartBase, unittest.TestCase): + + def setUp(self): + super(TestNoOrganization, self).setUp() + self.credentials['username'] = None + + +class TestNoConsumerUuid (StartFailBase, ConsumerStartBase, unittest.TestCase): + + def setUp(self): + super(TestNoConsumerUuid, self).setUp() + self.credentials['uuid'] = None + + +class TestNoConsumerApiKey (StartFailBase, ConsumerStartBase, unittest.TestCase): + + def setUp(self): + super(TestNoConsumerApiKey, self).setUp() + self.credentials['api_key'] = None \ No newline at end of file diff --git a/tests/test_stop.py b/tests/test_stop.py new file mode 100644 index 0000000..0452750 --- /dev/null +++ b/tests/test_stop.py @@ -0,0 +1,29 @@ + +import unittest + +import utils + +class Test(utils.Base, unittest.TestCase): + def test_stop_all (self): + exp1 = self.exp_sdk.start(**self.consumer_credentials) + exp2 = self.exp_sdk.start(**self.consumer_credentials) + self.exp_sdk.stop() + try: + exp1.get_auth() + except self.exp_sdk.RuntimeError: + pass + else: + raise Error + + def test_stop (self): + exp1 = self.exp_sdk.start(**self.consumer_credentials) + exp2 = self.exp_sdk.start(**self.consumer_credentials) + exp1.stop() + try: + exp1.get_auth() + except self.exp_sdk.RuntimeError: + pass + else: + raise Error + exp2.get_auth() + diff --git a/tests/test_things.py b/tests/test_things.py new file mode 100644 index 0000000..0ea908c --- /dev/null +++ b/tests/test_things.py @@ -0,0 +1,44 @@ +import unittest + +from . import utils + +class Test(utils.Device, utils.CommonResourceBase): + + get_name = 'get_thing' + find_name = 'find_things' + create_name = 'create_thing' + class_ = utils.api.Thing + + def generate_valid_document (self): + return { 'subtype': 'scala:thing:rfid', 'id': self.generate_name(), 'name': self.generate_name()} + + def test_get_location (self): + thing = self.create_valid() + location = self.exp.create_location() + thing.document['location'] = {} + thing.document['location']['uuid'] = location.uuid + thing.save() + location = thing.get_location() + if not location: + raise Exception + + def test_get_zones (self): + location = self.exp.create_location({ 'zones': [{ 'key': 'key_1' }, { 'key': 'key_2' }]}) + document = self.generate_valid_document() + document['location'] = { 'uuid': location.uuid, 'zones': [{ 'key': 'key_2'}] } + thing = self.create(document) + zones = thing.get_zones() + if zones[0].key != 'key_2': + raise Exception + + def test_delete (self): + thing = self.create_valid() + uuid = thing.uuid + thing.delete() + if self.exp.get_thing(uuid): + raise Exception + thing = self.create_valid() + uuid = thing.uuid + self.exp.delete_thing(uuid) + if self.exp.get_thing(uuid): + raise Exception diff --git a/tests/test_zones.py b/tests/test_zones.py new file mode 100644 index 0000000..cae0a9b --- /dev/null +++ b/tests/test_zones.py @@ -0,0 +1,59 @@ + +import unittest + +from . import utils + +class Test(utils.Device, utils.ResourceBase): + + class_ = utils.api.Zone + creatable = False + findable = False + + + def create (self, junk=None): + return self.exp.create_location({ 'zones': [{ 'key': self.generate_name(), 'name': self.generate_name() }]}).get_zones()[0] + + def test_get_devices (self): + zone = self.create_valid() + device = self.exp.create_device({ 'location': { 'uuid': zone.get_location().uuid, 'zones': [{ 'key': zone.key }]}}) + devices = zone.get_devices() + if device.uuid not in [x.uuid for x in devices]: + raise Exception + if len(devices) > 1: + raise Exception + + def test_get_things (self): + zone = self.create_valid() + thing = self.exp.create_thing({ 'location': { 'uuid': zone.get_location().uuid, 'zones': [{ 'key': zone.key }]}, 'id': self.generate_name(), 'name': self.generate_name(), 'subtype': 'scala:thing:rfid'}) + things = zone.get_things() + if thing.uuid not in [x.uuid for x in things]: + raise Exception + if len(things) > 1: + raise Exception + + def test_get_current (self): + device = self.exp.get_current_device() + device.document['location'] = {} + device.document['location']['uuid'] = None + device.save() + if len(self.exp.get_current_zones()) != 0: + raise Exception + + location = self.exp.create_location({ 'zones': [{ 'key': 'a1' }, { 'key': 'b1' }] }) + + device.document['location'] = {} + device.document['location']['uuid'] = location.uuid + device.document['location']['zones'] = [{ 'key': 'a1' }] + device.save() + + zones = self.exp.get_current_zones() + if zones[0].key != 'a1': + raise Exception + + exp = self.exp_sdk.start(**self.consumer_credentials) + if len(exp.get_current_zones()) != 0: + raise Exception + exp = self.exp_sdk.start(**self.user_credentials) + if len(exp.get_current_zones()) != 0: + raise Exception() + diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..9ea5d6d --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,179 @@ +import exp_sdk +import string +import random + + +from exp_sdk import api + + +class Base (object): + + exp_sdk = exp_sdk + + def setUp(self): + self.device_credentials = { 'uuid': 'test-uuid', 'secret': 'test-secret', 'host': 'http://localhost:9000/' } + self.user_credentials = { 'username': 'test@goexp.io', 'password': 'test-Password1', 'organization': 'scala', 'host': 'http://localhost:9000/' } + self.consumer_credentials = { 'uuid': 'test-uuid', 'api_key': 'test-api-key', 'host': 'http://localhost:9000/' } + + def tearDown (self): + self.exp_sdk.stop() + + @staticmethod + def generate_name(): + return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16)) + + + +class Device (Base): + + def setUp (self): + super(Device, self).setUp() + self.exp = exp_sdk.start(**self.device_credentials) + + +class User (Base): + + def setUp (self): + super(User, self).setUp() + self.exp = exp_sdk.start(**self.user_credentials) + + +class Consumer (Base): + + def setUp (self): + super(Consumer, self).setUp() + self.exp = exp_sdk.start(**self.consumer_credentials) + + + + +class ResourceBase (object): + + class_ = None + create_name = '' + find_name = '' + findable = True + savable = True + creatable = True + + def create (self, document=None): + return getattr(self.exp, self.create_name)(document) + + def find (self, params=None): + return getattr(self.exp, self.find_name)(params) + + def create_valid (self): + return self.create(self.generate_valid_document()) + + def generate_valid_document (self): + return {} + + def assert_isinstance (self, resource): + if not isinstance(resource, self.class_): + raise Exception + + def test_create (self): + if self.creatable: + self.assert_isinstance(self.create_valid()) + try: + self.create() + except self.exp_sdk.ApiError: + pass + try: + self.create({}) + except self.exp_sdk.ApiError: + pass + + def test_document (self): + resource = self.create_valid() + if not isinstance(resource.document, dict): + raise Exception + + def test_find (self): + if not self.findable: + return + self.create_valid() + collection = self.find() + [self.assert_isinstance(resource) for resource in collection] + if not collection.total: + raise Exception + resources = self.find({ 'name': resource.name }) + if not resources.results[0] == resources[0].document: + raise Exception + if not resources.total and resources.total != 0: + raise Exception + if not resources: + raise Exception + self.assert_isinstance(resources[0]) + + def test_save (self): + if self.savable: + self.create_valid().save() + + def test_refresh (self): + self.create_valid().refresh() + + def test_get_channel (self): + resource = self.create_valid() + channel = resource.get_channel(consumer=False, system=True) + if not channel.broadcast: # Duck typed. + raise Exception + + +class CommonResourceBase (ResourceBase): + + get_name = '' + + def get (self, uuid=None): + return getattr(self.exp, self.get_name)(uuid) + + def test_get (self): + resource_1 = self.create_valid() + resource_2 = self.get(resource_1.uuid) + self.assert_isinstance(resource_2) + if self.get('invalid uuid') is not None: + raise Exception + if self.get() is not None: + raise Exception + if getattr(self.exp, self.get_name)() is not None: + raise Exception + + def test_save (self): + if self.savable: + name = self.generate_name() + resource_1 = self.create_valid() + resource_1.name = name + resource_1.save() + resource_2 = self.get(resource_1.uuid) + if resource_2.name != name: + raise Exception + super(CommonResourceBase, self).test_save() + + def test_refresh (self): + if self.savable: + name = self.generate_name() + resource_1 = self.create_valid() + resource_2 = self.get(resource_1.uuid) + resource_1.name = name + resource_1.save() + resource_2.refresh() + if resource_2.name != name: + raise Exception + super(CommonResourceBase, self).test_refresh() + + def test_uuid (self): + resource = self.create_valid() + if not resource.uuid or resource.uuid != resource.document['uuid']: + raise Exception + + def test_name (self): + name = self.generate_name() + resource = self.create_valid() + if resource.document['name'] != resource.name: + raise Exception + resource.name = name + if resource.name != name: + raise Exception + if resource.document['name'] != name: + raise Exception +