From 327092d6ed20afc5d34add7a3c7fc05f5b0e6066 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 15 May 2020 16:19:35 -0700 Subject: [PATCH 01/25] Add new challenge model properties --- maproulette/models/challenge.py | 67 +++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/maproulette/models/challenge.py b/maproulette/models/challenge.py index 2a3bcf6..89b88c7 100644 --- a/maproulette/models/challenge.py +++ b/maproulette/models/challenge.py @@ -26,6 +26,24 @@ def name(self): def name(self, value): self._name = value + @property + def created(self): + """Time stamp of when the challenge was created""" + return self._created + + @created.setter + def created(self, value): + self._created = value + + @property + def modified(self): + """Time stamp pf when the challenge was last modified""" + return self._modified + + @modified.setter + def modified(self, value): + self._modified = value + @property def description(self): """The description for the challenge""" @@ -35,6 +53,15 @@ def description(self): def description(self, value): self._description = value + @property + def deleted(self): + """Whether a challenge has been deleted or not""" + return self._deleted + + @deleted.setter + def deleted(self, value): + self._deleted = value + @property def parent(self): """The parent ID for the challenge""" @@ -98,6 +125,15 @@ def featured(self): def featured(self, value): self._featured = value + @property + def virtual_parents(self): + """The virtual parents of a challenge""" + return self._virtual_parents + + @virtual_parents.setter + def virtual_parents(self, value): + self._virtual_parents = value + @property def overpassQL(self): """The Overpass query for this challenge""" @@ -143,12 +179,34 @@ def max_zoom(self): def max_zoom(self, value): self._max_zoom = value - def __init__(self, name, id=None, description=None, parent=None, instruction=None, difficulty=None, blurb=None, - enabled=None, challenge_type=None, featured=None, overpassQL=None, default_priority=None, default_zoom=None, - min_zoom=None, max_zoom=None): + @property + def last_task_refresh(self): + """Timestamp of when the challenge's tasks were last refreshed""" + return self._last_task_refresh + + @last_task_refresh.setter + def last_task_refresh(self, value): + self._last_task_refresh = value + + @property + def data_origin_date(self): + """Timestamp of when data was originated""" + return self._data_origin_date + + @data_origin_date.setter + def data_origin_date(self, value): + self._data_origin_date = value + + def __init__(self, name, id=None, created=None, modified=None, description=None, deleted = None, parent=None, + instruction=None, difficulty=None, blurb=None, enabled=None, challenge_type=None, featured=None, + virtual_parents=None, overpassQL=None, default_priority=None, default_zoom=None, min_zoom=None, + max_zoom=None, last_task_refresh=None, data_origin_date=None): self._id = id self._name = name + self._created = created + self._modified = modified self._description = description + self._deleted = deleted self._parent = parent self._instruction = instruction self._difficulty = difficulty @@ -156,11 +214,14 @@ def __init__(self, name, id=None, description=None, parent=None, instruction=Non self._enabled = enabled self._challenge_type = challenge_type self._featured = featured + self._virtual_parents = virtual_parents self._overpassQL = overpassQL self._default_priority = default_priority self._default_zoom = default_zoom self._min_zoom = min_zoom self._max_zoom = max_zoom + self._last_task_refresh = last_task_refresh + self._data_origin_date = data_origin_date def to_dict(self): """Converts all non-null properties of a challenge object into a dictionary""" From 7f2d9617ac8f55406d9da70642c97297b96edc96 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Mon, 18 May 2020 10:23:56 -0700 Subject: [PATCH 02/25] Add new props to to_dict --- maproulette/models/challenge.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/maproulette/models/challenge.py b/maproulette/models/challenge.py index 89b88c7..9e4a123 100644 --- a/maproulette/models/challenge.py +++ b/maproulette/models/challenge.py @@ -228,7 +228,10 @@ def to_dict(self): properties = { "id": self._id, "name": self._name, + "created": self._created, + "modified": self._modified, "description": self._description, + "deleted": self.description, "parent": self._parent, "instruction": self._instruction, "difficulty": self._difficulty, @@ -236,11 +239,14 @@ def to_dict(self): "enabled": self._enabled, "challengeType": self._challenge_type, "featured": self._featured, + "virtualParents": self._virtual_parents, "overpassQL": self._overpassQL, "defaultPriority": self._default_priority, "defaultZoom": self._default_zoom, "minZoom": self._min_zoom, - "maxZoom": self._max_zoom + "maxZoom": self._max_zoom, + "lastTaskRefresh": self._last_task_refresh, + "dataOriginDate": self._data_origin_date } return {k: v for (k, v) in properties.items() if v is not None} From 4ee4b3ecfd70f558e37eb541011c52ff3318e4fa Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Mon, 18 May 2020 10:42:21 -0700 Subject: [PATCH 03/25] Add new properties to task model --- maproulette/models/task.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/maproulette/models/task.py b/maproulette/models/task.py index 7c0f2c4..41178db 100644 --- a/maproulette/models/task.py +++ b/maproulette/models/task.py @@ -27,6 +27,24 @@ def name(self): def name(self, value): self._name = value + @property + def created(self): + """Timestamp of when the task was created""" + return self._created + + @created.setter + def created(self, value): + self._created = value + + @property + def modified(self): + """Timestamp of when the task was modified""" + return self._modified + + @modified.setter + def modified(self, value): + self._modified = value + @property def parent(self): """The parent ID for the task""" @@ -153,11 +171,13 @@ def mapillary_images(self): def mapillary_images(self, value): self._mapillary_images = value - def __init__(self, name, parent, geometries, id=None, instruction=None, location=None, suggested_fix=None, - status=None, mapped_on=None, review=None, priority=None, changeset_id=None, completion_responses=None, - bundle_id=None, is_bundle_primary=None, mapillary_images=None): + def __init__(self, name, parent, geometries, created=None, modified=None, id=None, instruction=None, location=None, + suggested_fix=None, status=None, mapped_on=None, review=None, priority=None, changeset_id=None, + completion_responses=None, bundle_id=None, is_bundle_primary=None, mapillary_images=None): self._id = id self._name = name + self._created = created + self._modified = modified self._parent = parent self._geometries = geometries self._instruction = instruction @@ -178,6 +198,8 @@ def to_dict(self): properties = { "id": self._id, "name": self._name, + "created": self._created, + "modified": self._modified, "parent": self._parent, "instruction": self._instruction, "location": self._location, From d82e957bef769b76826fa6d9efd38ca2528633c3 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 19 May 2020 15:26:18 -0700 Subject: [PATCH 04/25] Added new user methods --- docs/usage/functionality.rst | 7 +++ .../add_list_of_users_to_project_group.py | 22 ++++++++ examples/add_user_to_project_group.py | 22 ++++++++ examples/find_user_by_username.py | 14 +++++ maproulette/api/maproulette_server.py | 17 ++++-- maproulette/api/user.py | 56 ++++++++++++++++++- tests/test_user_api.py | 40 +++++++++++++ 7 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 examples/add_list_of_users_to_project_group.py create mode 100644 examples/add_user_to_project_group.py create mode 100644 examples/find_user_by_username.py create mode 100644 tests/test_user_api.py diff --git a/docs/usage/functionality.rst b/docs/usage/functionality.rst index 08cdbf7..e96505f 100644 --- a/docs/usage/functionality.rst +++ b/docs/usage/functionality.rst @@ -22,6 +22,13 @@ Module: task :members: :show-inheritance: +Module: user +----------------------------------------------------- + +.. automodule:: maproulette.api.user + :members: + :show-inheritance: + Module: configuration ----------------------------------------------------- diff --git a/examples/add_list_of_users_to_project_group.py b/examples/add_list_of_users_to_project_group.py new file mode 100644 index 0000000..f795c3f --- /dev/null +++ b/examples/add_list_of_users_to_project_group.py @@ -0,0 +1,22 @@ +import maproulette +import json + +# Create a configuration object using a url and API key +config = maproulette.Configuration(api_key='{YOUR_API_KEY}') + +# Create an API object using your config object +api = maproulette.User(config) + +# Specify the user IDs to grant privileges to +user_ids = [123, 456, 789] + +# Specify the project ID to update the privileges for +project_id = '147' + +# Specify what level of access you want to grant this user (1 - Admin, 2 - Write, 3 - Read) +group = '2' + +# Print the API response +print(json.dumps(api.add_list_of_users_to_project_group(user_ids=user_ids, + project_id=project_id, + group_type=group), indent=4, sort_keys=True)) diff --git a/examples/add_user_to_project_group.py b/examples/add_user_to_project_group.py new file mode 100644 index 0000000..32973c5 --- /dev/null +++ b/examples/add_user_to_project_group.py @@ -0,0 +1,22 @@ +import maproulette +import json + +# Create a configuration object using a url and API key +config = maproulette.Configuration(api_key='{YOUR_API_KEY}') + +# Create an API object using your config object +api = maproulette.User(config) + +# Specify the user ID to grant privileges to +user_id = '{SOME_USER_ID}' + +# Specify the project ID to update the privileges for +project_id = '{YOUR_PROJECT_ID}' + +# Specify what level of access you want to grant this user (1 - Admin, 2 - Write, 3 - Read) +group = '2' + +# Print the API response +print(json.dumps(api.add_user_to_project_group(user_id=user_id, + project_id=project_id, + group_type=group), indent=4, sort_keys=True)) diff --git a/examples/find_user_by_username.py b/examples/find_user_by_username.py new file mode 100644 index 0000000..cadd4cc --- /dev/null +++ b/examples/find_user_by_username.py @@ -0,0 +1,14 @@ +import maproulette +import json + +# Create a configuration object using a url and API key +config = maproulette.Configuration(api_key='{YOUR_API_KEY}') + +# Create an API object using your config object +api = maproulette.User(config) + +# We want to fetch a user with a particular name +username = '{YOUR_USERNAME}' + +# Print the API response +print(json.dumps(api.find_user_by_username(username), indent=4, sort_keys=True)) diff --git a/maproulette/api/maproulette_server.py b/maproulette/api/maproulette_server.py index b4b36e1..dfee469 100644 --- a/maproulette/api/maproulette_server.py +++ b/maproulette/api/maproulette_server.py @@ -57,7 +57,8 @@ def get(self, endpoint, params=None): """ response = self.session.get( self.url + endpoint, - params=params) + params=params + ) try: response.raise_for_status() except requests.exceptions.HTTPError as e: @@ -89,16 +90,19 @@ def get(self, endpoint, params=None): "status": response.status_code } - def post(self, endpoint, body=None): + def post(self, endpoint, body=None, params=None): """Method that completes a POST request to the MapRoulette API :param endpoint: the server endpoint to use for the POST request :param body: the body of the request (optional) + :param params: the parameters that pertain to the request (optional) :returns: a JSON object containing the API response """ response = self.session.post( self.url + endpoint, - json=body) + params=params, + json=body + ) try: response.raise_for_status() except requests.exceptions.HTTPError as e: @@ -132,16 +136,19 @@ def post(self, endpoint, body=None): "status": response.status_code } - def put(self, endpoint, body=None): + def put(self, endpoint, body=None, params=None): """Method that completes a PUT request to the MapRoulette API :param endpoint: the server endpoint to use for the PUT request :param body: the body of the request (optional) + :param params: the parameters that pertain to the request (optional) :returns: a JSON object containing the response code and the API response if """ response = self.session.put( self.url + endpoint, - json=body) + params=params, + json=body + ) try: response.raise_for_status() except requests.exceptions.HTTPError as e: diff --git a/maproulette/api/user.py b/maproulette/api/user.py index a401f8e..ced0176 100644 --- a/maproulette/api/user.py +++ b/maproulette/api/user.py @@ -1,6 +1,5 @@ """This module contains the methods that the user will use directly to interact with MapRoulette users""" -import json from maproulette.api.maproulette_server import MapRouletteServer @@ -9,3 +8,58 @@ class User(MapRouletteServer): def __init__(self, config): super().__init__(configuration=config) + + def find_user_by_username(self, username, limit=10, page=0): + """Method to search for a user based on a specific username + + :param username: the username to search for. + :param limit: the limit to the number of results returned in the response. Default is 10 + :param page: used in conjunction with the limit parameter to page through X number of responses. Default is 0. + :returns: the API response from the GET request + """ + query_params = { + "limit": str(limit), + "page": str(page) + } + response = self.get( + endpoint=f"/users/find/{username}", + params=query_params + ) + return response + + def add_user_to_project_group(self, user_id, project_id, group_type, is_osm_user_id='true'): + """Method to add a user to a project group + + :param user_id: the user ID to add to the specified project group + :param project_id: the ID of the project + :param group_type: the group type to add the user to (1 - Admin, 2 - Write, 3 - Read) + :param is_osm_user_id: whether or not the specified user ID is an OSM user ID. Default is 'false'. + :returns: the API response from the POST request + """ + query_params = { + "isOSMUserId": str(is_osm_user_id) + } + response = self.post( + endpoint=f"/user/{user_id}/project/{project_id}/{group_type}", + params=query_params + ) + return response + + def add_list_of_users_to_project_group(self, user_ids, project_id, group_type, is_osm_user_id='true'): + """Method to add a user to a project group + + :param user_ids: a list of user IDs to add to the specified project group. IDs should be integers. + :param project_id: the ID of the project + :param group_type: the group type to add the user to (1 - Admin, 2 - Write, 3 - Read) + :param is_osm_user_id: whether or not the specified user ID is an OSM user ID. Default is 'false'. + :returns: the API response from the PUT request + """ + query_params = { + "isOSMUserId": str(is_osm_user_id) + } + response = self.put( + endpoint=f"/user/project/{project_id}/{group_type}", + params=query_params, + body=user_ids + ) + return response diff --git a/tests/test_user_api.py b/tests/test_user_api.py new file mode 100644 index 0000000..f1ac165 --- /dev/null +++ b/tests/test_user_api.py @@ -0,0 +1,40 @@ +import maproulette +import unittest +from unittest.mock import patch + + +class TestUserAPI(unittest.TestCase): + + config = maproulette.Configuration(api_key="API_KEY") + api = maproulette.User(config) + + @patch('maproulette.api.maproulette_server.requests.Session.get') + def test_find_user_by_username(self, mock_request, api_instance=api): + test_username = 'my_username_123' + mock_request.return_value.status_code = '200' + response = api_instance.find_user_by_username(test_username) + self.assertEqual(response['status'], '200') + + @patch('maproulette.api.maproulette_server.requests.Session.post') + def test_add_user_to_project_group(self, mock_request, api_instance=api): + test_user_id = '12345' + test_project_id = '6789' + test_group = '2' + mock_request.return_value.status_code = '200' + response = api_instance.add_user_to_project_group(user_id=test_user_id, + project_id=test_project_id, + group_type=test_group, + is_osm_user_id='true') + self.assertEqual(response['status'], '200') + + @patch('maproulette.api.maproulette_server.requests.Session.put') + def test_add_list_of_users_to_project_group(self, mock_request, api_instance=api): + test_user_ids = [123, 456, 789] + test_project_id = '6789' + test_group = '2' + mock_request.return_value.status_code = '200' + response = api_instance.add_list_of_users_to_project_group(user_ids=test_user_ids, + project_id=test_project_id, + group_type=test_group, + is_osm_user_id='true') + self.assertEqual(response['status'], '200') From d210da9e6883a11756c6b4cb67646b46059509b7 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 26 May 2020 08:20:34 -0700 Subject: [PATCH 05/25] Started model for priority rule --- maproulette/__init__.py | 1 + maproulette/models/__init__.py | 1 + maproulette/models/priority_rule.py | 56 +++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 maproulette/models/priority_rule.py diff --git a/maproulette/__init__.py b/maproulette/__init__.py index bc10a07..5d5ffe1 100644 --- a/maproulette/__init__.py +++ b/maproulette/__init__.py @@ -7,6 +7,7 @@ from .models.project import ProjectModel from .models.challenge import ChallengeModel from .models.task import TaskModel +from .models.priority_rule import PriorityRuleModel from .api.project import Project from .api.challenge import Challenge from .api.task import Task diff --git a/maproulette/models/__init__.py b/maproulette/models/__init__.py index b5f6d9e..9ffe7e6 100644 --- a/maproulette/models/__init__.py +++ b/maproulette/models/__init__.py @@ -1,3 +1,4 @@ from .project import ProjectModel from .challenge import ChallengeModel from .task import TaskModel +from .priority_rule import PriorityRuleModel diff --git a/maproulette/models/priority_rule.py b/maproulette/models/priority_rule.py new file mode 100644 index 0000000..1de163e --- /dev/null +++ b/maproulette/models/priority_rule.py @@ -0,0 +1,56 @@ +"""This module contains the definition of a priority rule object in MapRoulette.""" + +import json +import os + +VALID_TYPES = {'string', 'integer', 'double', 'long', 'nested rule'} + + +class PriorityRuleModel: + """Definition for a MapRoulette priority rule""" + + @property + def priority_value(self): + """The value for the priority rule""" + return self._priority_value + + @priority_value.setter + def priority_value(self, value): + self._priority_value = value + + @property + def priority_type(self): + """The type for the priority rule""" + return self._priority_type + + @priority_type.setter + def priority_type(self, value): + if value not in VALID_TYPES: + raise ValueError("Priority type must be one of %s.", VALID_TYPES) + self._priority_type = value + + @property + def priority_operator(self): + """The type for the priority rule""" + return self._priority_operator + + @priority_operator.setter + def priority_operator(self, value): + self._priority_operator = value + + def __init__(self, priority_value, priority_type, priority_operator): + self._priority_value = priority_value + self._priority_type = priority_type + self._priority_operator = priority_operator + + def to_dict(self): + """Converts all non-null properties of a project object into a dictionary""" + return { + "value": self._priority_value, + "type": self._priority_type, + "operator": self._priority_operator + } + + def to_json(self): + """Converts all non-null properties of a project object into a JSON object""" + return json.dumps(self.to_dict()) From 97db670d2bc94b6bb3420ac56f42065e0a576433 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 26 May 2020 08:24:50 -0700 Subject: [PATCH 06/25] Changed method names --- ...t_group.py => add_user_list_to_project.py} | 6 +++--- ...roject_group.py => add_user_to_project.py} | 6 +++--- maproulette/api/user.py | 4 ++-- tests/test_user_api.py | 20 +++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) rename examples/{add_list_of_users_to_project_group.py => add_user_list_to_project.py} (66%) rename examples/{add_user_to_project_group.py => add_user_to_project.py} (69%) diff --git a/examples/add_list_of_users_to_project_group.py b/examples/add_user_list_to_project.py similarity index 66% rename from examples/add_list_of_users_to_project_group.py rename to examples/add_user_list_to_project.py index f795c3f..dd55d04 100644 --- a/examples/add_list_of_users_to_project_group.py +++ b/examples/add_user_list_to_project.py @@ -17,6 +17,6 @@ group = '2' # Print the API response -print(json.dumps(api.add_list_of_users_to_project_group(user_ids=user_ids, - project_id=project_id, - group_type=group), indent=4, sort_keys=True)) +print(json.dumps(api.add_user_list_to_project(user_ids=user_ids, + project_id=project_id, + group_type=group), indent=4, sort_keys=True)) diff --git a/examples/add_user_to_project_group.py b/examples/add_user_to_project.py similarity index 69% rename from examples/add_user_to_project_group.py rename to examples/add_user_to_project.py index 32973c5..7708372 100644 --- a/examples/add_user_to_project_group.py +++ b/examples/add_user_to_project.py @@ -17,6 +17,6 @@ group = '2' # Print the API response -print(json.dumps(api.add_user_to_project_group(user_id=user_id, - project_id=project_id, - group_type=group), indent=4, sort_keys=True)) +print(json.dumps(api.add_user_to_project(user_id=user_id, + project_id=project_id, + group_type=group), indent=4, sort_keys=True)) diff --git a/maproulette/api/user.py b/maproulette/api/user.py index ced0176..579177c 100644 --- a/maproulette/api/user.py +++ b/maproulette/api/user.py @@ -27,7 +27,7 @@ def find_user_by_username(self, username, limit=10, page=0): ) return response - def add_user_to_project_group(self, user_id, project_id, group_type, is_osm_user_id='true'): + def add_user_to_project(self, user_id, project_id, group_type, is_osm_user_id='true'): """Method to add a user to a project group :param user_id: the user ID to add to the specified project group @@ -45,7 +45,7 @@ def add_user_to_project_group(self, user_id, project_id, group_type, is_osm_user ) return response - def add_list_of_users_to_project_group(self, user_ids, project_id, group_type, is_osm_user_id='true'): + def add_user_list_to_project(self, user_ids, project_id, group_type, is_osm_user_id='true'): """Method to add a user to a project group :param user_ids: a list of user IDs to add to the specified project group. IDs should be integers. diff --git a/tests/test_user_api.py b/tests/test_user_api.py index f1ac165..861e264 100644 --- a/tests/test_user_api.py +++ b/tests/test_user_api.py @@ -16,25 +16,25 @@ def test_find_user_by_username(self, mock_request, api_instance=api): self.assertEqual(response['status'], '200') @patch('maproulette.api.maproulette_server.requests.Session.post') - def test_add_user_to_project_group(self, mock_request, api_instance=api): + def test_add_user_to_project(self, mock_request, api_instance=api): test_user_id = '12345' test_project_id = '6789' test_group = '2' mock_request.return_value.status_code = '200' - response = api_instance.add_user_to_project_group(user_id=test_user_id, - project_id=test_project_id, - group_type=test_group, - is_osm_user_id='true') + response = api_instance.add_user_to_project(user_id=test_user_id, + project_id=test_project_id, + group_type=test_group, + is_osm_user_id='true') self.assertEqual(response['status'], '200') @patch('maproulette.api.maproulette_server.requests.Session.put') - def test_add_list_of_users_to_project_group(self, mock_request, api_instance=api): + def test_add_user_list_to_project(self, mock_request, api_instance=api): test_user_ids = [123, 456, 789] test_project_id = '6789' test_group = '2' mock_request.return_value.status_code = '200' - response = api_instance.add_list_of_users_to_project_group(user_ids=test_user_ids, - project_id=test_project_id, - group_type=test_group, - is_osm_user_id='true') + response = api_instance.add_user_list_to_project(user_ids=test_user_ids, + project_id=test_project_id, + group_type=test_group, + is_osm_user_id='true') self.assertEqual(response['status'], '200') From bac3cffe0ab3df8bbcc034712c66828803181a0f Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Wed, 27 May 2020 08:42:42 -0700 Subject: [PATCH 07/25] Updated priority model --- maproulette/models/priority_rule.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/maproulette/models/priority_rule.py b/maproulette/models/priority_rule.py index 1de163e..fd1cd63 100644 --- a/maproulette/models/priority_rule.py +++ b/maproulette/models/priority_rule.py @@ -3,7 +3,9 @@ import json import os -VALID_TYPES = {'string', 'integer', 'double', 'long', 'nested rule'} +VALID_TYPES = {'string', 'integer', 'double', 'long'} +VALID_STRING_OPERATORS = {'equal', 'not_equal', 'contains', 'not_contains', 'is_empty', 'is_not_empty'} +VALID_NUMERIC_OPERATORS = {'==', '!=', '<', '<=', '>', '>='} class PriorityRuleModel: @@ -26,7 +28,7 @@ def priority_type(self): @priority_type.setter def priority_type(self, value): if value not in VALID_TYPES: - raise ValueError("Priority type must be one of %s.", VALID_TYPES) + raise ValueError(f"Priority type must be one of {VALID_TYPES}.") self._priority_type = value @property @@ -36,9 +38,15 @@ def priority_operator(self): @priority_operator.setter def priority_operator(self, value): + if self.priority_type == 'string': + if value not in VALID_STRING_OPERATORS: + raise ValueError(f"Priority operator must be one of {VALID_STRING_OPERATORS}.") + else: + if value not in VALID_NUMERIC_OPERATORS: + raise ValueError(f"Priority operator must be one of {VALID_NUMERIC_OPERATORS}.") self._priority_operator = value - def __init__(self, priority_value, priority_type, priority_operator): + def __init__(self, priority_value=None, priority_type=None, priority_operator=None): self._priority_value = priority_value self._priority_type = priority_type self._priority_operator = priority_operator From 6660f058ce6ab99a93bd2efd778d9c7f1a4287c2 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Wed, 3 Jun 2020 10:35:31 -0700 Subject: [PATCH 08/25] Change properties --- maproulette/models/challenge.py | 77 +++++---------------------------- 1 file changed, 11 insertions(+), 66 deletions(-) diff --git a/maproulette/models/challenge.py b/maproulette/models/challenge.py index 9e4a123..c66b307 100644 --- a/maproulette/models/challenge.py +++ b/maproulette/models/challenge.py @@ -26,24 +26,6 @@ def name(self): def name(self, value): self._name = value - @property - def created(self): - """Time stamp of when the challenge was created""" - return self._created - - @created.setter - def created(self, value): - self._created = value - - @property - def modified(self): - """Time stamp pf when the challenge was last modified""" - return self._modified - - @modified.setter - def modified(self, value): - self._modified = value - @property def description(self): """The description for the challenge""" @@ -53,15 +35,6 @@ def description(self): def description(self, value): self._description = value - @property - def deleted(self): - """Whether a challenge has been deleted or not""" - return self._deleted - - @deleted.setter - def deleted(self, value): - self._deleted = value - @property def parent(self): """The parent ID for the challenge""" @@ -125,15 +98,6 @@ def featured(self): def featured(self, value): self._featured = value - @property - def virtual_parents(self): - """The virtual parents of a challenge""" - return self._virtual_parents - - @virtual_parents.setter - def virtual_parents(self, value): - self._virtual_parents = value - @property def overpassQL(self): """The Overpass query for this challenge""" @@ -180,33 +144,20 @@ def max_zoom(self, value): self._max_zoom = value @property - def last_task_refresh(self): - """Timestamp of when the challenge's tasks were last refreshed""" - return self._last_task_refresh - - @last_task_refresh.setter - def last_task_refresh(self, value): - self._last_task_refresh = value - - @property - def data_origin_date(self): - """Timestamp of when data was originated""" - return self._data_origin_date + def osm_id_property(self): + """The id property of an osm feature""" + return self._osm_id_property - @data_origin_date.setter - def data_origin_date(self, value): - self._data_origin_date = value + @osm_id_property.setter + def osm_id_property(self, value): + self._osm_id_property = value - def __init__(self, name, id=None, created=None, modified=None, description=None, deleted = None, parent=None, - instruction=None, difficulty=None, blurb=None, enabled=None, challenge_type=None, featured=None, - virtual_parents=None, overpassQL=None, default_priority=None, default_zoom=None, min_zoom=None, - max_zoom=None, last_task_refresh=None, data_origin_date=None): + def __init__(self, name, id=None, description=None, parent=None, instruction=None, difficulty=None, blurb=None, + enabled=None, challenge_type=None, featured=None, overpassQL=None, default_priority=None, + default_zoom=None, min_zoom=None, max_zoom=None, osm_id_property=None): self._id = id self._name = name - self._created = created - self._modified = modified self._description = description - self._deleted = deleted self._parent = parent self._instruction = instruction self._difficulty = difficulty @@ -214,22 +165,18 @@ def __init__(self, name, id=None, created=None, modified=None, description=None, self._enabled = enabled self._challenge_type = challenge_type self._featured = featured - self._virtual_parents = virtual_parents self._overpassQL = overpassQL self._default_priority = default_priority self._default_zoom = default_zoom self._min_zoom = min_zoom self._max_zoom = max_zoom - self._last_task_refresh = last_task_refresh - self._data_origin_date = data_origin_date + self._osm_id_property = osm_id_property def to_dict(self): """Converts all non-null properties of a challenge object into a dictionary""" properties = { "id": self._id, "name": self._name, - "created": self._created, - "modified": self._modified, "description": self._description, "deleted": self.description, "parent": self._parent, @@ -239,14 +186,12 @@ def to_dict(self): "enabled": self._enabled, "challengeType": self._challenge_type, "featured": self._featured, - "virtualParents": self._virtual_parents, "overpassQL": self._overpassQL, "defaultPriority": self._default_priority, "defaultZoom": self._default_zoom, "minZoom": self._min_zoom, "maxZoom": self._max_zoom, - "lastTaskRefresh": self._last_task_refresh, - "dataOriginDate": self._data_origin_date + "osmIdProperty": self._osm_id_property } return {k: v for (k, v) in properties.items() if v is not None} From 280d70cc5e081f32213a13132ee6a9e54954f485 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 5 Jun 2020 11:32:47 -0700 Subject: [PATCH 09/25] Add more challenge properties --- maproulette/models/challenge.py | 162 +++++++++++++++++++++++++++++++- 1 file changed, 160 insertions(+), 2 deletions(-) diff --git a/maproulette/models/challenge.py b/maproulette/models/challenge.py index c66b307..adc7779 100644 --- a/maproulette/models/challenge.py +++ b/maproulette/models/challenge.py @@ -116,6 +116,24 @@ def default_priority(self): def default_priority(self, value): self._default_priority = value + @property + def high_priority_rule(self): + """The high priority for this challenge""" + return self._high_priority_rule + + @high_priority_rule.setter + def high_priority_rule(self, value): + self._high_priority_rule = value + + @property + def low_priority_rule(self): + """The low priority of this challenge""" + return self._low_priority_rule + + @low_priority_rule.setter + def low_priority_rule(self, value): + self._low_priority_rule = value + @property def default_zoom(self): """The default zoom level for this challenge""" @@ -152,9 +170,121 @@ def osm_id_property(self): def osm_id_property(self, value): self._osm_id_property = value + @property + def cooperative_type(self): + """""" + return self._cooperative_type + + @cooperative_type.setter + def cooperative_type(self, value): + self._cooperative_type = value + + @property + def popularity(self): + """The popularity of a challenge""" + return self._popularity + + @popularity.setter + def popularity(self, value): + self._popularity = value + + @property + def check_in_comment(self): + """Comment to be associated with changes made by users""" + return self._check_in_comment + + @check_in_comment.setter + def check_in_comment(self, value): + self._check_in_comment = value + + @property + def check_in_source(self): + """Hashtag appended to changeset comments""" + return self._check_in_source + + @check_in_source.setter + def check_in_source(self, value): + self._check_in_source = value + + @property + def requires_local(self): + """Whether or not tasks require local knowledge to complete""" + return self._requires_local + + @requires_local.setter + def requires_local(self, value): + self._requires_local = value + + @property + def default_basemap(self): + """The default basemap to use for this challenge""" + return self._default_basemap + + @default_basemap.setter + def default_basemap(self, value): + self._default_basemap = value + + @property + def default_basemap_id(self): + """The id of the default basemap""" + return self._default_basemap_id + + @default_basemap_id.setter + def default_basemap_id(self, value): + self._default_basemap_id = value + + @property + def custom_basemap(self): + """The custom basemap of this challenge""" + return self._custom_basemap + + @custom_basemap.setter + def custom_basemap(self, value): + self._custom_basemap = value + + @property + def update_tasks(self): + """Whether or not to periodically delete old tasks""" + return self._update_tasks + + @update_tasks.setter + def update_tasks(self, value): + self._update_tasks = value + + @property + def exportable_properties(self): + """Comma separated list of properties to be exportable""" + return self._exportable_properties + + @exportable_properties.setter + def exportable_properties(self, value): + self._exportable_properties = value + + @property + def preferred_tags(self): + """List of preferred tags the user can use when completing tasks""" + return self._preferred_tags + + @preferred_tags.setter + def preferred_tags(self, value): + self._preferred_tags = value + + @property + def task_styles(self): + """Custom task styling based on specific task feature properties""" + return self._task_styles + + @task_styles.setter + def task_styles(self, value): + self._task_styles = value + def __init__(self, name, id=None, description=None, parent=None, instruction=None, difficulty=None, blurb=None, enabled=None, challenge_type=None, featured=None, overpassQL=None, default_priority=None, - default_zoom=None, min_zoom=None, max_zoom=None, osm_id_property=None): + high_priority_rule=None, low_priority_rule=None, default_zoom=None, min_zoom=None, max_zoom=None, + osm_id_property=None, cooperative_type=None, popularity=None, check_in_comment=None, + check_in_source=None, requires_local=None, default_basemap=None, default_basemap_id=None, + custom_basemap=None, update_tasks=None, exportable_properties=None, preferred_tags=None, + task_styles=None): self._id = id self._name = name self._description = description @@ -167,10 +297,24 @@ def __init__(self, name, id=None, description=None, parent=None, instruction=Non self._featured = featured self._overpassQL = overpassQL self._default_priority = default_priority + self._high_priority_rule = high_priority_rule + self._low_priority_rule = low_priority_rule self._default_zoom = default_zoom self._min_zoom = min_zoom self._max_zoom = max_zoom self._osm_id_property = osm_id_property + self._cooperative_type = cooperative_type + self._popularity = popularity + self._check_in_comment = check_in_comment + self._check_in_source = check_in_source + self._requires_local = requires_local + self._default_basemap = default_basemap + self._default_basemap_id = default_basemap_id + self._custom_basemap = custom_basemap + self._update_tasks = update_tasks + self._exportable_properties = exportable_properties + self._preferred_tags = preferred_tags + self._task_styles = task_styles def to_dict(self): """Converts all non-null properties of a challenge object into a dictionary""" @@ -188,10 +332,24 @@ def to_dict(self): "featured": self._featured, "overpassQL": self._overpassQL, "defaultPriority": self._default_priority, + "highPriorityRule": self._high_priority_rule, + "lowPriorityRule": self._low_priority_rule, "defaultZoom": self._default_zoom, "minZoom": self._min_zoom, "maxZoom": self._max_zoom, - "osmIdProperty": self._osm_id_property + "osmIdProperty": self._osm_id_property, + "cooperativeType": self._cooperative_type, + "popularity": self._popularity, + "checkInComment": self._check_in_comment, + "checkInSource": self._check_in_source, + "requiresLocal": self._requires_local, + "defaultBasemap": self._default_basemap, + "defautlBasemapId": self._default_basemap_id, + "customBasemap": self._custom_basemap, + "updateTasks": self._update_tasks, + "exportableProperties": self._exportable_properties, + "preferredTags": self._preferred_tags, + "taskStyles": self._task_styles } return {k: v for (k, v) in properties.items() if v is not None} From 34474546e4b66938c82ab8efa2ab9d866f2fe1bd Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 5 Jun 2020 12:06:35 -0700 Subject: [PATCH 10/25] Add project property --- maproulette/models/project.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/maproulette/models/project.py b/maproulette/models/project.py index 5d32dce..a0e37e7 100644 --- a/maproulette/models/project.py +++ b/maproulette/models/project.py @@ -65,22 +65,22 @@ def display_name(self, value): self._display_name = value @property - def is_virtual(self): - """Whether or not this project is virtual""" - return self._is_virtual + def featured(self): + """Whether or not the project is featured""" + return self._featured - @is_virtual.setter - def is_virtual(self, value): - self._is_virtual = value + @featured.setter + def featured(self, value): + self._featured = value - def __init__(self, name, id=None, description=None, groups=None, enabled=None, display_name=None, is_virtual=None): + def __init__(self, name, id=None, description=None, groups=None, enabled=None, display_name=None, featured=None): self._id = id self._name = name self._description = description self._groups = groups self._enabled = enabled self._display_name = display_name - self._is_virtual = is_virtual + self._featured = featured def to_dict(self): """Converts all non-null properties of a project object into a dictionary""" @@ -91,7 +91,7 @@ def to_dict(self): "groups": self._groups, "enabled": self._enabled, "display_name": self._display_name, - "is_virtual": self._is_virtual + "featured": self._featured } return {k: v for (k, v) in properties.items() if v is not None} From 93d06144dbdfeb620f5fdc7cf7e3249abdf30b23 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 5 Jun 2020 12:14:34 -0700 Subject: [PATCH 11/25] Adjust tests to reflect properties --- tests/test_project_api.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_project_api.py b/tests/test_project_api.py index 718831a..ef9b4b4 100644 --- a/tests/test_project_api.py +++ b/tests/test_project_api.py @@ -48,8 +48,7 @@ def test_create_project(self, mock_request, api_instance=api): @patch('maproulette.api.maproulette_server.requests.Session.post') def test_add_challenge_to_project(self, mock_request, api_instance=api): test_virtual_project_model = maproulette.ProjectModel(name='Test Virtual Project Name', - id=1234, - is_virtual=True) + id=1234) test_challenge_model = maproulette.ChallengeModel(name='Test Challenge Name', id=246) test_virtual_project_id = test_virtual_project_model.id @@ -61,8 +60,7 @@ def test_add_challenge_to_project(self, mock_request, api_instance=api): @patch('maproulette.api.maproulette_server.requests.Session.post') def test_remove_challenge_from_project(self, mock_request, api_instance=api): test_virtual_project_model = maproulette.ProjectModel(name='Test Virtual Project Name', - id=1234, - is_virtual=True) + id=1234) test_challenge_model = maproulette.ChallengeModel(name='Test Challenge Name', id=246) test_virtual_project_id = test_virtual_project_model.id test_challenge_id = test_challenge_model.id From 5a5d5a34ab8b213b730b4a774a2475fa3f4e6183 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 5 Jun 2020 13:10:02 -0700 Subject: [PATCH 12/25] Add is virtual back to project props --- maproulette/models/project.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/maproulette/models/project.py b/maproulette/models/project.py index a0e37e7..aa5b453 100644 --- a/maproulette/models/project.py +++ b/maproulette/models/project.py @@ -64,6 +64,15 @@ def display_name(self): def display_name(self, value): self._display_name = value + @property + def is_virtual(self): + """Whether or not a project is virtual""" + return self._is_virtual + + @is_virtual.setter + def is_virtual(self, value): + self._is_virtual = value + @property def featured(self): """Whether or not the project is featured""" @@ -73,7 +82,8 @@ def featured(self): def featured(self, value): self._featured = value - def __init__(self, name, id=None, description=None, groups=None, enabled=None, display_name=None, featured=None): + def __init__(self, name, id=None, description=None, groups=None, enabled=None, + is_virtual=None, display_name=None, featured=None): self._id = id self._name = name self._description = description @@ -81,6 +91,7 @@ def __init__(self, name, id=None, description=None, groups=None, enabled=None, d self._enabled = enabled self._display_name = display_name self._featured = featured + self._is_virtual = is_virtual def to_dict(self): """Converts all non-null properties of a project object into a dictionary""" @@ -91,7 +102,8 @@ def to_dict(self): "groups": self._groups, "enabled": self._enabled, "display_name": self._display_name, - "featured": self._featured + "featured": self._featured, + "isVirtual": self._is_virtual } return {k: v for (k, v) in properties.items() if v is not None} From b927e063c3727d52c7a0399628f10798104da5b7 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Fri, 5 Jun 2020 15:59:25 -0700 Subject: [PATCH 13/25] Remove created and modified task props --- maproulette/models/task.py | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/maproulette/models/task.py b/maproulette/models/task.py index 41178db..11ea3cb 100644 --- a/maproulette/models/task.py +++ b/maproulette/models/task.py @@ -27,24 +27,6 @@ def name(self): def name(self, value): self._name = value - @property - def created(self): - """Timestamp of when the task was created""" - return self._created - - @created.setter - def created(self, value): - self._created = value - - @property - def modified(self): - """Timestamp of when the task was modified""" - return self._modified - - @modified.setter - def modified(self, value): - self._modified = value - @property def parent(self): """The parent ID for the task""" @@ -171,13 +153,11 @@ def mapillary_images(self): def mapillary_images(self, value): self._mapillary_images = value - def __init__(self, name, parent, geometries, created=None, modified=None, id=None, instruction=None, location=None, - suggested_fix=None, status=None, mapped_on=None, review=None, priority=None, changeset_id=None, + def __init__(self, name, parent, geometries, id=None, instruction=None, location=None, suggested_fix=None, + status=None, mapped_on=None, review=None, priority=None, changeset_id=None, completion_responses=None, bundle_id=None, is_bundle_primary=None, mapillary_images=None): self._id = id self._name = name - self._created = created - self._modified = modified self._parent = parent self._geometries = geometries self._instruction = instruction @@ -198,8 +178,6 @@ def to_dict(self): properties = { "id": self._id, "name": self._name, - "created": self._created, - "modified": self._modified, "parent": self._parent, "instruction": self._instruction, "location": self._location, From fe6755d364cd2feb3dc9f3d08b0688b6c91d1915 Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Tue, 9 Jun 2020 11:44:56 -0700 Subject: [PATCH 14/25] Add remote geojson prop to challenge model --- maproulette/models/challenge.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/maproulette/models/challenge.py b/maproulette/models/challenge.py index adc7779..0df3f49 100644 --- a/maproulette/models/challenge.py +++ b/maproulette/models/challenge.py @@ -278,13 +278,22 @@ def task_styles(self): def task_styles(self, value): self._task_styles = value + @property + def remote_geojson(self): + """Create a challenge from a GeoJSON URL""" + return self._remote_geojson + + @remote_geojson.setter + def remote_geojson(self, value): + self._remote_geojson = value + def __init__(self, name, id=None, description=None, parent=None, instruction=None, difficulty=None, blurb=None, enabled=None, challenge_type=None, featured=None, overpassQL=None, default_priority=None, high_priority_rule=None, low_priority_rule=None, default_zoom=None, min_zoom=None, max_zoom=None, osm_id_property=None, cooperative_type=None, popularity=None, check_in_comment=None, check_in_source=None, requires_local=None, default_basemap=None, default_basemap_id=None, custom_basemap=None, update_tasks=None, exportable_properties=None, preferred_tags=None, - task_styles=None): + task_styles=None, remote_geojson=None): self._id = id self._name = name self._description = description @@ -315,6 +324,7 @@ def __init__(self, name, id=None, description=None, parent=None, instruction=Non self._exportable_properties = exportable_properties self._preferred_tags = preferred_tags self._task_styles = task_styles + self._remote_geojson = remote_geojson def to_dict(self): """Converts all non-null properties of a challenge object into a dictionary""" @@ -349,7 +359,8 @@ def to_dict(self): "updateTasks": self._update_tasks, "exportableProperties": self._exportable_properties, "preferredTags": self._preferred_tags, - "taskStyles": self._task_styles + "taskStyles": self._task_styles, + "remoteGeoJson": self.remote_geojson } return {k: v for (k, v) in properties.items() if v is not None} From c3d646037bd49cca2a97f6355d44067f7bf45c5a Mon Sep 17 00:00:00 2001 From: Daniel Castro Date: Tue, 9 Jun 2020 12:02:00 -0700 Subject: [PATCH 15/25] Add cooperativeWork prop to Task model --- maproulette/models/task.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/maproulette/models/task.py b/maproulette/models/task.py index 11ea3cb..f3a4112 100644 --- a/maproulette/models/task.py +++ b/maproulette/models/task.py @@ -153,9 +153,19 @@ def mapillary_images(self): def mapillary_images(self, value): self._mapillary_images = value + @property + def cooperative_work(self): + """A tag that is or will be used for a new type of task resolution""" + return self._cooperative_work + + @cooperative_work.setter + def cooperative_work(self, value): + self._cooperative_work = value + def __init__(self, name, parent, geometries, id=None, instruction=None, location=None, suggested_fix=None, status=None, mapped_on=None, review=None, priority=None, changeset_id=None, - completion_responses=None, bundle_id=None, is_bundle_primary=None, mapillary_images=None): + completion_responses=None, bundle_id=None, is_bundle_primary=None, mapillary_images=None, + cooperative_work=None): self._id = id self._name = name self._parent = parent @@ -172,6 +182,7 @@ def __init__(self, name, parent, geometries, id=None, instruction=None, location self._bundle_id = bundle_id self._is_bundle_primary = is_bundle_primary self._mapillary_images = mapillary_images + self._cooperative_work = cooperative_work def to_dict(self): """Converts all non-null properties of a task object into a dictionary""" @@ -190,7 +201,8 @@ def to_dict(self): "completionResponses": self._completion_responses, "bundleId": self._bundle_id, "isBundlePrimary": self._is_bundle_primary, - "mapillaryImages": self._mapillary_images + "mapillaryImages": self._mapillary_images, + "cooperativeWork": self._cooperative_work } return {k: v for (k, v) in properties.items() if v is not None} From f7850ef6e1cbafe59b21ccc24c4e9e9be1ce9f7c Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 9 Jun 2020 15:27:43 -0700 Subject: [PATCH 16/25] Added new priority rule structure and updated example --- examples/create_challenge_from_model.py | 11 +++-- maproulette/__init__.py | 1 + maproulette/models/__init__.py | 1 + maproulette/models/challenge.py | 1 - maproulette/models/priority_rule.py | 53 +++++++++++++++++++++++-- 5 files changed, 60 insertions(+), 7 deletions(-) diff --git a/examples/create_challenge_from_model.py b/examples/create_challenge_from_model.py index 6e26b1e..a29e028 100644 --- a/examples/create_challenge_from_model.py +++ b/examples/create_challenge_from_model.py @@ -2,7 +2,8 @@ import json # Create a configuration object for MapRoulette using your API key: -config = maproulette.Configuration(api_key="API_KEY") +config = maproulette.Configuration(api_key="2909|40fee283-0d8c-4b9d-ae4a-28850c132e66", + verify=False) # Create an API objects with the above config object: api = maproulette.Challenge(config) @@ -16,10 +17,14 @@ # Adding required instruction challenge_data.instruction = "Do something" +# Let's create a basic rule to classify features with tags 'highway' = 'footway' to be high priority +rule_1 = maproulette.PriorityRule(priority_value='highway.footway', priority_type='string', priority_operator='equal') + +# Create a formal priority rule for the challenge +challenge_data.high_priority_rule = maproulette.PriorityRuleModel(condition='OR', rules=[rule_1]).to_json() + # Adding example overpass QL input for challenge challenge_data.overpassQL = open('data/Example_OverpassQL_Query', 'r').read() -print(challenge_data.overpassQL) - # Create challenge print(json.dumps(api.create_challenge(challenge_data))) diff --git a/maproulette/__init__.py b/maproulette/__init__.py index 5d5ffe1..4cbd5e2 100644 --- a/maproulette/__init__.py +++ b/maproulette/__init__.py @@ -8,6 +8,7 @@ from .models.challenge import ChallengeModel from .models.task import TaskModel from .models.priority_rule import PriorityRuleModel +from .models.priority_rule import PriorityRule from .api.project import Project from .api.challenge import Challenge from .api.task import Task diff --git a/maproulette/models/__init__.py b/maproulette/models/__init__.py index 9ffe7e6..d43cfb5 100644 --- a/maproulette/models/__init__.py +++ b/maproulette/models/__init__.py @@ -2,3 +2,4 @@ from .challenge import ChallengeModel from .task import TaskModel from .priority_rule import PriorityRuleModel +from .priority_rule import PriorityRule diff --git a/maproulette/models/challenge.py b/maproulette/models/challenge.py index 0df3f49..533aecd 100644 --- a/maproulette/models/challenge.py +++ b/maproulette/models/challenge.py @@ -332,7 +332,6 @@ def to_dict(self): "id": self._id, "name": self._name, "description": self._description, - "deleted": self.description, "parent": self._parent, "instruction": self._instruction, "difficulty": self._difficulty, diff --git a/maproulette/models/priority_rule.py b/maproulette/models/priority_rule.py index fd1cd63..37b7c16 100644 --- a/maproulette/models/priority_rule.py +++ b/maproulette/models/priority_rule.py @@ -1,8 +1,8 @@ """This module contains the definition of a priority rule object in MapRoulette.""" import json -import os +VALID_CONDITIONS = {'OR', 'AND'} VALID_TYPES = {'string', 'integer', 'double', 'long'} VALID_STRING_OPERATORS = {'equal', 'not_equal', 'contains', 'not_contains', 'is_empty', 'is_not_empty'} VALID_NUMERIC_OPERATORS = {'==', '!=', '<', '<=', '>', '>='} @@ -11,6 +11,53 @@ class PriorityRuleModel: """Definition for a MapRoulette priority rule""" + @property + def condition(self): + """The condition (AND/OR) to use to chain together multiple priority rules""" + return self._condition + + @condition.setter + def condition(self, value): + if value not in VALID_CONDITIONS: + raise ValueError(f"Priority condition must be one of {VALID_CONDITIONS}.") + self._condition = value + + @property + def rules(self): + """The list of priority rules to be used in the priority rule model""" + return self._rules + + @rules.setter + def rules(self, value): + self._rules = value + + def __init__(self, condition, rules): + self.condition = condition + self.rules = list() + if isinstance(rules, list): + for i in rules: + if isinstance(i, PriorityRule): + self.rules.append(i.to_dict()) + else: + raise ValueError("Rules must be PriorityRule instances") + else: + raise ValueError("Rules must be a list of PriorityRule instances") + + def to_dict(self): + """Converts all properties of a priority rule model object into a dictionary""" + return { + "condition": self._condition, + "rules": self._rules + } + + def to_json(self): + """Converts all properties of a priority rule model object into a JSON object""" + return json.dumps(self.to_dict()) + + +class PriorityRule: + """Definition for a single priority rule""" + @property def priority_value(self): """The value for the priority rule""" @@ -52,7 +99,7 @@ def __init__(self, priority_value=None, priority_type=None, priority_operator=No self._priority_operator = priority_operator def to_dict(self): - """Converts all non-null properties of a project object into a dictionary""" + """Converts all properties of a priority rule object into a dictionary""" return { "value": self._priority_value, "type": self._priority_type, @@ -60,5 +107,5 @@ def to_dict(self): } def to_json(self): - """Converts all non-null properties of a project object into a JSON object""" + """Converts all properties of a priority rule object into a JSON object""" return json.dumps(self.to_dict()) From 66d27d5a349c9db792f08af77d273a1a08c2651a Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 9 Jun 2020 15:30:17 -0700 Subject: [PATCH 17/25] Updating example --- examples/create_challenge_from_model.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/create_challenge_from_model.py b/examples/create_challenge_from_model.py index a29e028..039cc64 100644 --- a/examples/create_challenge_from_model.py +++ b/examples/create_challenge_from_model.py @@ -2,8 +2,7 @@ import json # Create a configuration object for MapRoulette using your API key: -config = maproulette.Configuration(api_key="2909|40fee283-0d8c-4b9d-ae4a-28850c132e66", - verify=False) +config = maproulette.Configuration() # Create an API objects with the above config object: api = maproulette.Challenge(config) @@ -26,5 +25,7 @@ # Adding example overpass QL input for challenge challenge_data.overpassQL = open('data/Example_OverpassQL_Query', 'r').read() +print(challenge_data.overpassQL) + # Create challenge print(json.dumps(api.create_challenge(challenge_data))) From ee41637aa2fcc4d4daeb3043e73ca1022f028317 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 9 Jun 2020 15:31:30 -0700 Subject: [PATCH 18/25] Fixing example again --- examples/create_challenge_from_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/create_challenge_from_model.py b/examples/create_challenge_from_model.py index 039cc64..ac3edfa 100644 --- a/examples/create_challenge_from_model.py +++ b/examples/create_challenge_from_model.py @@ -2,7 +2,7 @@ import json # Create a configuration object for MapRoulette using your API key: -config = maproulette.Configuration() +config = maproulette.Configuration(api_key="API_KEY") # Create an API objects with the above config object: api = maproulette.Challenge(config) From d0f989f3e2b4fea154ad269b393b9b0b8d4889b4 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 9 Jun 2020 15:40:54 -0700 Subject: [PATCH 19/25] Added support for single rule --- examples/create_challenge_from_model.py | 2 +- maproulette/models/priority_rule.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/create_challenge_from_model.py b/examples/create_challenge_from_model.py index ac3edfa..3b287d6 100644 --- a/examples/create_challenge_from_model.py +++ b/examples/create_challenge_from_model.py @@ -20,7 +20,7 @@ rule_1 = maproulette.PriorityRule(priority_value='highway.footway', priority_type='string', priority_operator='equal') # Create a formal priority rule for the challenge -challenge_data.high_priority_rule = maproulette.PriorityRuleModel(condition='OR', rules=[rule_1]).to_json() +challenge_data.high_priority_rule = maproulette.PriorityRuleModel(condition='OR', rules=rule_1).to_json() # Adding example overpass QL input for challenge challenge_data.overpassQL = open('data/Example_OverpassQL_Query', 'r').read() diff --git a/maproulette/models/priority_rule.py b/maproulette/models/priority_rule.py index 37b7c16..fb7256c 100644 --- a/maproulette/models/priority_rule.py +++ b/maproulette/models/priority_rule.py @@ -40,8 +40,10 @@ def __init__(self, condition, rules): self.rules.append(i.to_dict()) else: raise ValueError("Rules must be PriorityRule instances") + elif isinstance(rules, PriorityRule): + self.rules.append(rules.to_dict()) else: - raise ValueError("Rules must be a list of PriorityRule instances") + raise ValueError("Rules must PriorityRule instances") def to_dict(self): """Converts all properties of a priority rule model object into a dictionary""" From 481fbc04d25e9cf5cfcc20b21033c8cb3292ebb1 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Tue, 9 Jun 2020 15:52:13 -0700 Subject: [PATCH 20/25] Updated docs --- docs/index.rst | 1 + docs/usage/models/priority_rule.rst | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 docs/usage/models/priority_rule.rst diff --git a/docs/index.rst b/docs/index.rst index 3854f2c..dbf1044 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ Welcome to MapRoulette's documentation! usage/models/project usage/models/challenge usage/models/task + usage/models/priority_rule usage/exceptions --------------- diff --git a/docs/usage/models/priority_rule.rst b/docs/usage/models/priority_rule.rst new file mode 100644 index 0000000..ed052a1 --- /dev/null +++ b/docs/usage/models/priority_rule.rst @@ -0,0 +1,8 @@ +Priority Rule Model +===================================================== + +.. automodule:: maproulette.models.priority_rule + :members: + :undoc-members: + :inherited-members: + :show-inheritance: From a65566affac5a9a311fca4099870cc0d8bbebc5d Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Wed, 10 Jun 2020 16:24:44 -0700 Subject: [PATCH 21/25] Added enums --- docs/usage/models/priority_rule.rst | 25 +++- examples/create_challenge_from_model.py | 8 +- maproulette/__init__.py | 3 +- maproulette/models/__init__.py | 3 +- maproulette/models/priority_rule.py | 154 ++++++++++++++++++------ 5 files changed, 145 insertions(+), 48 deletions(-) diff --git a/docs/usage/models/priority_rule.rst b/docs/usage/models/priority_rule.rst index ed052a1..ef0027f 100644 --- a/docs/usage/models/priority_rule.rst +++ b/docs/usage/models/priority_rule.rst @@ -1,8 +1,27 @@ Priority Rule Model ===================================================== -.. automodule:: maproulette.models.priority_rule +.. autoclass:: maproulette.models.priority_rule.PriorityRule + :members: + +.. autoclass:: maproulette.models.priority_rule.PriorityRuleModel + :members: + +.. autoclass:: maproulette.models.priority_rule.Conditions + :members: + +.. autoclass:: maproulette.models.priority_rule.Types + :members: + :undoc-members: + +.. autoclass:: maproulette.models.priority_rule.Conditions + :members: + :undoc-members: + +.. autoclass:: maproulette.models.priority_rule.StringOperators + :members: + :undoc-members: + +.. autoclass:: maproulette.models.priority_rule.NumericOperators :members: :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/examples/create_challenge_from_model.py b/examples/create_challenge_from_model.py index 3b287d6..15f9f79 100644 --- a/examples/create_challenge_from_model.py +++ b/examples/create_challenge_from_model.py @@ -17,10 +17,14 @@ challenge_data.instruction = "Do something" # Let's create a basic rule to classify features with tags 'highway' = 'footway' to be high priority -rule_1 = maproulette.PriorityRule(priority_value='highway.footway', priority_type='string', priority_operator='equal') +rule_1 = maproulette.PriorityRule(priority_value='highway.footway', + priority_type=maproulette.Types.STRING, + priority_operator=maproulette.StringOperators.EQUAL) # Create a formal priority rule for the challenge -challenge_data.high_priority_rule = maproulette.PriorityRuleModel(condition='OR', rules=rule_1).to_json() +challenge_data.high_priority_rule = maproulette.PriorityRuleModel(condition=maproulette.Conditions.OR, + rules=rule_1 + ).to_json() # Adding example overpass QL input for challenge challenge_data.overpassQL = open('data/Example_OverpassQL_Query', 'r').read() diff --git a/maproulette/__init__.py b/maproulette/__init__.py index 4cbd5e2..d791b3e 100644 --- a/maproulette/__init__.py +++ b/maproulette/__init__.py @@ -7,8 +7,7 @@ from .models.project import ProjectModel from .models.challenge import ChallengeModel from .models.task import TaskModel -from .models.priority_rule import PriorityRuleModel -from .models.priority_rule import PriorityRule +from .models.priority_rule import * from .api.project import Project from .api.challenge import Challenge from .api.task import Task diff --git a/maproulette/models/__init__.py b/maproulette/models/__init__.py index d43cfb5..117148b 100644 --- a/maproulette/models/__init__.py +++ b/maproulette/models/__init__.py @@ -1,5 +1,4 @@ from .project import ProjectModel from .challenge import ChallengeModel from .task import TaskModel -from .priority_rule import PriorityRuleModel -from .priority_rule import PriorityRule +from .priority_rule import * diff --git a/maproulette/models/priority_rule.py b/maproulette/models/priority_rule.py index fb7256c..17e9468 100644 --- a/maproulette/models/priority_rule.py +++ b/maproulette/models/priority_rule.py @@ -1,26 +1,73 @@ """This module contains the definition of a priority rule object in MapRoulette.""" import json +import enum -VALID_CONDITIONS = {'OR', 'AND'} -VALID_TYPES = {'string', 'integer', 'double', 'long'} -VALID_STRING_OPERATORS = {'equal', 'not_equal', 'contains', 'not_contains', 'is_empty', 'is_not_empty'} -VALID_NUMERIC_OPERATORS = {'==', '!=', '<', '<=', '>', '>='} + +class ExtendedEnum(enum.Enum): + @classmethod + def list(cls): + return [i.value for i in cls] + + +class Conditions(ExtendedEnum): + """An enumeration of valid logical conditions for a priority rule object""" + OR = "OR" + AND = "AND" + + +class Types(ExtendedEnum): + """An enumeration of valid types for a priority rule object""" + STRING = "string" + INTEGER = "integer" + DOUBLE = "double" + LONG = "long" + + +class StringOperators(ExtendedEnum): + """An enumeration of valid string operators for a priority rule object""" + EQUAL = "equal" + NOT_EQUAL = "not_equal" + CONTAINS = "contains" + NOT_CONTAINS = "not_contains" + IS_EMPTY = "is_empty" + IS_NOT_EMPTY = "is_not_empty" + + +class NumericOperators(ExtendedEnum): + """An enumeration of valid numeric operators for a priority rule object""" + EQUAL_TO = "==" + NOT_EQUAL_TO = "!=" + LESS_THAN = "<" + LESS_THAN_OR_EQUAL = "<=" + GREATER_THAN = ">" + GREATER_THAN_OR_EQUAL = ">=" class PriorityRuleModel: - """Definition for a MapRoulette priority rule""" + """A model for a priority rule definition in MapRoulette. + + :param condition: the logical condition to use to string together multiple rules. The valid options for conditions + are defined by the :class:`~maproulette.models.priority_rule.Conditions` enum. + :type condition: Conditions or str + :param rules: one or more rules to use for the priority rule definition. Rules should be instances of the + :class:`~maproulette.models.priority_rule.PriorityRule` class. + :type rules: PriorityRule or list + """ @property def condition(self): - """The condition (AND/OR) to use to chain together multiple priority rules""" + """The condition to use to chain together multiple priority rules""" return self._condition @condition.setter def condition(self, value): - if value not in VALID_CONDITIONS: - raise ValueError(f"Priority condition must be one of {VALID_CONDITIONS}.") - self._condition = value + if isinstance(value, Conditions): + self._condition = value.value + else: + if value not in Conditions.list(): + raise ValueError(f"Priority condition must be one of {Conditions.list()}.") + self._condition = value @property def rules(self): @@ -32,14 +79,18 @@ def rules(self, value): self._rules = value def __init__(self, condition, rules): + """The constructor for the PriorityRuleModel class""" self.condition = condition self.rules = list() if isinstance(rules, list): - for i in rules: - if isinstance(i, PriorityRule): - self.rules.append(i.to_dict()) - else: - raise ValueError("Rules must be PriorityRule instances") + if len(rules) > 0: + for i in rules: + if isinstance(i, PriorityRule): + self.rules.append(i.to_dict()) + else: + raise ValueError("Rules must be PriorityRule instances") + else: + ValueError("Rule list cannot be empty") elif isinstance(rules, PriorityRule): self.rules.append(rules.to_dict()) else: @@ -58,16 +109,21 @@ def to_json(self): class PriorityRule: - """Definition for a single priority rule""" - - @property - def priority_value(self): - """The value for the priority rule""" - return self._priority_value - - @priority_value.setter - def priority_value(self, value): - self._priority_value = value + """Definition for a single priority rule + + :param priority_type: the data type for the priority rule. The valid options are defined by the + :class:`~maproulette.models.priority_rule.Types` enum. + :type priority_type: Types or str + :param priority_operator: the operator to use for the priority rule. The valid options for string-type priority + rules are defined by the :class:`~maproulette.models.priority_rule.StringOperators` enum and the valid options + for numeric-type priority rules are defined by the :class:`~maproulette.models.priority_rule.NumericOperators` + enum. + :type priority_operator: NumericOperators or StringOperators or str + :param priority_value: the value to use for the priority rule. This should be formatted like 'highway.footway' to + indicate that any task with a 'highway' property equal to 'footway' should be considered for this rule. Multiple + values can be specified using commas. Example: 'highway.footway,pedestrian'. + :type priority_value: str + """ @property def priority_type(self): @@ -76,29 +132,49 @@ def priority_type(self): @priority_type.setter def priority_type(self, value): - if value not in VALID_TYPES: - raise ValueError(f"Priority type must be one of {VALID_TYPES}.") - self._priority_type = value + if isinstance(value, Types): + self._priority_type = value.value + elif value in Types.list(): + self._priority_type = value + else: + raise ValueError(f"Priority types must be one of {Types.list()}.") @property def priority_operator(self): - """The type for the priority rule""" + """The operator to use for the priority rule""" return self._priority_operator @priority_operator.setter def priority_operator(self, value): - if self.priority_type == 'string': - if value not in VALID_STRING_OPERATORS: - raise ValueError(f"Priority operator must be one of {VALID_STRING_OPERATORS}.") + if self._priority_type == 'string': + if isinstance(value, StringOperators): + self._priority_operator = value.value + elif value in StringOperators.list(): + self._priority_operator = value + else: + raise ValueError(f"String type priority operators must be one of {StringOperators.list()}.") else: - if value not in VALID_NUMERIC_OPERATORS: - raise ValueError(f"Priority operator must be one of {VALID_NUMERIC_OPERATORS}.") - self._priority_operator = value - - def __init__(self, priority_value=None, priority_type=None, priority_operator=None): - self._priority_value = priority_value - self._priority_type = priority_type - self._priority_operator = priority_operator + if isinstance(value, NumericOperators): + self._priority_operator = value.value + elif value in NumericOperators.list(): + self._priority_operator = value + else: + raise ValueError(f"Numeric type priority operators must be one of {NumericOperators.list()}.") + + @property + def priority_value(self): + """The value for the priority rule""" + return self._priority_value + + @priority_value.setter + def priority_value(self, value): + self._priority_value = value + + def __init__(self, priority_type, priority_operator, priority_value): + """The constructor for the PriorityRule class""" + self.priority_type = priority_type + self.priority_operator = priority_operator + self.priority_value = priority_value def to_dict(self): """Converts all properties of a priority rule object into a dictionary""" From bcf6d3894c8f15021472fdde919f49c9fb2f90f7 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Thu, 11 Jun 2020 13:02:18 -0700 Subject: [PATCH 22/25] Updated enums and importing strategy --- examples/create_challenge_from_model.py | 6 ++--- maproulette/__init__.py | 4 +++- maproulette/models/__init__.py | 2 +- maproulette/models/priority_rule.py | 29 ++++++++++++++----------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/examples/create_challenge_from_model.py b/examples/create_challenge_from_model.py index 15f9f79..903baa6 100644 --- a/examples/create_challenge_from_model.py +++ b/examples/create_challenge_from_model.py @@ -18,11 +18,11 @@ # Let's create a basic rule to classify features with tags 'highway' = 'footway' to be high priority rule_1 = maproulette.PriorityRule(priority_value='highway.footway', - priority_type=maproulette.Types.STRING, - priority_operator=maproulette.StringOperators.EQUAL) + priority_type=maproulette.priority_rule.Types.STRING, + priority_operator=maproulette.priority_rule.StringOperators.EQUAL) # Create a formal priority rule for the challenge -challenge_data.high_priority_rule = maproulette.PriorityRuleModel(condition=maproulette.Conditions.OR, +challenge_data.high_priority_rule = maproulette.PriorityRuleModel(condition=maproulette.priority_rule.Conditions.OR, rules=rule_1 ).to_json() diff --git a/maproulette/__init__.py b/maproulette/__init__.py index d791b3e..ce91270 100644 --- a/maproulette/__init__.py +++ b/maproulette/__init__.py @@ -7,7 +7,9 @@ from .models.project import ProjectModel from .models.challenge import ChallengeModel from .models.task import TaskModel -from .models.priority_rule import * +from .models.priority_rule import PriorityRule +from .models.priority_rule import PriorityRuleModel +from .models import priority_rule from .api.project import Project from .api.challenge import Challenge from .api.task import Task diff --git a/maproulette/models/__init__.py b/maproulette/models/__init__.py index 117148b..89d6eb5 100644 --- a/maproulette/models/__init__.py +++ b/maproulette/models/__init__.py @@ -1,4 +1,4 @@ from .project import ProjectModel from .challenge import ChallengeModel from .task import TaskModel -from .priority_rule import * +from .priority_rule import PriorityRule, PriorityRuleModel diff --git a/maproulette/models/priority_rule.py b/maproulette/models/priority_rule.py index 17e9468..518e28c 100644 --- a/maproulette/models/priority_rule.py +++ b/maproulette/models/priority_rule.py @@ -1,10 +1,13 @@ """This module contains the definition of a priority rule object in MapRoulette.""" import json -import enum +from enum import Enum, auto -class ExtendedEnum(enum.Enum): +class ExtendedEnum(Enum): + def _generate_next_value_(name, start, count, last_values): + return name.lower() + @classmethod def list(cls): return [i.value for i in cls] @@ -18,20 +21,20 @@ class Conditions(ExtendedEnum): class Types(ExtendedEnum): """An enumeration of valid types for a priority rule object""" - STRING = "string" - INTEGER = "integer" - DOUBLE = "double" - LONG = "long" + STRING = auto() + INTEGER = auto() + DOUBLE = auto() + LONG = auto() class StringOperators(ExtendedEnum): """An enumeration of valid string operators for a priority rule object""" - EQUAL = "equal" - NOT_EQUAL = "not_equal" - CONTAINS = "contains" - NOT_CONTAINS = "not_contains" - IS_EMPTY = "is_empty" - IS_NOT_EMPTY = "is_not_empty" + EQUAL = auto() + NOT_EQUAL = auto() + CONTAINS = auto() + NOT_CONTAINS = auto() + IS_EMPTY = auto() + IS_NOT_EMPTY = auto() class NumericOperators(ExtendedEnum): @@ -94,7 +97,7 @@ def __init__(self, condition, rules): elif isinstance(rules, PriorityRule): self.rules.append(rules.to_dict()) else: - raise ValueError("Rules must PriorityRule instances") + raise ValueError("Rules must be PriorityRule instances") def to_dict(self): """Converts all properties of a priority rule model object into a dictionary""" From b7a0bbf8d1ec8d1ed11dbcf179cdd5d0df2cbe86 Mon Sep 17 00:00:00 2001 From: Daniel B Date: Thu, 11 Jun 2020 13:46:26 -0700 Subject: [PATCH 23/25] add badges to readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 608c8a1..8abd548 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # MapRoulette - A Python Client for the MapRoulette API -https://maproulette-python-client.readthedocs.io/ + +https://maproulette-python-client.readthedocs.io + +[![PyPI version](https://badge.fury.io/py/maproulette.svg)](https://badge.fury.io/py/maproulette) +[![Build Status](https://travis-ci.com/osmlab/maproulette-python-client.svg?branch=master)](https://travis-ci.com/osmlab/maproulette-python-client) This client makes it easy for users to communicate with the MapRoulette API from within their Python environment. In the example below, we are able to access a MapRoulette project in just four lines of code: From 9eb5c17889ec83175752ea4ae1e8090c711a5295 Mon Sep 17 00:00:00 2001 From: Matt Manley Date: Thu, 11 Jun 2020 13:47:56 -0700 Subject: [PATCH 24/25] Moved imports to single line --- maproulette/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/maproulette/__init__.py b/maproulette/__init__.py index ce91270..56dea3c 100644 --- a/maproulette/__init__.py +++ b/maproulette/__init__.py @@ -7,8 +7,7 @@ from .models.project import ProjectModel from .models.challenge import ChallengeModel from .models.task import TaskModel -from .models.priority_rule import PriorityRule -from .models.priority_rule import PriorityRuleModel +from .models.priority_rule import PriorityRule, PriorityRuleModel from .models import priority_rule from .api.project import Project from .api.challenge import Challenge From 31bd947cdb3c4883e1dcc260f168c11c02331e1a Mon Sep 17 00:00:00 2001 From: mattmanley Date: Fri, 12 Jun 2020 11:20:36 -0700 Subject: [PATCH 25/25] Update __init__.py Version bump --- maproulette/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maproulette/__init__.py b/maproulette/__init__.py index 56dea3c..4ea4ccc 100644 --- a/maproulette/__init__.py +++ b/maproulette/__init__.py @@ -14,4 +14,4 @@ from .api.task import Task from .api.user import User -__version__ = '1.0.0' +__version__ = '1.1.0'