Skip to content
This repository has been archived by the owner on Feb 15, 2018. It is now read-only.

Commit

Permalink
Add metadata based filtering on resources
Browse files Browse the repository at this point in the history
  • Loading branch information
madninja committed Dec 21, 2016
1 parent d1eeb54 commit b9bfd50
Show file tree
Hide file tree
Showing 4 changed files with 198 additions and 11 deletions.
9 changes: 4 additions & 5 deletions helium/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,13 @@ def _publish_metadata(self, publish, attributes):
target_resource_id = None if self.is_singleton() else self.id
url = session._build_url(target_resource_path, target_resource_id,
resource_type)
attributes = build_request_body(resource_type,
target_resource_id,
attributes=attributes)

body = build_request_body(resource_type,
target_resource_id,
attributes=attributes)
def _process(json):
data = json.get('data')
return Metadata(data, session, target_resource_path)
return publish(url, CB.json(200, _process), json=attributes)
return publish(url, CB.json(200, _process), json=body)

def update(self, attributes):
"""Update metadata.
Expand Down
61 changes: 56 additions & 5 deletions helium/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import unicode_literals
from future.utils import iteritems
from builtins import filter as _filter
from json import dumps as to_json
from . import (
CB,
build_request_body,
Expand Down Expand Up @@ -170,7 +171,7 @@ def all(cls, session, include=None):
.. code-block:: python
org = Organization.singleton(include=[Sensor])
org = Organization.singleton(session, include=[Sensor])
org.sensors(use_included=True)
Will fetch the sensors for the authorized organization as part
Expand All @@ -193,10 +194,7 @@ def all(cls, session, include=None):
this type
"""
url = session._build_url(cls._resource_path())
params = build_request_include(include, None)
process = cls._mk_many(session, include=include)
return session.get(url, CB.json(200, process), params=params)
return cls.where(session, include=include)

@classmethod
def find(cls, session, resource_id, include=None):
Expand Down Expand Up @@ -225,6 +223,59 @@ def find(cls, session, resource_id, include=None):
process = cls._mk_one(session, include=include)
return session.get(url, CB.json(200, process), params=params)

@classmethod
def where(cls, session, include=None, metadata=None):
"""Get filtered resources of the given resource class.
This should be called on sub-classes only.
The include argument allows relationship fetches to be
optimized by including the target resources in the request of
the containing resource. For example::
.. code-block:: python
org = Organization.singleton(session, include=[Sensor])
org.sensors(use_included=True)
Will fetch the sensors for the authorized organization as part
of retrieving the organization. The ``use_included`` forces
the use of included resources and avoids making a separate
request to get the sensors for the organization.
The metadata argument enables filtering on resources that
support metadata filters. For example::
.. code-block:: puython
sensors = Sensor.where(session, metadata={ 'asset_id': '23456' })
Will fetch all sensors that match the given metadata attribute.
Args:
session(Session): The session to look up the resources in
Keyword Args:
incldue(list): The resource classes to include in the
request.
metadata(dict or list): The metadata filter to apply
Returns:
iterable(Resource): An iterator over all found resources
of this type
"""
url = session._build_url(cls._resource_path())
params = build_request_include(include, None)
if metadata is not None:
params['filter[metadata]'] = to_json(metadata)
process = cls._mk_many(session, include=include)
return session.get(url, CB.json(200, process), params=params)

@classmethod
def create(cls, session, attributes=None, relationships=None):
"""Create a resource of the resource.
Expand Down
125 changes: 125 additions & 0 deletions tests/cassettes/tests.test_sensor.test_metadata_filter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
interactions:
- request:
body: !!python/unicode '{"data": {"type": "sensor", "attributes": {"name": "test"}}}'
headers:
Accept: [!!python/unicode application/json]
Accept-Charset: [!!python/unicode utf-8]
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Length: ['60']
Content-Type: [!!python/unicode application/json]
User-Agent: [!!python/unicode helium-python/0.6.4]
method: POST
uri: https://api.helium.com/v1/sensor
response:
body: {string: !!python/unicode '{"data":{"attributes":{"name":"test"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"f5101810-e7af-426c-8dff-b2e91de652e5","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"f5101810-e7af-426c-8dff-b2e91de652e5","meta":{"card":null,"mac":null,"created":"2016-12-21T14:10:30.590469Z","last-seen":null,"ports":[],"updated":"2016-12-21T14:10:30.590469Z"},"type":"sensor"}}'}
headers:
access-control-allow-headers: ['Origin, Content-Type, Accept, Authorization']
access-control-allow-origin: ['*']
airship-quip: [never breaks eye contact]
airship-trace: ['b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,n16,n11,p11']
connection: [keep-alive]
content-length: ['420']
content-type: [application/json;charset=utf8]
date: ['Wed, 21 Dec 2016 14:10:30 GMT']
location: [/v1/sensor/f5101810-e7af-426c-8dff-b2e91de652e5]
server: [Warp/3.2.7]
status: {code: 201, message: Created}
- request:
body: null
headers:
Accept: [!!python/unicode application/json]
Accept-Charset: [!!python/unicode utf-8]
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Type: [!!python/unicode application/json]
User-Agent: [!!python/unicode helium-python/0.6.4]
method: GET
uri: https://api.helium.com/v1/sensor/f5101810-e7af-426c-8dff-b2e91de652e5/metadata
response:
body: {string: !!python/unicode '{"data":{"attributes":{},"id":"f5101810-e7af-426c-8dff-b2e91de652e5","type":"metadata"}}'}
headers:
access-control-allow-headers: ['Origin, Content-Type, Accept, Authorization']
access-control-allow-origin: ['*']
airship-quip: [sharkfed]
airship-trace: ['b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,n16,o16,o17,o18']
connection: [keep-alive]
content-length: ['88']
content-type: [application/json;charset=utf8]
date: ['Wed, 21 Dec 2016 14:10:30 GMT']
server: [Warp/3.2.7]
status: {code: 200, message: OK}
- request:
body: !!python/unicode '{"data": {"type": "metadata", "attributes": {"foo": 22},
"id": "f5101810-e7af-426c-8dff-b2e91de652e5"}}'
headers:
Accept: [!!python/unicode application/json]
Accept-Charset: [!!python/unicode utf-8]
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Length: ['103']
Content-Type: [!!python/unicode application/json]
User-Agent: [!!python/unicode helium-python/0.6.4]
method: PUT
uri: https://api.helium.com/v1/sensor/f5101810-e7af-426c-8dff-b2e91de652e5/metadata
response:
body: {string: !!python/unicode '{"data":{"attributes":{"foo":22},"id":"f5101810-e7af-426c-8dff-b2e91de652e5","type":"metadata"}}'}
headers:
access-control-allow-headers: ['Origin, Content-Type, Accept, Authorization']
access-control-allow-origin: ['*']
airship-quip: [RB_GC_GUARD]
airship-trace: ['b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,n16,o16,o14,p11,o20,o18']
connection: [keep-alive]
content-length: ['96']
content-type: [application/json;charset=utf8]
date: ['Wed, 21 Dec 2016 14:10:30 GMT']
server: [Warp/3.2.7]
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: [!!python/unicode application/json]
Accept-Charset: [!!python/unicode utf-8]
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Type: [!!python/unicode application/json]
User-Agent: [!!python/unicode helium-python/0.6.4]
method: GET
uri: https://api.helium.com/v1/sensor?filter%5Bmetadata%5D=%7B%22foo%22%3A+22%7D
response:
body: {string: !!python/unicode '{"data":[{"attributes":{"name":"test"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"f5101810-e7af-426c-8dff-b2e91de652e5","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"f5101810-e7af-426c-8dff-b2e91de652e5","meta":{"card":null,"mac":null,"created":"2016-12-21T14:10:30.590469Z","last-seen":null,"ports":[],"updated":"2016-12-21T14:10:30.590469Z"},"type":"sensor"}]}'}
headers:
access-control-allow-headers: ['Origin, Content-Type, Accept, Authorization']
access-control-allow-origin: ['*']
airship-quip: [firm pat on the back]
airship-trace: ['b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,n16,o16,o17,o18']
connection: [keep-alive]
content-length: ['422']
content-type: [application/json;charset=utf8]
date: ['Wed, 21 Dec 2016 14:10:30 GMT']
server: [Warp/3.2.7]
status: {code: 200, message: OK}
- request:
body: null
headers:
Accept: [!!python/unicode application/json]
Accept-Charset: [!!python/unicode utf-8]
Accept-Encoding: ['gzip, deflate']
Connection: [keep-alive]
Content-Length: ['0']
Content-Type: [!!python/unicode application/json]
User-Agent: [!!python/unicode helium-python/0.6.4]
method: DELETE
uri: https://api.helium.com/v1/sensor/f5101810-e7af-426c-8dff-b2e91de652e5
response:
body: {string: !!python/unicode ''}
headers:
access-control-allow-headers: ['Origin, Content-Type, Accept, Authorization']
access-control-allow-origin: ['*']
airship-quip: ['$300,000 worth of cows']
airship-trace: ['b13,b12,b11,b10,b09,b08,b07,b06,b05,b04,b03,c03,c04,d04,e05,e06,f06,f07,g07,g08,h10,i12,l13,m16,m20,o20']
connection: [keep-alive]
date: ['Wed, 21 Dec 2016 14:10:30 GMT']
server: [Warp/3.2.7]
status: {code: 204, message: No Content}
version: 1
14 changes: 13 additions & 1 deletion tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,26 @@ def test_metadata(first_sensor):
assert metadata is not None


def test_metadata_filter(client, tmp_sensor):
metadata = tmp_sensor.metadata()
updated = metadata.replace({
'foo': 22
})
assert updated.foo == 22
found = Sensor.where(client, metadata={
'foo': 22
})
assert tmp_sensor in found


def test_meta(first_sensor):
assert first_sensor.meta is not None


def test_element(client):
sensors = Sensor.all(client, include=[Element])
found_sensors = list(filter(lambda s: s.element(use_included=True) is not None,
sensors))
sensors))
assert len(found_sensors) > 0
found_sensor = found_sensors[0]
found_element = found_sensor.element(use_included=True)
Expand Down

0 comments on commit b9bfd50

Please sign in to comment.