From e92ab7c6a1e43f83b7d50d089df3bb885d305d0e Mon Sep 17 00:00:00 2001 From: Marc Nijdam Date: Sat, 25 Feb 2017 11:25:39 -0800 Subject: [PATCH 1/2] Add where filter support This adds an filter keyword argument to the `where` fetch method. --- helium/resource.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/helium/resource.py b/helium/resource.py index 612a948..aa1a5cf 100644 --- a/helium/resource.py +++ b/helium/resource.py @@ -147,16 +147,17 @@ def func(json): return func @classmethod - def _mk_many(cls, session, include=None, resource_classes=None): + def _mk_many(cls, session, include=None, resource_classes=None, filter=None): classes = resource_classes or [cls] registry = {clazz._resource_type(): clazz for clazz in classes} def func(json): included = json.get('included') if include else None data = json.get('data') - return [cls._resource_class(entry, registry) - (entry, session, include=include, included=included) - for entry in data] + result = [cls._resource_class(entry, registry) + (entry, session, include=include, included=included) + for entry in data] + return result if filter is None else list(_filter(filter, result)) return func @classmethod @@ -224,7 +225,7 @@ def find(cls, session, resource_id, include=None): return session.get(url, CB.json(200, process), params=params) @classmethod - def where(cls, session, include=None, metadata=None): + def where(cls, session, include=None, metadata=None, filter=None): """Get filtered resources of the given resource class. This should be called on sub-classes only. @@ -246,12 +247,23 @@ def where(cls, session, include=None, metadata=None): The metadata argument enables filtering on resources that support metadata filters. For example:: - .. code-block:: puython + .. code-block:: python sensors = Sensor.where(session, metadata={ 'asset_id': '23456' }) Will fetch all sensors that match the given metadata attribute. + The filter argument enables filtering the resulting resources + based on a passed in function. For example:: + + .. code-block::python + + sensors = Sensor.where(session, filter=lambda s: s.name.startswith("a")) + + Will fetch all sensors and apply the given filter to only + return sensors who's name start with the given string. + + Args: session(Session): The session to look up the resources in @@ -273,7 +285,7 @@ def where(cls, session, include=None, metadata=None): params = build_request_include(include, None) if metadata is not None: params['filter[metadata]'] = to_json(metadata) - process = cls._mk_many(session, include=include) + process = cls._mk_many(session, include=include, filter=filter) return session.get(url, CB.json(200, process), params=params) @classmethod From 7963c59c9f2ec9b55874ec040f33d0445e5b76f5 Mon Sep 17 00:00:00 2001 From: Marc Nijdam Date: Sat, 25 Feb 2017 12:03:58 -0800 Subject: [PATCH 2/2] add filter to to_many relationship methods --- helium/relations.py | 18 ++-- .../tests.test_sensor.test_where.yml | 91 +++++++++++++++++++ tests/test_element.py | 6 +- tests/test_sensor.py | 7 +- 4 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 tests/cassettes/tests.test_sensor.test_where.yml diff --git a/helium/relations.py b/helium/relations.py index f5213b8..26d4b3d 100644 --- a/helium/relations.py +++ b/helium/relations.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from inflection import pluralize +from builtins import filter as _filter from . import ( CB, Resource, @@ -267,7 +268,7 @@ def method_builder(cls): iterable({to_class}): The {to_name} of :class:`{from_class}` """ - def _fetch_relationship_included(self): + def _fetch_relationship_included(self, filter=None): session = self._session include = self._include if include is None or dest_class not in include: @@ -278,11 +279,12 @@ def _fetch_relationship_included(self): included = self._included.get(dest_resource_type) mk_one = dest_class._mk_one(session, resource_classes=resource_classes) - return [mk_one({'data': entry}) for entry in included] + result = [mk_one({'data': entry}) for entry in included] + return result if filter is None else list(_filter(filter, result)) - def fetch_relationship_include(self, use_included=False): + def fetch_relationship_include(self, use_included=False, filter=None): if use_included: - return _fetch_relationship_included(self) + return _fetch_relationship_included(self, filter=filter) session = self._session id = None if self.is_singleton() else self.id url = session._build_url(self._resource_path(), id) @@ -292,10 +294,11 @@ def _process(json): included = json.get('included') mk_one = dest_class._mk_one(session, resource_classes=resource_classes) - return [mk_one({'data': entry}) for entry in included] + result = [mk_one({'data': entry}) for entry in included] + return result if filter is None else list(_filter(filter, result)) return session.get(url, CB.json(200, _process), params=params) - def fetch_relationship_direct(self, use_included=False): + def fetch_relationship_direct(self, use_included=False, filter=None): if use_included: return _fetch_relationship_included(self) session = self._session @@ -303,7 +306,8 @@ def fetch_relationship_direct(self, use_included=False): url = session._build_url(self._resource_path(), id, dest_resource_type) process = dest_class._mk_many(session, - resource_classes=resource_classes) + resource_classes=resource_classes, + filter=filter) return session.get(url, CB.json(200, process)) if type == RelationType.DIRECT: diff --git a/tests/cassettes/tests.test_sensor.test_where.yml b/tests/cassettes/tests.test_sensor.test_where.yml new file mode 100644 index 0000000..3c811cc --- /dev/null +++ b/tests/cassettes/tests.test_sensor.test_where.yml @@ -0,0 +1,91 @@ +interactions: +- request: + body: !!python/unicode '{"data": {"attributes": {"name": "test"}, "type": "sensor"}}' + 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.8.1.post2] + 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":"88eb19e3-e1d5-44a2-848e-4c8ef052d07e","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"88eb19e3-e1d5-44a2-848e-4c8ef052d07e","meta":{"card":null,"mac":null,"created":"2017-02-25T19:59:19.485204Z","last-seen":null,"ports":[],"device-type":null,"updated":"2017-02-25T19:59:19.485204Z"},"type":"sensor"}}'} + 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,n16,n11,p11'] + connection: [keep-alive] + content-length: ['439'] + content-type: [application/json;charset=utf8] + date: ['Sat, 25 Feb 2017 19:59:19 GMT'] + location: [/v1/sensor/88eb19e3-e1d5-44a2-848e-4c8ef052d07e] + 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.8.1.post2] + method: GET + uri: https://api.helium.com/v1/sensor + response: + body: {string: !!python/unicode '{"data":[{"attributes":{"name":"weather-94945"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"11b5a9e9-e098-4d57-9c10-ddb08f81ecfa","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"91293050-f5cb-45ef-9eac-c26c9dbdd515","type":"label"},{"id":"ec2b4ffb-ce77-455e-a3b3-cc2d99c2f843","type":"label"},{"id":"f9a3632d-df6a-4096-9699-c20c517434c4","type":"label"},{"id":"e0192913-b1e5-49ae-b2da-8049eed90d9d","type":"label"}]}},"id":"11b5a9e9-e098-4d57-9c10-ddb08f81ecfa","meta":{"card":null,"mac":null,"created":"2016-12-14T22:13:48.883309Z","last-seen":"2017-02-25T19:58:16.633294Z","ports":["t","h","p"],"device-type":null,"updated":"2016-12-14T22:13:48.883309Z"},"type":"sensor"},{"attributes":{"name":"Amir''s + Dev Board"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"28ea925a-c7d1-43c1-993c-50b9a4c86d0a","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"28ea925a-c7d1-43c1-993c-50b9a4c86d0a","meta":{"card":{"id":16},"mac":"6081f9fffe000b67","created":"2016-07-26T21:24:21.529879Z","last-seen":"2017-02-25T19:59:09.00786Z","ports":["_se","_b","t","h","p","v","c"],"device-type":"dev","updated":"2017-01-27T04:32:48.147404Z"},"type":"sensor"},{"attributes":{"name":"weather-94103"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"3ad9123f-0f93-48cb-a67c-205d448f56df","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"91293050-f5cb-45ef-9eac-c26c9dbdd515","type":"label"},{"id":"ec2b4ffb-ce77-455e-a3b3-cc2d99c2f843","type":"label"},{"id":"f9a3632d-df6a-4096-9699-c20c517434c4","type":"label"},{"id":"e0192913-b1e5-49ae-b2da-8049eed90d9d","type":"label"}]}},"id":"3ad9123f-0f93-48cb-a67c-205d448f56df","meta":{"card":null,"mac":null,"created":"2016-11-19T19:27:17.849498Z","last-seen":"2017-02-25T19:58:16.473452Z","ports":["t","h","p"],"device-type":null,"updated":"2016-12-14T22:59:27.103255Z"},"type":"sensor"},{"attributes":{"name":"Mark + Office Probeless"},"relationships":{"device-configuration":{"data":[{"id":"c00f0567-51c3-438d-89ca-3b0484fbb09a","type":"device-configuration"}]},"metadata":{"data":{"id":"3f37b3ad-e299-4e32-8db1-45787ce341f2","type":"metadata"}},"element":{"data":{"id":"d89ed12c-c7bb-4205-a48a-9fe59c96c459","type":"element"}},"label":{"data":[]}},"id":"3f37b3ad-e299-4e32-8db1-45787ce341f2","meta":{"card":{"id":1},"mac":"6081f9fffe0004df","created":"2016-06-15T19:01:37.358728Z","last-seen":"2017-01-29T10:10:09.314306Z","ports":["_se","d","_b","b"],"device-type":"blue","updated":"2016-12-13T00:34:54.167478Z"},"type":"sensor"},{"attributes":{"name":"Andrew + SP02"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"492759da-afb0-4d66-a83c-bb001d20c280","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"492759da-afb0-4d66-a83c-bb001d20c280","meta":{"card":{"id":1},"mac":"6081f9fffe0001a8","created":"2016-03-31T19:40:51.624362Z","last-seen":"2017-02-25T19:50:29.919303Z","ports":["t","b","_se","d"],"device-type":"blue","updated":"2016-12-23T23:07:55.565357Z"},"type":"sensor"},{"attributes":{"name":"Helium + Metrics"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"51667c26-2414-4106-b21d-08a5bce736dc","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"51667c26-2414-4106-b21d-08a5bce736dc","meta":{"card":null,"mac":null,"created":"2016-10-10T20:03:50.324721Z","last-seen":"2017-02-25T18:23:29.816846Z","ports":["sensor.count"],"device-type":null,"updated":"2016-10-10T20:03:50.324721Z"},"type":"sensor"},{"attributes":{"name":"SF + Teal Door"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"63c4911c-330c-405c-afed-1e3daf3e7c57","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"d81df823-a7a9-4476-b624-888f9fc56390","type":"label"},{"id":"ce9aad92-70d0-44a3-9ba4-8bc834d71256","type":"label"},{"id":"8c058249-ce71-47c5-aa97-282ee37d887e","type":"label"},{"id":"e919712a-eb39-4abc-8580-ed5c75505049","type":"label"}]}},"id":"63c4911c-330c-405c-afed-1e3daf3e7c57","meta":{"card":{"id":1},"mac":"6081f9fffe0000eb","created":"2015-11-05T23:20:37.076492Z","last-seen":"2017-02-25T19:54:24.928014Z","ports":["_b","t","b","_se","d","Glowfish + Alert Level"],"device-type":"blue","updated":"2016-12-13T00:23:24.886815Z"},"type":"sensor"},{"attributes":{"name":"Mark + Office"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"6774cda0-ef19-4c33-acb2-ee6addd2687c","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"6774cda0-ef19-4c33-acb2-ee6addd2687c","meta":{"card":{"id":1},"mac":"6081f9fffe0004db","created":"2016-03-17T16:45:12.688781Z","last-seen":"2016-12-22T19:45:16.395257Z","ports":["glowfish_sensor_performance","b","t","d","_se"],"device-type":"blue","updated":"2016-12-13T00:30:52.991585Z"},"type":"sensor"},{"attributes":{"name":"Marc''s + Dev Board"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"7132021b-d7ff-4a61-a014-c99b77810ff4","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"7132021b-d7ff-4a61-a014-c99b77810ff4","meta":{"card":{"id":16},"mac":"6081f9fffe000fba","created":"2016-09-13T20:16:03.571533Z","last-seen":"2017-02-24T01:21:39.806805Z","ports":["_se","values","string","_b","test","t","p","h"],"device-type":"dev","updated":"2017-02-23T22:49:29.008367Z"},"type":"sensor"},{"attributes":{"name":"SF + Front Door"},"relationships":{"device-configuration":{"data":[{"id":"0609820f-0bd4-404b-bc2f-1abe06018a02","type":"device-configuration"}]},"metadata":{"data":{"id":"85fc3b72-5a3a-471d-9bda-d18e29a42d24","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"d81df823-a7a9-4476-b624-888f9fc56390","type":"label"},{"id":"ce9aad92-70d0-44a3-9ba4-8bc834d71256","type":"label"},{"id":"e919712a-eb39-4abc-8580-ed5c75505049","type":"label"}]}},"id":"85fc3b72-5a3a-471d-9bda-d18e29a42d24","meta":{"card":{"id":1},"mac":"6081f9fffe000166","created":"2015-11-05T18:36:41.791751Z","last-seen":"2017-02-25T19:55:21.660036Z","ports":["_b","t","b","_se","d","Glowfish + Alert Level"],"device-type":"blue","updated":"2016-12-13T00:22:22.079652Z"},"type":"sensor"},{"attributes":{"name":"test"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"88eb19e3-e1d5-44a2-848e-4c8ef052d07e","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"88eb19e3-e1d5-44a2-848e-4c8ef052d07e","meta":{"card":null,"mac":null,"created":"2017-02-25T19:59:19.485204Z","last-seen":null,"ports":[],"device-type":null,"updated":"2017-02-25T19:59:19.485204Z"},"type":"sensor"},{"attributes":{"name":"SF + Marc Desk"},"relationships":{"device-configuration":{"data":[{"id":"767dc65c-6001-4cad-aad3-82d2c4328c4d","type":"device-configuration"}]},"metadata":{"data":{"id":"aba370be-837d-4b41-bee5-686b0069d874","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"d81df823-a7a9-4476-b624-888f9fc56390","type":"label"}]}},"id":"aba370be-837d-4b41-bee5-686b0069d874","meta":{"card":{"id":1},"mac":"6081f9fffe000475","created":"2016-03-30T20:52:26.314159Z","last-seen":"2017-02-25T19:01:01.361865Z","ports":["_e.info","m","h","t","b","_b","p","_se","l","lr"],"device-type":"green","updated":"2016-12-13T23:53:38.261321Z"},"type":"sensor"},{"attributes":{"name":"weather-60618"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"b61c08c2-98ec-4800-8e59-1b24e844132c","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"91293050-f5cb-45ef-9eac-c26c9dbdd515","type":"label"},{"id":"ec2b4ffb-ce77-455e-a3b3-cc2d99c2f843","type":"label"},{"id":"e0192913-b1e5-49ae-b2da-8049eed90d9d","type":"label"}]}},"id":"b61c08c2-98ec-4800-8e59-1b24e844132c","meta":{"card":null,"mac":null,"created":"2016-12-14T22:15:56.827473Z","last-seen":"2017-02-25T19:58:16.584731Z","ports":["t","h","p"],"device-type":null,"updated":"2016-12-14T22:15:56.827473Z"},"type":"sensor"},{"attributes":{"name":"SF + Teal Ceiling"},"relationships":{"device-configuration":{"data":[{"id":"c6ad5c1d-2870-45b0-a969-d45f40cfe047","type":"device-configuration"}]},"metadata":{"data":{"id":"efdea376-d80b-4ca4-af04-0fba62c183f3","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"d81df823-a7a9-4476-b624-888f9fc56390","type":"label"},{"id":"ce9aad92-70d0-44a3-9ba4-8bc834d71256","type":"label"},{"id":"8c058249-ce71-47c5-aa97-282ee37d887e","type":"label"},{"id":"e919712a-eb39-4abc-8580-ed5c75505049","type":"label"}]}},"id":"efdea376-d80b-4ca4-af04-0fba62c183f3","meta":{"card":{"id":1},"mac":"6081f9fffe00062b","created":"2016-03-31T21:20:09.859344Z","last-seen":"2017-02-25T19:19:29.545239Z","ports":["_se","l","p","b","m","_b","h","t","_e.info","Glowfish + Alert Level"],"device-type":"green","updated":"2016-12-13T00:23:39.854631Z"},"type":"sensor"},{"attributes":{"name":"Amir''s + Dev Board 2"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"f92258f1-80f8-45b3-9741-5d55fb207823","type":"metadata"}},"element":{"data":null},"label":{"data":[]}},"id":"f92258f1-80f8-45b3-9741-5d55fb207823","meta":{"card":{"id":16},"mac":"6081f9fffe000fc4","created":"2016-09-13T20:16:34.787341Z","last-seen":"2017-02-25T19:58:25.164266Z","ports":["_se","_b","t","p","v","c"],"device-type":"dev","updated":"2017-02-23T22:49:04.4244Z"},"type":"sensor"},{"attributes":{"name":"SF + Kitchen Wall"},"relationships":{"device-configuration":{"data":[]},"metadata":{"data":{"id":"fd56a7e7-ccc7-4263-9a89-05a9cc0eed6f","type":"metadata"}},"element":{"data":null},"label":{"data":[{"id":"d81df823-a7a9-4476-b624-888f9fc56390","type":"label"},{"id":"ce9aad92-70d0-44a3-9ba4-8bc834d71256","type":"label"},{"id":"e919712a-eb39-4abc-8580-ed5c75505049","type":"label"}]}},"id":"fd56a7e7-ccc7-4263-9a89-05a9cc0eed6f","meta":{"card":{"id":1},"mac":"6081f9fffe000530","created":"2016-03-31T21:21:10.493288Z","last-seen":"2017-01-31T16:09:46.702607Z","ports":["_b","h","t","_se","l","p","_e.info","b","m","Glowfish + Alert Level"],"device-type":"green","updated":"2016-12-13T00:22:55.390141Z"},"type":"sensor"}]}'} + headers: + access-control-allow-headers: ['Origin, Content-Type, Accept, Authorization'] + access-control-allow-origin: ['*'] + airship-quip: [shut it down] + 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: ['10095'] + content-type: [application/json;charset=utf8] + date: ['Sat, 25 Feb 2017 19:59:18 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.8.1.post2] + method: DELETE + uri: https://api.helium.com/v1/sensor/88eb19e3-e1d5-44a2-848e-4c8ef052d07e + response: + body: {string: !!python/unicode ''} + headers: + access-control-allow-headers: ['Origin, Content-Type, Accept, Authorization'] + access-control-allow-origin: ['*'] + airship-quip: [evacuation not done in time] + 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: ['Sat, 25 Feb 2017 19:59:19 GMT'] + server: [Warp/3.2.7] + status: {code: 204, message: No Content} +version: 1 diff --git a/tests/test_element.py b/tests/test_element.py index e84176e..cbc2ccf 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from helium import Element, Sensor -from builtins import filter def test_elements(elements, first_element): @@ -32,12 +31,11 @@ def test_include(client): def test_sensor(client): - elements = Element.all(client, include=[Sensor]) - def _has_sensors(elem): return len(elem.sensors(use_included=True)) > 0 - elements = list(filter(_has_sensors, elements)) + elements = Element.where(client, include=[Sensor], filter=_has_sensors) + assert len(elements) > 0, "No elements with attached sensors found" for elem in elements: diff --git a/tests/test_sensor.py b/tests/test_sensor.py index b55eec7..a01c6b6 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -37,12 +37,17 @@ def test_metadata_filter(client, tmp_sensor): assert tmp_sensor in found +def test_where(client, tmp_sensor): + found_sensors = Sensor.where(client, filter=lambda s: s.id == tmp_sensor.id) + assert len(found_sensors) == 1 and found_sensors[0] == tmp_sensor + + def test_meta(first_sensor): assert first_sensor.meta is not None def test_element(client): - sensors = Sensor.all(client, include=[Element]) + sensors = Sensor.where(client, include=[Element]) found_sensors = list(filter(lambda s: s.element(use_included=True) is not None, sensors)) assert len(found_sensors) > 0