Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Endpoints
* ``Client.accounts``
* ``Client.events``
* ``Client.signals``
* ``Client.places``

For a description of all available endpoints, refer to our `API Documentation <https://developer.predicthq.com/>`_.

Expand Down
1 change: 1 addition & 0 deletions predicthq/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def initialize_endpoints(self):
self.events = endpoints.EventsEndpoint(proxy(self))
self.accounts = endpoints.AccountsEndpoint(proxy(self))
self.signals = endpoints.SignalsEndpoint(proxy(self))
self.places = endpoints.PlacesEndpoint(proxy(self))

def get_headers(self, headers):
_headers = {"Accept": "application/json",}
Expand Down
1 change: 1 addition & 0 deletions predicthq/endpoints/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
from .v1.accounts import AccountsEndpoint
from .v1.events import EventsEndpoint
from .v1.signals import SignalsEndpoint
from .v1.places import PlacesEndpoint
27 changes: 25 additions & 2 deletions predicthq/endpoints/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def _export(self, *args, **kwargs):
class ListType(SchematicsListType):

def _coerce(self, value):
if isinstance(value, six.string_types):
if not isinstance(value, (list, tuple)):
return [value]
else:
return super(ListType, self)._coerce(value)
Expand All @@ -111,6 +111,20 @@ class Area(StringModel):
longitude = FloatType(required=True)


class Location(StringModel):

import_format = r'@(?P<latitude>-?\d+(\.\d+)?),(?P<longitude>-?\d+(\.\d+)?)'
export_format = "@{latitude},{longitude}"

latitude = FloatType(required=True)
longitude = FloatType(required=True)


class Place(Model):

scope = ListType(IntType, required=True)


class DateTimeRange(Model):

class Options:
Expand Down Expand Up @@ -145,12 +159,21 @@ class Options:
lte = IntType()


class PaginatedMixin(Model):
class LimitMixin(Model):

limit = IntType(min_value=1, max_value=200)


class OffsetMixin(Model):

offset = IntType(min_value=0, max_value=50)


class PaginatedMixin(LimitMixin, OffsetMixin):

pass


class SortableMixin(Model):

sort = ListType(StringType())
Expand Down
3 changes: 2 additions & 1 deletion predicthq/endpoints/v1/events/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from predicthq.endpoints.schemas import PaginatedMixin, SortableMixin, Model, ResultSet, \
ListType, StringType, GeoJSONPointType, StringListType, StringModelType, Area, \
ModelType, IntRange, IntType, DateTimeRange, DateTimeType, FloatType, ResultType, \
DictType, DateType
DictType, DateType, Place


class SearchParams(PaginatedMixin, SortableMixin, Model):
Expand All @@ -23,6 +23,7 @@ class Options:
rank = ModelType(IntRange)
country = ListType(StringType)
within = StringListType(StringModelType(Area), separator="+")
place = ModelType(Place)


class Event(Model):
Expand Down
5 changes: 5 additions & 0 deletions predicthq/endpoints/v1/places/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function

from .endpoint import PlacesEndpoint
from .schemas import Place
14 changes: 14 additions & 0 deletions predicthq/endpoints/v1/places/endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function

from predicthq.endpoints.base import BaseEndpoint
from predicthq.endpoints.decorators import accepts, returns
from .schemas import SearchParams, PlaceResultSet


class PlacesEndpoint(BaseEndpoint):

@accepts(SearchParams)
@returns(PlaceResultSet)
def search(self, **params):
return self.client.get(self.build_url('v1', 'places'), params=params)
44 changes: 44 additions & 0 deletions predicthq/endpoints/v1/places/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function

from predicthq.endpoints.schemas import LimitMixin, Model, ResultSet, \
ListType, StringType, GeoJSONPointType, StringListType, StringModelType, Location, \
DateTimeType, ResultType, SchematicsValidationError


class SearchParams(LimitMixin, Model):

class Options:
serialize_when_none = False

q = StringType()
id = ListType(StringType)
location = StringListType(StringModelType(Location), separator="+")
country = ListType(StringType)
type = ListType(StringType(choices=('planet', 'continent', 'country', 'region', 'county', 'local', 'major', 'metro', 'all')))

def validate(self, *args, **kwargs):
super(SearchParams, self).validate(*args, **kwargs)
if not any((self.q, self.id, self.location, self.country)):
raise SchematicsValidationError("Places search requires one of q, id, location or country")


class Place(Model):

class Options:
serialize_when_none = False

id = StringType()
type = StringType()
name = StringType()
county = StringType()
region = StringType()
country = StringType()
country_alpha2 = StringType()
country_alpha3 = StringType()
location = GeoJSONPointType()


class PlaceResultSet(ResultSet):

results = ResultType(Place)
1 change: 1 addition & 0 deletions tests/client_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def test_endpoints_initialization(self):
self.assertIsInstance(self.client.accounts, endpoints.AccountsEndpoint)
self.assertIsInstance(self.client.events, endpoints.EventsEndpoint)
self.assertIsInstance(self.client.signals, endpoints.SignalsEndpoint)
self.assertIsInstance(self.client.places, endpoints.PlacesEndpoint)

@with_mock_responses()
def test_request(self, responses):
Expand Down
25 changes: 25 additions & 0 deletions tests/endpoints/schemas_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,31 @@ class SchemaExample(schemas.Model):
with self.assertRaises(schemas.SchematicsDataError):
m.import_data(invalid_data)

def test_location_model(self):

class SchemaExample(schemas.Model):

location = schemas.StringModelType(schemas.Location)

short_data = {"location": "@-36.847585,174.765742"}
long_data = {"location": {"latitude": -36.847585, "longitude": 174.765742}}
model_data = {"location": schemas.Location("@-36.847585,174.765742")}
invalid_data = {"location": "-36.847585,174.765742"}

expected_expected = {"location": "@-36.847585,174.765742"}

m = SchemaExample()
self.assertDictEqual(m.import_data(short_data).to_primitive(), expected_expected)
self.assertDictEqual(m.import_data(long_data).to_primitive(), expected_expected)
self.assertDictEqual(m.import_data(model_data).to_primitive(), expected_expected)

self.assertDictEqual(m.import_data(short_data).to_dict(), expected_expected)
self.assertDictEqual(m.import_data(long_data).to_dict(), expected_expected)
self.assertDictEqual(m.import_data(model_data).to_dict(), expected_expected)

with self.assertRaises(schemas.SchematicsDataError):
m.import_data(invalid_data)

def test_resultset(self):

class ResultExample(schemas.Model):
Expand Down
30 changes: 30 additions & 0 deletions tests/endpoints/v1/places_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals, absolute_import, print_function

import unittest

from predicthq.endpoints import schemas
from tests import with_mock_client, with_mock_responses, with_client

from predicthq.endpoints.v1.places.schemas import PlaceResultSet


class PlacesTest(unittest.TestCase):

@with_mock_client()
def test_search_params(self, client):
client.places.search(country=["NZ", "AU"])
client.request.assert_called_once_with('get', '/v1/places/', params={'country': 'NZ,AU'})

@with_mock_client()
def test_invalide_search_params(self, client):
with self.assertRaises(schemas.SchematicsValidationError):
client.places.search()

@with_client()
@with_mock_responses()
def test_search(self, client, responses):
result = client.places.search(country=["NZ", "AU"])
self.assertIsInstance(result, PlaceResultSet)
self.assertEqual(result.count, len(list(result.iter_all())))
self.assertEqual(1, len(responses.calls))
44 changes: 44 additions & 0 deletions tests/fixtures/requests_responses/places_test/test_search.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[
{
"method": "GET",
"match_querystring": true,
"url": "/v1/places/?country=NZ,AU",
"status": 200,
"content_type": "application/json",
"body": {
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": "2186224",
"type": "country",
"name": "New Zealand",
"county": null,
"region": null,
"country": "New Zealand",
"country_alpha2": "NZ",
"country_alpha3": "NZL",
"location": [
174,
-42
]
},
{
"id": "2077456",
"type": "country",
"name": "Australia",
"county": null,
"region": null,
"country": "Australia",
"country_alpha2": "AU",
"country_alpha3": "AUS",
"location": [
135,
-25
]
}
]
}
}
]