From f5eb7f8a0b5f81324196d0e38c43587084db5f4c Mon Sep 17 00:00:00 2001 From: Jose Bogarin Solano Date: Sun, 17 Sep 2017 21:10:18 -0600 Subject: [PATCH 01/31] Include orgId parameter for people list method --- ciscosparkapi/api/people.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 73d5456..6fe6823 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -137,7 +137,7 @@ def __init__(self, session): self._session = session @generator_container - def list(self, email=None, displayName=None, max=None): + def list(self, email=None, displayName=None, orgId=None, max=None): """List people This method supports Cisco Spark's implementation of RFC5988 Web @@ -154,6 +154,7 @@ def list(self, email=None, displayName=None, max=None): email(basestring): The e-mail address of the person to be found. displayName(basestring): The complete or beginning portion of the displayName to be searched. + orgId(basestring): The organization id. max(int): Limits the maximum number of people returned from the Spark service per request. @@ -169,12 +170,15 @@ def list(self, email=None, displayName=None, max=None): # Process args assert email is None or isinstance(email, basestring) assert displayName is None or isinstance(displayName, basestring) + assert orgId is None or isinstance(orgId, basestring) assert max is None or isinstance(max, int) params = {} if email: params['email'] = email elif displayName: params['displayName'] = displayName + if orgId: + params["orgId"] = orgId if max: params['max'] = max # API request - get items From 92445c14a0f376dcc2f48294157337b7ed7c9fd2 Mon Sep 17 00:00:00 2001 From: Jose Bogarin Solano Date: Wed, 20 Sep 2017 23:19:03 -0600 Subject: [PATCH 02/31] Include the id param to the people list method. No other param can be used with this one. The Spark error message is pretty clear: > GET by ID cannot also other query filters, or a 'max' parameter --- ciscosparkapi/api/people.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 6fe6823..e88322e 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -137,7 +137,7 @@ def __init__(self, session): self._session = session @generator_container - def list(self, email=None, displayName=None, orgId=None, max=None): + def list(self, email=None, displayName=None, orgId=None, id=None, max=None): """List people This method supports Cisco Spark's implementation of RFC5988 Web @@ -154,6 +154,7 @@ def list(self, email=None, displayName=None, orgId=None, max=None): email(basestring): The e-mail address of the person to be found. displayName(basestring): The complete or beginning portion of the displayName to be searched. + id(basestring): List people by ID. Accepts up to 85 person IDs separated by commas. orgId(basestring): The organization id. max(int): Limits the maximum number of people returned from the Spark service per request. @@ -171,16 +172,20 @@ def list(self, email=None, displayName=None, orgId=None, max=None): assert email is None or isinstance(email, basestring) assert displayName is None or isinstance(displayName, basestring) assert orgId is None or isinstance(orgId, basestring) + assert id is None or isinstance(id, basestring) assert max is None or isinstance(max, int) params = {} - if email: - params['email'] = email - elif displayName: - params['displayName'] = displayName - if orgId: - params["orgId"] = orgId - if max: - params['max'] = max + if id: + params["id"] = id + else: + if email: + params['email'] = email + elif displayName: + params['displayName'] = displayName + if orgId: + params["orgId"] = orgId + if max: + params['max'] = max # API request - get items items = self._session.get_items('people', params=params) # Yield Person objects created from the returned items JSON objects From 654753380acfdd9f2723e0f047e92b65bc5cc471 Mon Sep 17 00:00:00 2001 From: Jose Bogarin Solano Date: Thu, 21 Sep 2017 08:15:31 -0600 Subject: [PATCH 03/31] Update people list method to include **query_params --- ciscosparkapi/api/people.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index e88322e..540ccdf 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -137,7 +137,7 @@ def __init__(self, session): self._session = session @generator_container - def list(self, email=None, displayName=None, orgId=None, id=None, max=None): + def list(self, email=None, displayName=None, orgId=None, id=None, max=None, **query_params): """List people This method supports Cisco Spark's implementation of RFC5988 Web @@ -186,6 +186,9 @@ def list(self, email=None, displayName=None, orgId=None, id=None, max=None): params["orgId"] = orgId if max: params['max'] = max + # Process query_param keyword arguments + if query_params: + params.update(query_params) # API request - get items items = self._session.get_items('people', params=params) # Yield Person objects created from the returned items JSON objects From fac6308bed53845aba65641d30a716c31f2c7ebf Mon Sep 17 00:00:00 2001 From: Jose Bogarin Solano Date: Thu, 21 Sep 2017 08:59:36 -0600 Subject: [PATCH 04/31] Change how the id parameter is being handled and include the related test --- ciscosparkapi/api/people.py | 15 +++++++-------- tests/api/test_people.py | 6 ++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 540ccdf..9b6c359 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -177,15 +177,14 @@ def list(self, email=None, displayName=None, orgId=None, id=None, max=None, **qu params = {} if id: params["id"] = id - else: - if email: + elif email: params['email'] = email - elif displayName: - params['displayName'] = displayName - if orgId: - params["orgId"] = orgId - if max: - params['max'] = max + elif displayName: + params['displayName'] = displayName + if orgId: + params["orgId"] = orgId + if max: + params['max'] = max # Process query_param keyword arguments if query_params: params.update(query_params) diff --git a/tests/api/test_people.py b/tests/api/test_people.py index 712fba5..73e1557 100644 --- a/tests/api/test_people.py +++ b/tests/api/test_people.py @@ -180,6 +180,12 @@ def test_list_people_by_display_name(self, api, test_people): assert len(list_of_people) >= 1 assert are_valid_people(list_of_people) + def test_list_people_by_id(self, api, test_people): + id = test_people["not_a_member"].id + list_of_people = list_people(api, id=id) + assert len(list_of_people) >= 1 + assert are_valid_people(list_of_people) + def test_list_people_with_paging(self, api, test_people, additional_group_room_memberships): page_size = 1 From 3b662344e1062262b154da8429eb84a006dab36d Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Thu, 21 Sep 2017 15:32:06 -0400 Subject: [PATCH 05/31] Add new attributes to `Person` class & Update some DocStrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the following new Spark attributes to the `Person` object class: type nickName invitePending loginEnabled Update any docstrings that don’t look right. ;-) --- ciscosparkapi/api/people.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 9b6c359..096fe44 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -35,10 +35,10 @@ class Person(SparkData): """Model a Spark 'person' JSON object as a native Python object.""" def __init__(self, json): - """Init a new Person data object from a JSON dictionary or string. + """Initialize a Person data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -46,6 +46,11 @@ def __init__(self, json): """ super(Person, self).__init__(json) + @property + def type(self): + """The type of object returned by Cisco Spark (should be `person`).""" + return self._json.get('type') + @property def id(self): """The person's unique ID.""" @@ -67,6 +72,11 @@ def displayName(self): """Full name of the person.""" return self._json.get('displayName') + @property + def nickName(self): + """'Nick name' or preferred short name of the person.""" + return self._json.get('nickName') + @property def firstName(self): """First name of the person.""" @@ -112,17 +122,27 @@ def lastActivity(self): """The date and time of the person's last activity.""" return self._json.get('lastActivity') + @property + def invitePending(self): + """Person has been sent an invite, but hasn't responded.""" + return self._json.get('invitePending') + + @property + def loginEnabled(self): + """Person is allowed to login.""" + return self._json.get('loginEnabled') + class PeopleAPI(object): """Cisco Spark People-API wrapper class. - Wrappers the Cisco Spark People-API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark People-API and exposes the APIs as native Python + methods that return native Python objects. """ def __init__(self, session): - """Init a new PeopleAPI object with the provided RestSession. + """Initialize a new PeopleAPI object with the provided RestSession. Args: session(RestSession): The RESTful session object to be used for From 1630ce1243b1913242ca1cb87892c2817a3ec341 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Thu, 5 Oct 2017 16:43:15 -0400 Subject: [PATCH 06/31] Update docstrings --- ciscosparkapi/sparkdata.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ciscosparkapi/sparkdata.py b/ciscosparkapi/sparkdata.py index 7d04ea6..131e650 100644 --- a/ciscosparkapi/sparkdata.py +++ b/ciscosparkapi/sparkdata.py @@ -2,8 +2,8 @@ """SparkData base-class; models Spark JSON objects as native Python objects. The SparkData class models any JSON object passed to it as a string or Python -dictionary as a native Python object; providing attribute access access using -native object.attribute syntax. +dictionary as a native Python object; providing attribute access using native +dot-syntax (`object.attribute`). SparkData is intended to serve as a base-class, which provides inheritable functionality, for concrete sub-classes that model specific Cisco Spark data @@ -14,11 +14,11 @@ to objects by the Cisco Spark cloud. Example: - >>> json_obj = '{"created": "2012-06-15T20:36:48.914Z", - "displayName": "Chris Lunsford (chrlunsf)", - "id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mZjhlZTZmYi1hZm...", - "avatar": "https://1efa7a94ed216783e352-c622665287144...", - "emails": ["chrlunsf@cisco.com"]}' + >>> json_obj = '''{"created": "2012-06-15T20:36:48.914Z", + "displayName": "Chris Lunsford (chrlunsf)", + "id": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mZjhlZTZmYi1h...", + "avatar": "https://1efa7a94ed216783e352-c6226652871...", + "emails": ["chrlunsf@cisco.com"]}''' >>> python_obj = SparkData(json_obj) >>> python_obj.displayName u'Chris Lunsford (chrlunsf)' @@ -48,7 +48,7 @@ def _json_dict(json): - """Given a JSON dictionary or string; return a dictionary. + """Given a dictionary or JSON string; return a dictionary. Args: json(dict, str): Input JSON object. @@ -74,7 +74,7 @@ class SparkData(object): """Model Spark JSON objects as native Python objects.""" def __init__(self, json): - """Init a new SparkData object from a JSON dictionary or string. + """Init a new SparkData object from a dictionary or JSON string. Args: json(dict, str): Input JSON object. @@ -87,7 +87,7 @@ def __init__(self, json): self._json = _json_dict(json) def __getattr__(self, item): - """Provide native attribute access to the JSON object's attributes. + """Provide native attribute access to the JSON object attributes. This method is called when attempting to access a object attribute that hasn't been defined for the object. For example trying to access From eaafb62cb7709d76fdaf16749496e37752ada23a Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Thu, 5 Oct 2017 16:49:31 -0400 Subject: [PATCH 07/31] Update the PeopleAPI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure all currently documented Spark API request parameters are represented in the package’s API. Refactor the request methods to defer request-construction errors to the Spark-API (future proofing and simplifies the number of exception types that need to be caught). Refactor parameter type checking to utilize a common package utility function, which raises `TypeError`s when parameter types are incorrect (more accurate exception representation - TypeError as opposed to AssertionError). --- ciscosparkapi/api/people.py | 250 ++++++++++++++++++++---------------- ciscosparkapi/utils.py | 54 ++++++++ 2 files changed, 193 insertions(+), 111 deletions(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 096fe44..dd31714 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -3,8 +3,8 @@ Classes: Person: Models a Spark 'person' JSON object as a native Python object. - PeopleAPI: Wrappers the Cisco Spark People-API and exposes the API calls as - Python method calls that return native Python objects. + PeopleAPI: Wraps the Cisco Spark People-API and exposes the APIs as native + Python methods that return native Python objects. """ @@ -19,8 +19,11 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.exceptions import ciscosparkapiException -from ciscosparkapi.utils import generator_container +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData @@ -32,7 +35,7 @@ class Person(SparkData): - """Model a Spark 'person' JSON object as a native Python object.""" + """Model a Spark person JSON object as a native Python object.""" def __init__(self, json): """Initialize a Person data object from a dictionary or JSON string. @@ -58,13 +61,7 @@ def id(self): @property def emails(self): - """Email address(es) of the person. - - CURRENT LIMITATION: Spark (today) only allows you to provide a single - email address for a person. The list data type was selected to enable - future support for providing multiple email address. - - """ + """Email address(es) of the person.""" return self._json['emails'] @property @@ -149,20 +146,23 @@ def __init__(self, session): API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession) + super(PeopleAPI, self).__init__() + self._session = session @generator_container - def list(self, email=None, displayName=None, orgId=None, id=None, max=None, **query_params): + def list(self, email=None, displayName=None, id=None, orgId=None, max=None, + **query_params): """List people This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator - container that incrementally yield all people returned by the + container that incrementally yields all people returned by the query. The generator will automatically request additional 'pages' of responses from Spark as needed until all responses have been returned. The container makes the generator safe for reuse. A new API call will @@ -174,163 +174,190 @@ def list(self, email=None, displayName=None, orgId=None, id=None, max=None, **qu email(basestring): The e-mail address of the person to be found. displayName(basestring): The complete or beginning portion of the displayName to be searched. - id(basestring): List people by ID. Accepts up to 85 person IDs separated by commas. + id(basestring): List people by ID. Accepts up to 85 person IDs + separated by commas. orgId(basestring): The organization id. max(int): Limits the maximum number of people returned from the Spark service per request. + **query_params: Additional query parameters (provides support for + query parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the people returned by the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the people returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert email is None or isinstance(email, basestring) - assert displayName is None or isinstance(displayName, basestring) - assert orgId is None or isinstance(orgId, basestring) - assert id is None or isinstance(id, basestring) - assert max is None or isinstance(max, int) - params = {} - if id: - params["id"] = id - elif email: - params['email'] = email - elif displayName: - params['displayName'] = displayName - if orgId: - params["orgId"] = orgId - if max: - params['max'] = max - # Process query_param keyword arguments - if query_params: - params.update(query_params) + check_type(id, basestring) + check_type(email, basestring) + check_type(displayName, basestring) + check_type(orgId, basestring) + check_type(max, int) + + params = dict_from_items_with_values( + query_params, + id=id, + email=email, + displayName=displayName, + orgId=orgId, + max=max, + ) + # API request - get items items = self._session.get_items('people', params=params) + # Yield Person objects created from the returned items JSON objects for item in items: yield Person(item) - def create(self, emails, **person_attributes): + def create(self, emails, displayName=None, firstName=None, lastName=None, + avatar=None, orgId=None, roles=None, licenses=None, + **person_attributes): """Create a new user account for a given organization Only an admin can create a new user account. - You must specify displayName and/or firstName and lastName. - Args: - emails(list): Email address(es) of the person. (list of strings) - CURRENT LIMITATION: Spark (today) only allows you to provide a - single email address for a person. The list data type was - selected to enable future support for providing multiple email - address. - **person_attributes - displayName(basestring): Full name of the person - firstName(basestring): First name of the person - lastName(basestring): Last name of the person - avatar(basestring): URL to the person's avatar in PNG format + emails(list): Email address(es) of the person (list of strings). + displayName(basestring): Full name of the person. + firstName(basestring): First name of the person. + lastName(basestring): Last name of the person. + avatar(basestring): URL to the person's avatar in PNG format. orgId(basestring): ID of the organization to which this - person belongs + person belongs. roles(list): Roles of the person (list of strings containing - the role IDs to be assigned to the person) + the role IDs to be assigned to the person). licenses(list): Licenses allocated to the person (list of - strings containing the license IDs to be allocated to the - person) + strings - containing the license IDs to be allocated to the + person). + **person_attributes: Additional person attributes (provides support + for attributes that may be added in the future). Returns: - Person: With the details of the created person. + Person: A Person object with the details of the created person. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If required parameters have been omitted. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(emails, list) and len(emails) == 1 - post_data = {} - post_data['emails'] = emails - post_data.update(person_attributes) + check_type(emails, list, may_be_none=False) + check_type(displayName, basestring) + check_type(firstName, basestring) + check_type(lastName, basestring) + check_type(avatar, basestring) + check_type(orgId, basestring) + check_type(roles, list) + check_type(licenses, list) + + post_data = dict_from_items_with_values( + person_attributes, + emails=emails, + displayName=displayName, + firstName=firstName, + lastName=lastName, + avatar=avatar, + orgId=orgId, + roles=roles, + licenses=licenses, + ) # API request - json_obj = self._session.post('people', json=post_data) + json_data = self._session.post('people', json=post_data) # Return a Room object created from the returned JSON object - return Person(json_obj) + return Person(json_data) - def update(self, personId, **person_attributes): + def update(self, personId, emails=None, displayName=None, firstName=None, + lastName=None, avatar=None, orgId=None, roles=None, + licenses=None, **person_attributes): """Update details for a person, by ID. - Only an admin can update a person details. + Only an admin can update a person's details. + + Email addresses for a person cannot be changed via the Spark API. + + Include all details for the person. This action expects all user + details to be present in the request. A common approach is to first GET + the person's details, make changes, then PUT both the changed and + unchanged values. Args: - personId(basestring): The ID of the person to be updated. - **person_attributes - emails(list): Email address(es) of the person. (list of - strings) CURRENT LIMITATION: Spark (today) only allows you - to provide a single email address for a person. The list - data type was selected to enable future support for - providing multiple email address. - displayName(basestring): Full name of the person - firstName(basestring): First name of the person - lastName(basestring): Last name of the person - avatar(basestring): URL to the person's avatar in PNG format + personId(basestring): The 'id' of the person to be updated. + emails(list): Email address(es) of the person (list of strings). + displayName(basestring): Full name of the person. + firstName(basestring): First name of the person. + lastName(basestring): Last name of the person. + avatar(basestring): URL to the person's avatar in PNG format. orgId(basestring): ID of the organization to which this - person belongs + person belongs. roles(list): Roles of the person (list of strings containing - the role IDs to be assigned to the person) + the role IDs to be assigned to the person). licenses(list): Licenses allocated to the person (list of - strings containing the license IDs to be allocated to the - person) + strings - containing the license IDs to be allocated to the + person). + **person_attributes: Additional person attributes (provides support + for attributes that may be added in the future). Returns: - Person: With the updated person details. + Person: A Person object with the updated details. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If an update attribute is not provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(personId, basestring) - - # Process update_attributes keyword arguments - if not person_attributes: - error_message = "At least one **update_attributes keyword " \ - "argument must be specified." - raise ciscosparkapiException(error_message) + check_type(emails, list) + check_type(displayName, basestring) + check_type(firstName, basestring) + check_type(lastName, basestring) + check_type(avatar, basestring) + check_type(orgId, basestring) + check_type(roles, list) + check_type(licenses, list) + + put_data = dict_from_items_with_values( + person_attributes, + emails=emails, + displayName=displayName, + firstName=firstName, + lastName=lastName, + avatar=avatar, + orgId=orgId, + roles=roles, + licenses=licenses, + ) # API request - json_obj = self._session.put('people/' + personId, - json=person_attributes) + json_data = self._session.put('people/' + personId, json=put_data) # Return a Person object created from the returned JSON object - return Person(json_obj) + return Person(json_data) def get(self, personId): """Get person details, by personId. Args: - personId(basestring): The personID of the person. + personId(basestring): The 'id' of the person to be retrieved. Returns: - Person: With the details of the requested person. + Person: A Person object with the details of the requested person. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(personId, basestring) + check_type(personId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('people/' + personId) + json_data = self._session.get('people/' + personId) + # Return a Person object created from the response JSON data - return Person(json_obj) + return Person(json_data) def delete(self, personId): """Remove a person from the system. @@ -338,26 +365,27 @@ def delete(self, personId): Only an admin can remove a person. Args: - personId(basestring): The personID of the person. + personId(basestring): The 'id' of the person to be deleted. Raises: AssertionError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(personId, basestring) + check_type(personId, basestring, may_be_none=False) + # API request self._session.delete('people/' + personId) def me(self): - """Get the person details of the account accessing the API 'me'. + """Get the details of the person accessing the API. Raises: SparkApiError: If the Cisco Spark cloud returns an error. """ # API request - json_obj = self._session.get('people/me') + json_data = self._session.get('people/me') + # Return a Person object created from the response JSON data - return Person(json_obj) + return Person(json_data) diff --git a/ciscosparkapi/utils.py b/ciscosparkapi/utils.py index 29c34dc..0c9cfa9 100644 --- a/ciscosparkapi/utils.py +++ b/ciscosparkapi/utils.py @@ -75,6 +75,60 @@ def open_local_file(file_path): content_type=content_type) +def check_type(o, acceptable_types, may_be_none=True): + """Object is an instance of one of the acceptable types or None. + + Args: + o: The object to be inspected. + acceptable_types: A type or tuple of acceptable types. + may_be_none(bool): Whether or not the object may be None. + + Raises: + TypeError: If the object is None and may_be_none=False, or if the + object is not an instance of one of the acceptable types. + + """ + if may_be_none and o is None: + # Object is None, and that is OK! + pass + elif isinstance(o, acceptable_types): + # Object is an instance of an acceptable type. + pass + else: + # Object is something else. + error_message = ( + "We were expecting to receive an instance of one of the following " + "types: {types}{none}; but instead we received {o} which is a " + "{o_type}.".format( + types=", ".join([repr(t.__name__) for t in acceptable_types]), + none="or 'None'" if may_be_none else "", + o=o, + o_type=repr(type(o).__name__) + ) + ) + raise TypeError(error_message) + + +def dict_from_items_with_values(*dictionaries, **items): + """Creates a dict with the inputted items; pruning any that are `None`. + + Args: + *dictionaries(dict): Dictionaries of items to be pruned and included. + **items: Items to be pruned and included. + + Returns: + dict: A dictionary containing all of the items with a 'non-None' value. + + """ + dictionaries.append(items) + result = {} + for d in dictionaries: + for key, value in d.items(): + if value is not None: + result[key] = value + return result + + def raise_if_extra_kwargs(kwargs): """Raise a TypeError if kwargs is not empty.""" if kwargs: From 0e753ed8694fb3011c22cc9bcf6e334cbfdd0ff0 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Thu, 5 Oct 2017 16:58:40 -0400 Subject: [PATCH 08/31] Correct comment typo --- ciscosparkapi/api/people.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index dd31714..fa76bfc 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -268,7 +268,7 @@ def create(self, emails, displayName=None, firstName=None, lastName=None, # API request json_data = self._session.post('people', json=post_data) - # Return a Room object created from the returned JSON object + # Return a Person object created from the returned JSON object return Person(json_data) def update(self, personId, emails=None, displayName=None, firstName=None, From b5e77765aa3adb9c1462052c751ec69669fb45c8 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Fri, 13 Oct 2017 11:33:25 -0400 Subject: [PATCH 09/31] Update Room data model and module docstrings Add docstrings to the Room data model and ensure all 'known' attributes return their value or `None`. --- ciscosparkapi/api/rooms.py | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/ciscosparkapi/api/rooms.py b/ciscosparkapi/api/rooms.py index 9f3ddd0..79b4a83 100644 --- a/ciscosparkapi/api/rooms.py +++ b/ciscosparkapi/api/rooms.py @@ -3,8 +3,8 @@ Classes: Room: Models a Spark 'room' JSON object as a native Python object. - RoomsAPI: Wrappers the Cisco Spark Rooms-API and exposes the API calls as - Python method calls that return native Python objects. + RoomsAPI: Wraps the Cisco Spark Rooms-API and exposes the APIs as native + Python methods that return native Python objects. """ @@ -35,10 +35,10 @@ class Room(SparkData): """Model a Spark 'room' JSON object as a native Python object.""" def __init__(self, json): - """Init a new Room data object from a JSON dictionary or string. + """Initialize a Room data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -48,42 +48,43 @@ def __init__(self, json): @property def id(self): - return self._json['id'] + """The rooms's unique ID.""" + return self._json.get('id') @property def title(self): - return self._json['title'] + """A user-friendly name for the room.""" + return self._json.get('title') @property def type(self): - return self._json['type'] + """The type of room (i.e. 'group', 'direct' etc.).""" + return self._json.get('type') @property def isLocked(self): - return self._json['isLocked'] + """Whether or not the room is locked and controled by moderator(s).""" + return self._json.get('isLocked') @property def lastActivity(self): - return self._json['lastActivity'] + """The date and time when the room was last active.""" + return self._json.get('lastActivity') @property def created(self): - return self._json['created'] + """The date and time when the room was created.""" + return self._json.get('created') @property def creatorId(self): - return self._json['creatorId'] + """The ID of the person who created the room.""" + return self._json.get('creatorId') @property def teamId(self): - """Return the room teamId, if it exists, otherwise return None. - - teamId is an 'optional' attribute that only exists for Spark rooms that - are associated with a Spark Team. To simplify use, rather than - requiring use of try/catch statements or hasattr() calls, we simply - return None if a room does not have a teamId attribute. - """ - return self._json.get('teamId', None) + """The ID for the team with which this room is associated.""" + return self._json.get('teamId') class RoomsAPI(object): From 13c27b466ae01d06f9c0e0a88c6d35dbdfd50fdd Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Fri, 13 Oct 2017 11:59:47 -0400 Subject: [PATCH 10/31] Refactor future-proofing request_parameters Use common `request_parameters` name for future-proofing keyword arguments. --- ciscosparkapi/api/people.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index fa76bfc..b50808f 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -157,7 +157,7 @@ def __init__(self, session): @generator_container def list(self, email=None, displayName=None, id=None, orgId=None, max=None, - **query_params): + **request_parameters): """List people This method supports Cisco Spark's implementation of RFC5988 Web @@ -179,8 +179,8 @@ def list(self, email=None, displayName=None, id=None, orgId=None, max=None, orgId(basestring): The organization id. max(int): Limits the maximum number of people returned from the Spark service per request. - **query_params: Additional query parameters (provides support for - query parameters that may be added in the future). + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: GeneratorContainer: A GeneratorContainer which, when iterated, @@ -198,7 +198,7 @@ def list(self, email=None, displayName=None, id=None, orgId=None, max=None, check_type(max, int) params = dict_from_items_with_values( - query_params, + request_parameters, id=id, email=email, displayName=displayName, @@ -215,7 +215,7 @@ def list(self, email=None, displayName=None, id=None, orgId=None, max=None, def create(self, emails, displayName=None, firstName=None, lastName=None, avatar=None, orgId=None, roles=None, licenses=None, - **person_attributes): + **request_parameters): """Create a new user account for a given organization Only an admin can create a new user account. @@ -233,8 +233,8 @@ def create(self, emails, displayName=None, firstName=None, lastName=None, licenses(list): Licenses allocated to the person (list of strings - containing the license IDs to be allocated to the person). - **person_attributes: Additional person attributes (provides support - for attributes that may be added in the future). + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: Person: A Person object with the details of the created person. @@ -254,7 +254,7 @@ def create(self, emails, displayName=None, firstName=None, lastName=None, check_type(licenses, list) post_data = dict_from_items_with_values( - person_attributes, + request_parameters, emails=emails, displayName=displayName, firstName=firstName, @@ -273,7 +273,7 @@ def create(self, emails, displayName=None, firstName=None, lastName=None, def update(self, personId, emails=None, displayName=None, firstName=None, lastName=None, avatar=None, orgId=None, roles=None, - licenses=None, **person_attributes): + licenses=None, **request_parameters): """Update details for a person, by ID. Only an admin can update a person's details. @@ -299,8 +299,8 @@ def update(self, personId, emails=None, displayName=None, firstName=None, licenses(list): Licenses allocated to the person (list of strings - containing the license IDs to be allocated to the person). - **person_attributes: Additional person attributes (provides support - for attributes that may be added in the future). + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: Person: A Person object with the updated details. @@ -320,7 +320,7 @@ def update(self, personId, emails=None, displayName=None, firstName=None, check_type(licenses, list) put_data = dict_from_items_with_values( - person_attributes, + request_parameters, emails=emails, displayName=displayName, firstName=firstName, From 3a479e52894378c2e7c5c12ade536efe9ec35518 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Fri, 13 Oct 2017 12:39:04 -0400 Subject: [PATCH 11/31] Update docstrings --- ciscosparkapi/api/people.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index b50808f..4787a29 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -338,10 +338,10 @@ def update(self, personId, emails=None, displayName=None, firstName=None, return Person(json_data) def get(self, personId): - """Get person details, by personId. + """Get a person's details, by ID. Args: - personId(basestring): The 'id' of the person to be retrieved. + personId(basestring): The ID of the person to be retrieved. Returns: Person: A Person object with the details of the requested person. From 6b26d1b1ca461b459ff2961be048289c773cce62 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Fri, 13 Oct 2017 12:39:55 -0400 Subject: [PATCH 12/31] Refactor RoomsAPI Apply structural changes to the RoomsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/rooms.py | 157 ++++++++++++++++++++++--------------- 1 file changed, 92 insertions(+), 65 deletions(-) diff --git a/ciscosparkapi/api/rooms.py b/ciscosparkapi/api/rooms.py index 79b4a83..d9d79d1 100644 --- a/ciscosparkapi/api/rooms.py +++ b/ciscosparkapi/api/rooms.py @@ -20,7 +20,11 @@ from past.builtins import basestring from ciscosparkapi.exceptions import ciscosparkapiException -from ciscosparkapi.utils import generator_container +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData @@ -90,28 +94,31 @@ def teamId(self): class RoomsAPI(object): """Cisco Spark Rooms-API wrapper class. - Wrappers the Cisco Spark Rooms-API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark Rooms-API and exposes the APIs as native Python + methods that return native Python objects. """ def __init__(self, session): - """Init a new RoomsAPI object with the provided RestSession. + """Initialize a new RoomsAPI object with the provided RestSession. Args: session(RestSession): The RESTful session object to be used for API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession, may_be_none=False) + super(RoomsAPI, self).__init__() + self._session = session @generator_container - def list(self, max=None, **query_params): + def list(self, teamId=None, type=None, sortBy=None, max=None, + **request_parameters): """List rooms. By default, lists rooms to which the authenticated user belongs. @@ -127,38 +134,49 @@ def list(self, max=None, **query_params): container. Args: - max(int): Limits the maximum number of rooms returned from the - Spark service per request. teamId(basestring): Limit the rooms to those associated with a - team. - type(basestring): - 'direct': returns all 1-to-1 rooms. - 'group': returns all group rooms. + team, by ID. + type(basestring): 'direct' returns all 1-to-1 rooms. `group` + returns all group rooms. If not specified or values not + matched, will return all room types. + sortBy(basestring): Sort results by room ID (`id`), most recent + activity (`lastactivity`), or most recently created + (`created`). + max(int): Limit the maximum number of rooms in the response from + the Spark service. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the rooms returned from the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the rooms returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert max is None or isinstance(max, int) - params = {} - if max: - params['max'] = max - # Process query_param keyword arguments - if query_params: - params.update(query_params) + check_type(teamId, basestring) + check_type(type, basestring) + check_type(sortBy, basestring) + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + teamId=teamId, + type=type, + sortBy=sortBy, + max=max, + ) + # API request - get items items = self._session.get_items('rooms', params=params) + # Yield Room objects created from the returned items JSON objects for item in items: yield Room(item) - def create(self, title, teamId=None): + def create(self, title, teamId=None, **request_parameters): """Create a room. The authenticated user is automatically added as a member of the room. @@ -167,88 +185,97 @@ def create(self, title, teamId=None): title(basestring): A user-friendly name for the room. teamId(basestring): The team ID with which this room is associated. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Room: With the details of the created room. + Room: A Room with the details of the created room. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(title, basestring) - assert teamId is None or isinstance(teamId, basestring) - post_data = {} - post_data['title'] = title - if teamId: - post_data['teamId'] = teamId + check_type(title, basestring) + check_type(teamId, basestring) + + post_data = dict_from_items_with_values( + request_parameters, + title=title, + teamId=teamId, + ) + # API request - json_obj = self._session.post('rooms', json=post_data) + json_data = self._session.post('rooms', json=post_data) + # Return a Room object created from the response JSON data - return Room(json_obj) + return Room(json_data) - def get(self, roomId): - """Get the details of a room, by ID. + def update(self, roomId, title=None, **request_parameters): + """Update details for a room, by ID. Args: - roomId(basestring): The roomId of the room. + roomId(basestring): The ID of the room to be updated. + title(basestring): A user-friendly name for the room. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Room: With the details of the requested room. + Room: A Room object with the updated Spark room details. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(roomId, basestring) + check_type(roomId, basestring, may_be_none=False) + check_type(roomId, basestring) + + put_data = dict_from_items_with_values( + request_parameters, + title=title, + ) + # API request - json_obj = self._session.get('rooms/' + roomId) + json_data = self._session.put('rooms/' + roomId, json=put_data) + # Return a Room object created from the response JSON data - return Room(json_obj) + return Room(json_data) - def update(self, roomId, **update_attributes): - """Update details for a room. + def get(self, roomId): + """Get the details of a room, by ID. Args: - roomId(basestring): The roomId of the room to be updated. - title(basestring): A user-friendly name for the room. + roomId(basestring): The ID of the room to be retrieved. Returns: - Room: With the updated Spark room details. + Room: A Room object with the details of the requested room. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If an update attribute is not provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(roomId, basestring) - # Process update_attributes keyword arguments - if not update_attributes: - error_message = "At least one **update_attributes keyword " \ - "argument must be specified." - raise ciscosparkapiException(error_message) + check_type(roomId, basestring, may_be_none=False) + # API request - json_obj = self._session.put('rooms/' + roomId, json=update_attributes) + json_data = self._session.get('rooms/' + roomId) + # Return a Room object created from the response JSON data - return Room(json_obj) + return Room(json_data) def delete(self, roomId): """Delete a room. Args: - roomId(basestring): The roomId of the room to be deleted. + roomId(basestring): The ID of the room to be deleted. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(roomId, basestring) + check_type(roomId, basestring, may_be_none=False) + # API request self._session.delete('rooms/' + roomId) From a3e28e3ba313f02313a094e61289d6db8cb2bd18 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 22 Oct 2017 08:57:30 -0400 Subject: [PATCH 13/31] Patch bug in new dict_from_items_with_values *args are passed in as a tuple, which needs to be converted to a list so that the `items` dictionary can be appended before processing. --- ciscosparkapi/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ciscosparkapi/utils.py b/ciscosparkapi/utils.py index f2a6ace..ebbef1c 100644 --- a/ciscosparkapi/utils.py +++ b/ciscosparkapi/utils.py @@ -151,9 +151,10 @@ def dict_from_items_with_values(*dictionaries, **items): dict: A dictionary containing all of the items with a 'non-None' value. """ - dictionaries.append(items) + dict_list = list(dictionaries) + dict_list.append(items) result = {} - for d in dictionaries: + for d in dict_list: for key, value in d.items(): if value is not None: result[key] = value From f89b6f4f9f44d654bbe04982944c8a43d688eb3a Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 22 Oct 2017 10:33:18 -0400 Subject: [PATCH 14/31] Refactor Memberships Apply structural changes to the RoomsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/memberships.py | 244 ++++++++++++++++--------------- 1 file changed, 126 insertions(+), 118 deletions(-) diff --git a/ciscosparkapi/api/memberships.py b/ciscosparkapi/api/memberships.py index a6557d6..901bf66 100644 --- a/ciscosparkapi/api/memberships.py +++ b/ciscosparkapi/api/memberships.py @@ -4,8 +4,8 @@ Classes: Membership: Models a Spark 'membership' JSON object as a native Python object. - MembershipsAPI: Wrappers the Cisco Spark Memberships-API and exposes the - API calls as Python method calls that return native Python objects. + MembershipsAPI: Wraps the Cisco Spark Memberships-API and exposes the + APIs as native Python methods that return native Python objects. """ @@ -20,10 +20,13 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.exceptions import ciscosparkapiException -from ciscosparkapi.utils import generator_container from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) __author__ = "Chris Lunsford" @@ -36,10 +39,10 @@ class Membership(SparkData): """Model a Spark 'membership' JSON object as a native Python object.""" def __init__(self, json): - """Init a new Membership data object from a JSON dictionary or string. + """Initialize a Membership object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -49,42 +52,55 @@ def __init__(self, json): @property def id(self): - return self._json['id'] + """The membership's unique ID.""" + return self._json.get('id') @property def roomId(self): - return self._json['roomId'] + """The ID of the room.""" + return self._json.get('roomId') @property def personId(self): - return self._json['personId'] + """The ID of the person.""" + return self._json.get('personId') @property def personEmail(self): - return self._json['personEmail'] + """The email address of the person.""" + return self._json.get('personEmail') @property def personDisplayName(self): - return self._json['personDisplayName'] + """The display name of the person.""" + return self._json.get('personDisplayName') + + @property + def personOrgId(self): + """The ID of the organization that the person is associated with.""" + return self._json.get('personOrgId') @property def isModerator(self): - return self._json['isModerator'] + """Person is a moderator for the room.""" + return self._json.get('isModerator') @property def isMonitor(self): - return self._json['isMonitor'] + """Person is a monitor for the room.""" + return self._json.get('isMonitor') @property def created(self): - return self._json['created'] + """The date and time the membership was created.""" + return self._json.get('created') class MembershipsAPI(object): """Cisco Spark Memberships-API wrapper class. - Wrappers the Cisco Spark Memberships-API and exposes the API calls as - Python method calls that return native Python objects. + Wraps the Cisco Spark Memberships-API and exposes the APIs as native Python + methods that return native Python objects. """ @@ -96,25 +112,28 @@ def __init__(self, session): API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession) + super(MembershipsAPI, self).__init__() + self._session = session @generator_container - def list(self, roomId=None, personId=None, personEmail=None, max=None): + def list(self, roomId=None, personId=None, personEmail=None, max=None, + **request_parameters): """List room memberships. - By default, lists memberships for rooms to which the authenticated - user belongs. + By default, lists memberships for rooms to which the authenticated user + belongs. Use query parameters to filter the response. - Use roomId to list memberships for a room, by ID. + Use `roomId` to list memberships for a room, by ID. - Use either personId or personEmail to filter the results. + Use either `personId` or `personEmail` to filter the results. This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator @@ -127,164 +146,153 @@ def list(self, roomId=None, personId=None, personEmail=None, max=None): container. Args: - roomId(basestring): List memberships for the room with roomId. - personId(basestring): Filter results to include only those with - personId. - personEmail(basestring): Filter results to include only those - with personEmail. - max(int): Limits the maximum number of memberships returned from - the Spark service per request. - + roomId(basestring): Limit results to a specific room, by ID. + personId(basestring): Limit results to a specific person, by ID. + personEmail(basestring): Limit results to a specific person, by + email address. + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the memberships returned from the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the memberships returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If a personId or personEmail argument is - specified without providing a roomId argument. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert roomId is None or isinstance(roomId, basestring) - assert personId is None or isinstance(personId, basestring) - assert personEmail is None or isinstance(personEmail, basestring) - assert max is None or isinstance(max, int) - params = {} - if roomId: - params['roomId'] = roomId - if personId: - params['personId'] = personId - elif personEmail: - params['personEmail'] = personEmail - elif personId or personEmail: - error_message = "A roomId must be specified. A personId or " \ - "personEmail filter may only be specified when " \ - "requesting the memberships for a room with the " \ - "roomId argument." - raise ciscosparkapiException(error_message) - if max: - params['max'] = max + check_type(roomId, basestring) + check_type(personId, basestring) + check_type(personEmail, basestring) + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + roomId=roomId, + personId=personId, + personEmail=personEmail, + max=max, + ) + # API request - get items items = self._session.get_items('memberships', params=params) + # Yield Person objects created from the returned items JSON objects for item in items: yield Membership(item) def create(self, roomId, personId=None, personEmail=None, - isModerator=False): + isModerator=False, **request_parameters): """Add someone to a room by Person ID or email address. Add someone to a room by Person ID or email address; optionally making them a moderator. Args: - roomId(basestring): ID of the room to which the person will be - added. - personId(basestring): ID of the person to be added to the room. - personEmail(basestring): Email address of the person to be added - to the room. - isModerator(bool): If True, adds the person as a moderator for the - room. If False, adds the person as normal member of the room. + roomId(basestring): The room ID. + personId(basestring): The ID of the person. + personEmail(basestring): The email address of the person. + isModerator(bool): Set to True to make the person a room moderator. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Membership: With the details of the created membership. + Membership: A Membership object with the details of the created + membership. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If neither a personId or personEmail are - provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(roomId, basestring) - assert personId is None or isinstance(personId, basestring) - assert personEmail is None or isinstance(personEmail, basestring) - assert isModerator is None or isinstance(isModerator, bool) - post_data = {} - post_data['roomId'] = roomId - if personId: - post_data['personId'] = personId - elif personEmail: - post_data['personEmail'] = personEmail - else: - error_message = "personId or personEmail must be provided to " \ - "add a person to a room. Neither were provided." - raise ciscosparkapiException(error_message) - post_data['isModerator'] = isModerator + check_type(roomId, basestring, may_be_none=False) + check_type(personId, basestring) + check_type(personEmail, basestring) + check_type(isModerator, bool) + + post_data = dict_from_items_with_values( + request_parameters, + roomId=roomId, + personId=personId, + personEmail=personEmail, + isModerator=isModerator, + ) + # API request - json_obj = self._session.post('memberships', json=post_data) + json_data = self._session.post('memberships', json=post_data) + # Return a Membership object created from the response JSON data - return Membership(json_obj) + return Membership(json_data) def get(self, membershipId): - """Get details for a membership by ID. + """Get details for a membership, by ID. Args: - membershipId(basestring): The membershipId of the membership. + membershipId(basestring): The membership ID. Returns: - Membership: With the details of the requested membership. + Membership: A Membership object with the details of the requested + membership. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(membershipId, basestring) + check_type(membershipId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('memberships/' + membershipId) + json_data = self._session.get('memberships/' + membershipId) + # Return a Membership object created from the response JSON data - return Membership(json_obj) + return Membership(json_data) - def update(self, membershipId, **update_attributes): - """Update details for a membership. + def update(self, membershipId, isModerator=None, **request_parameters): + """Updates properties for a membership, by ID. Args: - membershipId(basestring): The membershipId of the membership to - be updated. - isModerator(bool): If True, sets the person as a moderator for the - room. If False, removes the person as a moderator for the room. + membershipId(basestring): The membership ID. + isModerator(bool): Set to True to make the person a room moderator. Returns: - Membership: With the updated Spark membership details. + Membership: A Membership object with the updated Spark membership + details. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If an update attribute is not provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(membershipId, basestring) - # Process update_attributes keyword arguments - if not update_attributes: - error_message = "At least one **update_attributes keyword " \ - "argument must be specified." - raise ciscosparkapiException(error_message) + check_type(membershipId, basestring, may_be_none=False) + check_type(isModerator, bool) + + put_data = dict_from_items_with_values( + request_parameters, + isModerator=isModerator, + ) + # API request - json_obj = self._session.put('memberships/' + membershipId, - json=update_attributes) + json_data = self._session.put('memberships/' + membershipId, + json=put_data) + # Return a Membership object created from the response JSON data - return Membership(json_obj) + return Membership(json_data) def delete(self, membershipId): """Delete a membership, by ID. Args: - membershipId(basestring): The membershipId of the membership to - be deleted. + membershipId(basestring): The membership ID. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(membershipId, basestring) + check_type(membershipId, basestring) + # API request self._session.delete('memberships/' + membershipId) From d90c6f94230b1e9ca36cb35f2e3e19e7f47e36c5 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 22 Oct 2017 10:35:49 -0400 Subject: [PATCH 15/31] Consistency Update - rooms.py --- ciscosparkapi/api/rooms.py | 47 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/ciscosparkapi/api/rooms.py b/ciscosparkapi/api/rooms.py index d9d79d1..c38097a 100644 --- a/ciscosparkapi/api/rooms.py +++ b/ciscosparkapi/api/rooms.py @@ -19,14 +19,13 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.exceptions import ciscosparkapiException +from ciscosparkapi.restsession import RestSession +from ciscosparkapi.sparkdata import SparkData from ciscosparkapi.utils import ( check_type, dict_from_items_with_values, generator_container, ) -from ciscosparkapi.restsession import RestSession -from ciscosparkapi.sparkdata import SparkData __author__ = "Chris Lunsford" @@ -142,8 +141,8 @@ def list(self, teamId=None, type=None, sortBy=None, max=None, sortBy(basestring): Sort results by room ID (`id`), most recent activity (`lastactivity`), or most recently created (`created`). - max(int): Limit the maximum number of rooms in the response from - the Spark service. + max(int): Limit the maximum number of items returned from the Spark + service per request. **request_parameters: Additional request parameters (provides support for parameters that may be added in the future). @@ -211,17 +210,14 @@ def create(self, title, teamId=None, **request_parameters): # Return a Room object created from the response JSON data return Room(json_data) - def update(self, roomId, title=None, **request_parameters): - """Update details for a room, by ID. + def get(self, roomId): + """Get the details of a room, by ID. Args: - roomId(basestring): The ID of the room to be updated. - title(basestring): A user-friendly name for the room. - **request_parameters: Additional request parameters (provides - support for parameters that may be added in the future). + roomId(basestring): The ID of the room to be retrieved. Returns: - Room: A Room object with the updated Spark room details. + Room: A Room object with the details of the requested room. Raises: TypeError: If the parameter types are incorrect. @@ -229,27 +225,24 @@ def update(self, roomId, title=None, **request_parameters): """ check_type(roomId, basestring, may_be_none=False) - check_type(roomId, basestring) - - put_data = dict_from_items_with_values( - request_parameters, - title=title, - ) # API request - json_data = self._session.put('rooms/' + roomId, json=put_data) + json_data = self._session.get('rooms/' + roomId) # Return a Room object created from the response JSON data return Room(json_data) - def get(self, roomId): - """Get the details of a room, by ID. + def update(self, roomId, title=None, **request_parameters): + """Update details for a room, by ID. Args: - roomId(basestring): The ID of the room to be retrieved. + roomId(basestring): The room ID. + title(basestring): A user-friendly name for the room. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Room: A Room object with the details of the requested room. + Room: A Room object with the updated Spark room details. Raises: TypeError: If the parameter types are incorrect. @@ -257,9 +250,15 @@ def get(self, roomId): """ check_type(roomId, basestring, may_be_none=False) + check_type(roomId, basestring) + + put_data = dict_from_items_with_values( + request_parameters, + title=title, + ) # API request - json_data = self._session.get('rooms/' + roomId) + json_data = self._session.put('rooms/' + roomId, json=put_data) # Return a Room object created from the response JSON data return Room(json_data) From 6a748e4785499af9d2957b0ec1ee3ad55b3d98b6 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 22 Oct 2017 10:35:59 -0400 Subject: [PATCH 16/31] Consistency Update - people.py --- ciscosparkapi/api/people.py | 54 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 4787a29..db4a8fc 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -19,13 +19,13 @@ from builtins import * from past.builtins import basestring +from ciscosparkapi.restsession import RestSession +from ciscosparkapi.sparkdata import SparkData from ciscosparkapi.utils import ( check_type, dict_from_items_with_values, generator_container, ) -from ciscosparkapi.restsession import RestSession -from ciscosparkapi.sparkdata import SparkData __author__ = "Chris Lunsford" @@ -177,8 +177,8 @@ def list(self, email=None, displayName=None, id=None, orgId=None, max=None, id(basestring): List people by ID. Accepts up to 85 person IDs separated by commas. orgId(basestring): The organization id. - max(int): Limits the maximum number of people returned from the - Spark service per request. + max(int): Limit the maximum number of items returned from the Spark + service per request. **request_parameters: Additional request parameters (provides support for parameters that may be added in the future). @@ -271,6 +271,28 @@ def create(self, emails, displayName=None, firstName=None, lastName=None, # Return a Person object created from the returned JSON object return Person(json_data) + def get(self, personId): + """Get a person's details, by ID. + + Args: + personId(basestring): The ID of the person to be retrieved. + + Returns: + Person: A Person object with the details of the requested person. + + Raises: + TypeError: If the parameter types are incorrect. + SparkApiError: If the Cisco Spark cloud returns an error. + + """ + check_type(personId, basestring, may_be_none=False) + + # API request + json_data = self._session.get('people/' + personId) + + # Return a Person object created from the response JSON data + return Person(json_data) + def update(self, personId, emails=None, displayName=None, firstName=None, lastName=None, avatar=None, orgId=None, roles=None, licenses=None, **request_parameters): @@ -286,7 +308,7 @@ def update(self, personId, emails=None, displayName=None, firstName=None, unchanged values. Args: - personId(basestring): The 'id' of the person to be updated. + personId(basestring): The person ID. emails(list): Email address(es) of the person (list of strings). displayName(basestring): Full name of the person. firstName(basestring): First name of the person. @@ -337,28 +359,6 @@ def update(self, personId, emails=None, displayName=None, firstName=None, # Return a Person object created from the returned JSON object return Person(json_data) - def get(self, personId): - """Get a person's details, by ID. - - Args: - personId(basestring): The ID of the person to be retrieved. - - Returns: - Person: A Person object with the details of the requested person. - - Raises: - TypeError: If the parameter types are incorrect. - SparkApiError: If the Cisco Spark cloud returns an error. - - """ - check_type(personId, basestring, may_be_none=False) - - # API request - json_data = self._session.get('people/' + personId) - - # Return a Person object created from the response JSON data - return Person(json_data) - def delete(self, personId): """Remove a person from the system. From 2544be02dbff1f50dd1e11f66e1c77574c0c8dda Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sat, 28 Oct 2017 21:22:28 -0400 Subject: [PATCH 17/31] Refactor Messages Apply structural changes to the MessagesAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/messages.py | 289 +++++++++++++++++----------------- 1 file changed, 141 insertions(+), 148 deletions(-) diff --git a/ciscosparkapi/api/messages.py b/ciscosparkapi/api/messages.py index bb1b600..af9eb31 100644 --- a/ciscosparkapi/api/messages.py +++ b/ciscosparkapi/api/messages.py @@ -3,8 +3,8 @@ Classes: Message: Models a Spark 'message' JSON object as a native Python object. - MessagesAPI: Wrappers the Cisco Spark Messages-API and exposes the API - calls as Python method calls that return native Python objects. + MessagesAPI: Wraps the Cisco Spark Messages-API and exposes the APIs as + native Python methods that return native Python objects. """ @@ -21,10 +21,11 @@ from requests_toolbelt import MultipartEncoder -from ciscosparkapi.exceptions import ciscosparkapiException from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, generator_container, is_web_url, is_local_file, @@ -42,10 +43,10 @@ class Message(SparkData): """Model a Spark 'message' JSON object as a native Python object.""" def __init__(self, json): - """Init a new Message data object from a JSON dictionary or string. + """Initialize a Message data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -55,64 +56,65 @@ def __init__(self, json): @property def id(self): - return self._json['id'] + """The message's unique ID.""" + return self._json.get('id') @property def roomId(self): - return self._json['roomId'] + """The ID of the room.""" + return self._json.get('roomId') @property def roomType(self): - return self._json['roomType'] - - @property - def toPersonId(self): - """Optional attribute; returns None if not present.""" - return self._json.get('toPersonId') - - @property - def toPersonEmail(self): - """Optional attribute; returns None if not present.""" - return self._json.get('toPersonEmail') + """The type of room (i.e. 'group', 'direct' etc.).""" + return self._json.get('roomType') @property def text(self): - """Optional attribute; returns None if not present.""" + """The message, in plain text.""" return self._json.get('text') - @property - def markdown(self): - """Optional attribute; returns None if not present.""" - return self._json.get('markdown') - @property def files(self): - """Optional attribute; returns None if not present.""" + """Files attached to the the message (list of URLs).""" return self._json.get('files') @property def personId(self): - return self._json['personId'] + """The person ID of the sender.""" + return self._json.get('personId') @property def personEmail(self): - return self._json['personEmail'] + """The email address of the sender.""" + return self._json.get('personEmail') @property - def created(self): - return self._json['created'] + def markdown(self): + """The message, in markdown format.""" + return self._json.get('markdown') + + @property + def html(self): + """The message, in HTML format.""" + return self._json.get('html') @property def mentionedPeople(self): - """Optional attribute; returns None if not present.""" + """The list of IDs of people mentioned in the message.""" return self._json.get('mentionedPeople') + @property + def created(self): + """The date and time the message was created.""" + return self._json.get('created') + class MessagesAPI(object): """Cisco Spark Messages-API wrapper class. - Wrappers the Cisco Spark Messages-API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark Messages-API and exposes the APIs as native Python + methods that return native Python objects. """ @@ -133,12 +135,12 @@ def __init__(self, session): @generator_container def list(self, roomId, mentionedPeople=None, before=None, - beforeMessage=None, max=None): - """List all messages in a room. + beforeMessage=None, max=None, **request_parameters): + """Lists all messages in a room. + + Each message will include content attachments if present. - If present, includes the associated media content attachment for each - message. The list sorts the messages in descending order by creation - date. + The list API sorts the messages in descending order by creation date. This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator @@ -151,179 +153,170 @@ def list(self, roomId, mentionedPeople=None, before=None, container. Args: - roomId(basestring): List messages for the room with roomId. - mentionedPeople(basestring): List messages for a person, by - personId or me. - before(basestring): List messages sent before a date and time, - in ISO8601 format + roomId(basestring): List messages for a room, by ID. + mentionedPeople(basestring): List messages where the caller is + mentioned by specifying "me" or the caller `personId`. + before(basestring): List messages sent before a date and time, in + ISO8601 format. beforeMessage(basestring): List messages sent before a message, - by message ID - max(int): Limit the maximum number of messages returned from the - Spark service per request. + by ID. + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the messages returned by the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the messages returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(roomId, basestring) - assert mentionedPeople is None or isinstance(mentionedPeople, list) - assert before is None or isinstance(before, basestring) - assert beforeMessage is None or isinstance(beforeMessage, basestring) - assert max is None or isinstance(max, int) - params = {} - params['roomId'] = roomId - if mentionedPeople: - params['mentionedPeople'] = mentionedPeople - if before: - params['before'] = before - if beforeMessage: - params['beforeMessage'] = beforeMessage - if max: - params['max'] = max + check_type(roomId, basestring, may_be_none=False) + check_type(mentionedPeople, basestring) + check_type(before, basestring) + check_type(beforeMessage, basestring) + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + roomId=roomId, + mentionedPeople=mentionedPeople, + before=before, + beforeMessage=beforeMessage, + max=max, + ) + # API request - get items items = self._session.get_items('messages', params=params) + # Yield Message objects created from the returned items JSON objects for item in items: yield Message(item) def create(self, roomId=None, toPersonId=None, toPersonEmail=None, - text=None, markdown=None, files=None): - """Posts a message to a room. + text=None, markdown=None, files=None, **request_parameters): + """Posts a message, and optionally a attachment, to a room. - Posts a message, and optionally, a media content attachment, to a room. - - You must specify either a roomId, toPersonId or toPersonEmail when - posting a message, and you must supply some message content (text, - markdown, files). + The files parameter is a list, which accepts multiple values to allow + for future expansion, but currently only one file may be included with + the message. Args: roomId(basestring): The room ID. toPersonId(basestring): The ID of the recipient when sending a private 1:1 message. - toPersonEmail(basestring): The email address of the recipient - when sending a private 1:1 message. - text(basestring): The message, in plain text. If markdown is + toPersonEmail(basestring): The email address of the recipient when + sending a private 1:1 message. + text(basestring): The message, in plain text. If `markdown` is specified this parameter may be optionally used to provide - alternate text forUI clients that do not support rich text. + alternate text for UI clients that do not support rich text. markdown(basestring): The message, in markdown format. - files(list): A list containing local paths or URL references for - the message attachment(s). The files attribute currently only - takes a list containing one (1) filename or URL as an input. - This is a Spark API limitation that may be lifted at a later - date. + files(list): A list of public URL(s) or local path(s) to files to + be posted into the room. Only one file is allowed per message. + Uploaded files are automatically converted into a format that + all Spark clients can render. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Message: With the details of the created message. + Message: A Message object with the details of the created message. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If the required arguments are not - specified. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. + ValueError: If the files parameter is a list of length > 1, or if + the string in the list (the only element in the list) does not + contain a valid URL or path to a local file. """ - # Process args - assert roomId is None or isinstance(roomId, basestring) - assert toPersonId is None or isinstance(toPersonId, basestring) - assert toPersonEmail is None or isinstance(toPersonEmail, basestring) - assert text is None or isinstance(text, basestring) - assert markdown is None or isinstance(markdown, basestring) - assert files is None or isinstance(files, list) - post_data = {} - # Where is message to be posted? - if roomId: - post_data['roomId'] = roomId - elif toPersonId: - post_data['toPersonId'] = toPersonId - elif toPersonEmail: - post_data['toPersonEmail'] = toPersonEmail - else: - error_message = "You must specify a roomId, toPersonId, or " \ - "toPersonEmail to which you want to post a new " \ - "message." - raise ciscosparkapiException(error_message) - # Ensure some message 'content' is provided. - if not text and not markdown and not files: - error_message = "You must supply some message content (text, " \ - "markdown, files) when posting a message." - raise ciscosparkapiException(error_message) - # Process the content. - if text: - post_data['text'] = text - if markdown: - post_data['markdown'] = markdown - upload_local_file = False + check_type(roomId, basestring) + check_type(toPersonId, basestring) + check_type(toPersonEmail, basestring) + check_type(text, basestring) + check_type(markdown, basestring) + check_type(files, list) if files: - if len(files) > 1: - error_message = "The files attribute currently only takes a " \ - "list containing one (1) filename or URL as " \ - "an input. This is a Spark API limitation " \ - "that may be lifted at a later date." - raise ciscosparkapiException(error_message) - if is_web_url(files[0]): - post_data['files'] = files - elif is_local_file(files[0]): - upload_local_file = True - post_data['files'] = open_local_file(files[0]) - else: - error_message = "The provided files argument does not " \ - "contain a valid URL or local file path." - raise ciscosparkapiException(error_message) + if len(files) != 1: + raise ValueError("The length of the `files` list is greater " + "than one (1). The files parameter is a " + "list, which accepts multiple values to " + "allow for future expansion, but currently " + "only one file may be included with the " + "message.") + check_type(files[0], basestring) + + post_data = dict_from_items_with_values( + request_parameters, + roomId=roomId, + toPersonId=toPersonId, + toPersonEmail=toPersonEmail, + text=text, + markdown=markdown, + files=files, + ) + # API request - if upload_local_file: + if not files or is_web_url(files[0]): + # Standard JSON post + json_data = self._session.post('messages', json=post_data) + + elif is_local_file(files[0]): + # Multipart MIME post try: + post_data['files'] = open_local_file(files[0]) multipart_data = MultipartEncoder(post_data) headers = {'Content-type': multipart_data.content_type} - json_obj = self._session.post('messages', - data=multipart_data, - headers=headers) + json_data = self._session.post('messages', + headers=headers, + data=multipart_data) finally: post_data['files'].file_object.close() + else: - json_obj = self._session.post('messages', json=post_data) + raise ValueError("The `files` parameter does not contain a vaild " + "URL or path to a local file.") + # Return a Message object created from the response JSON data - return Message(json_obj) + return Message(json_data) def get(self, messageId): """Get the details of a message, by ID. Args: - messageId(basestring): The messageId of the message. + messageId(basestring): The ID of the message to be retrieved. Returns: - Message: With the details of the requested message. + Message: A Message object with the details of the requested + message. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(messageId, basestring) + check_type(messageId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('messages/' + messageId) + json_data = self._session.get('messages/' + messageId) + # Return a Message object created from the response JSON data - return Message(json_obj) + return Message(json_data) def delete(self, messageId): """Delete a message. Args: - messageId(basestring): The messageId of the message to be - deleted. + messageId(basestring): The ID of the message to be deleted. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(messageId, basestring) + check_type(messageId, basestring, may_be_none=False) + # API request self._session.delete('messages/' + messageId) From d294c0d17de8ea57e49990507ce576c36e82247c Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sat, 28 Oct 2017 21:58:55 -0400 Subject: [PATCH 18/31] Correct Indentation and Docstring Typo --- ciscosparkapi/api/memberships.py | 12 ++++++------ ciscosparkapi/api/messages.py | 2 +- ciscosparkapi/api/people.py | 12 ++++++------ ciscosparkapi/api/rooms.py | 12 ++++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/ciscosparkapi/api/memberships.py b/ciscosparkapi/api/memberships.py index 901bf66..daf1b95 100644 --- a/ciscosparkapi/api/memberships.py +++ b/ciscosparkapi/api/memberships.py @@ -137,7 +137,7 @@ def list(self, roomId=None, personId=None, personEmail=None, max=None, This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator - container that incrementally yield all memberships returned by the + container that incrementally yields all memberships returned by the query. The generator will automatically request additional 'pages' of responses from Spark as needed until all responses have been returned. The container makes the generator safe for reuse. A new API call will @@ -170,11 +170,11 @@ def list(self, roomId=None, personId=None, personEmail=None, max=None, check_type(max, int) params = dict_from_items_with_values( - request_parameters, - roomId=roomId, - personId=personId, - personEmail=personEmail, - max=max, + request_parameters, + roomId=roomId, + personId=personId, + personEmail=personEmail, + max=max, ) # API request - get items diff --git a/ciscosparkapi/api/messages.py b/ciscosparkapi/api/messages.py index af9eb31..2782d99 100644 --- a/ciscosparkapi/api/messages.py +++ b/ciscosparkapi/api/messages.py @@ -144,7 +144,7 @@ def list(self, roomId, mentionedPeople=None, before=None, This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator - container that incrementally yield all messages returned by the + container that incrementally yields all messages returned by the query. The generator will automatically request additional 'pages' of responses from Spark as needed until all responses have been returned. The container makes the generator safe for reuse. A new API call will diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index db4a8fc..3342728 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -198,12 +198,12 @@ def list(self, email=None, displayName=None, id=None, orgId=None, max=None, check_type(max, int) params = dict_from_items_with_values( - request_parameters, - id=id, - email=email, - displayName=displayName, - orgId=orgId, - max=max, + request_parameters, + id=id, + email=email, + displayName=displayName, + orgId=orgId, + max=max, ) # API request - get items diff --git a/ciscosparkapi/api/rooms.py b/ciscosparkapi/api/rooms.py index c38097a..ac45b5f 100644 --- a/ciscosparkapi/api/rooms.py +++ b/ciscosparkapi/api/rooms.py @@ -124,7 +124,7 @@ def list(self, teamId=None, type=None, sortBy=None, max=None, This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator - container that incrementally yield all rooms returned by the + container that incrementally yields all rooms returned by the query. The generator will automatically request additional 'pages' of responses from Spark as needed until all responses have been returned. The container makes the generator safe for reuse. A new API call will @@ -161,11 +161,11 @@ def list(self, teamId=None, type=None, sortBy=None, max=None, check_type(max, int) params = dict_from_items_with_values( - request_parameters, - teamId=teamId, - type=type, - sortBy=sortBy, - max=max, + request_parameters, + teamId=teamId, + type=type, + sortBy=sortBy, + max=max, ) # API request - get items From fd70b4a2ca42feb69a28719cc950d000cd67e23e Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sat, 28 Oct 2017 21:59:30 -0400 Subject: [PATCH 19/31] Refactor Teams Apply structural changes to the TeamsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/teams.py | 151 ++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 62 deletions(-) diff --git a/ciscosparkapi/api/teams.py b/ciscosparkapi/api/teams.py index 9efd17f..d325a53 100644 --- a/ciscosparkapi/api/teams.py +++ b/ciscosparkapi/api/teams.py @@ -3,8 +3,8 @@ Classes: Team: Models a Spark 'team' JSON object as a native Python object. - TeamsAPI: Wrappers the Cisco Spark Teams-API and exposes the API calls as - Python method calls that return native Python objects. + TeamsAPI: Wraps the Cisco Spark Teams-API and exposes the APIs as native + Python methods that return native Python objects. """ @@ -19,10 +19,13 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.exceptions import ciscosparkapiException -from ciscosparkapi.utils import generator_container from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) __author__ = "Chris Lunsford" @@ -35,10 +38,10 @@ class Team(SparkData): """Model a Spark 'team' JSON object as a native Python object.""" def __init__(self, json): - """Init a new Team data object from a JSON dictionary or string. + """Initialize a new Team data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON object. Raises: TypeError: If the input object is not a dictionary or string. @@ -48,47 +51,57 @@ def __init__(self, json): @property def id(self): - return self._json['id'] + """The team's unique ID.""" + return self._json.get('id') @property def name(self): - return self._json['name'] + """A user-friendly name for the team.""" + return self._json.get('name') @property def created(self): - return self._json['created'] + """The date and time the team was created.""" + return self._json.get('created') + + @property + def creatorId(self): + """The ID of the person who created the team.""" + return self._json.get('creatorId') class TeamsAPI(object): """Cisco Spark Teams-API wrapper class. - Wrappers the Cisco Spark Teams-API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark Teams-API and exposes the APIs as native Python + methods that return native Python objects. """ def __init__(self, session): - """Init a new TeamsAPI object with the provided RestSession. + """Initialize a new TeamsAPI object with the provided RestSession. Args: session(RestSession): The RESTful session object to be used for API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession, may_be_none=False) + super(TeamsAPI, self).__init__() + self._session = session @generator_container - def list(self, max=None): + def list(self, max=None, **request_parameters): """List teams to which the authenticated user belongs. This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator - container that incrementally yield all teams returned by the + container that incrementally yields all teams returned by the query. The generator will automatically request additional 'pages' of responses from Spark as needed until all responses have been returned. The container makes the generator safe for reuse. A new API call will @@ -97,116 +110,130 @@ def list(self, max=None): container. Args: - max(int): Limits the maximum number of teams returned from the - Spark service per request. + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the teams returned by the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the teams returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert max is None or isinstance(max, int) - params = {} - if max: - params['max'] = max + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + max=max, + ) + # API request - get items items = self._session.get_items('teams', params=params) + # Yield Team objects created from the returned items JSON objects for item in items: yield Team(item) - def create(self, name): + def create(self, name, **request_parameters): """Create a team. The authenticated user is automatically added as a member of the team. Args: name(basestring): A user-friendly name for the team. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Team: With the details of the created team. + Team: A Team object with the details of the created team. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(name, basestring) - post_data = {} - post_data['name'] = name + check_type(name, basestring, may_be_none=False) + + post_data = dict_from_items_with_values( + request_parameters, + name=name, + ) + # API request - json_obj = self._session.post('teams', json=post_data) + json_data = self._session.post('teams', json=post_data) + # Return a Team object created from the response JSON data - return Team(json_obj) + return Team(json_data) def get(self, teamId): """Get the details of a team, by ID. Args: - teamId(basestring): The teamId of the team. + teamId(basestring): The ID of the team to be retrieved. Returns: - Team: With the details of the requested team. + Team: A Team object with the details of the requested team. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(teamId, basestring) + check_type(teamId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('teams/' + teamId) + json_data = self._session.get('teams/' + teamId) + # Return a Team object created from the response JSON data - return Team(json_obj) + return Team(json_data) - def update(self, teamId, **update_attributes): - """Update details for a team. + def update(self, teamId, name=None, **request_parameters): + """Update details for a team, by ID. Args: - teamId(basestring): The teamId of the team to be updated. + teamId(basestring): The team ID. name(basestring): A user-friendly name for the team. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Team: With the updated Spark team details. + Team: A Team object with the updated Spark team details. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If an update attribute is not provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(teamId, basestring) - # Process update_attributes keyword arguments - if not update_attributes: - error_message = "At least one **update_attributes keyword " \ - "argument must be specified." - raise ciscosparkapiException(error_message) + check_type(teamId, basestring, may_be_none=False) + check_type(name, basestring) + + put_data = dict_from_items_with_values( + request_parameters, + name=name, + ) + # API request - json_obj = self._session.put('teams/' + teamId, - json=update_attributes) + json_data = self._session.put('teams/' + teamId, json=put_data) + # Return a Team object created from the response JSON data - return Team(json_obj) + return Team(json_data) def delete(self, teamId): """Delete a team. Args: - teamId(basestring): The teamId of the team to be deleted. + teamId(basestring): The ID of the team to be deleted. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(teamId, basestring) + check_type(teamId, basestring, may_be_none=False) + # API request self._session.delete('teams/' + teamId) From b841f3094d6976dffc03e2469771e73c17e57f84 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sat, 28 Oct 2017 22:33:02 -0400 Subject: [PATCH 20/31] Update indentation and docstrings --- ciscosparkapi/api/memberships.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ciscosparkapi/api/memberships.py b/ciscosparkapi/api/memberships.py index daf1b95..b6002a5 100644 --- a/ciscosparkapi/api/memberships.py +++ b/ciscosparkapi/api/memberships.py @@ -251,11 +251,13 @@ def get(self, membershipId): return Membership(json_data) def update(self, membershipId, isModerator=None, **request_parameters): - """Updates properties for a membership, by ID. + """Update properties for a membership, by ID. Args: membershipId(basestring): The membership ID. isModerator(bool): Set to True to make the person a room moderator. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: Membership: A Membership object with the updated Spark membership @@ -276,7 +278,7 @@ def update(self, membershipId, isModerator=None, **request_parameters): # API request json_data = self._session.put('memberships/' + membershipId, - json=put_data) + json=put_data) # Return a Membership object created from the response JSON data return Membership(json_data) From ab85067ccdb594bfe65b92f95df68386354b61c2 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sat, 28 Oct 2017 22:33:54 -0400 Subject: [PATCH 21/31] Refactor TeamMemberships Apply structural changes to the TeamMembershipsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/teammemberships.py | 223 +++++++++++++++------------ 1 file changed, 121 insertions(+), 102 deletions(-) diff --git a/ciscosparkapi/api/teammemberships.py b/ciscosparkapi/api/teammemberships.py index 652a4be..b25924b 100644 --- a/ciscosparkapi/api/teammemberships.py +++ b/ciscosparkapi/api/teammemberships.py @@ -4,8 +4,8 @@ Classes: TeamMembership: Models a Spark 'team membership' JSON object as a native Python object. - TeamMembershipsAPI: Wrappers the Cisco Spark Memberships-API and exposes - the API calls as Python method calls that return native Python objects. + TeamMembershipsAPI: Wraps the Cisco Spark Memberships-API and exposes + the APIs as native Python methods that return native Python objects. """ @@ -20,10 +20,13 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.exceptions import ciscosparkapiException -from ciscosparkapi.utils import generator_container from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) __author__ = "Chris Lunsford" @@ -37,10 +40,10 @@ class TeamMembership(SparkData): """ def __init__(self, json): - """Init a new TeamMembership object from a JSON dictionary or string. + """Initialize a TeamMembership object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON object. Raises: TypeError: If the input object is not a dictionary or string. @@ -50,38 +53,50 @@ def __init__(self, json): @property def id(self): - return self._json['id'] + """The team membership's unique ID.""" + return self._json.get('id') @property def teamId(self): - return self._json['teamId'] + """The ID of the team.""" + return self._json.get('teamId') @property def personId(self): - return self._json['personId'] + """The ID of the person.""" + return self._json.get('personId') @property def personEmail(self): - return self._json['personEmail'] + """The email address of the person.""" + return self._json.get('personEmail') @property def personDisplayName(self): - return self._json['personDisplayName'] + """The display name of the person.""" + return self._json.get('personDisplayName') + + @property + def personOrgId(self): + """The ID of the organization that the person is associated with.""" + return self._json.get('personOrgId') @property def isModerator(self): - return self._json['isModerator'] + """Person is a moderator for the team.""" + return self._json.get('isModerator') @property def created(self): - return self._json['created'] + """The date and time the team membership was created.""" + return self._json.get('created') class TeamMembershipsAPI(object): """Cisco Spark Team-Memberships-API wrapper class. - Wrappers the Cisco Spark Team-Memberships-API and exposes the API calls as - Python method calls that return native Python objects. + Wraps the Cisco Spark Memberships-API and exposes the APIs as native Python + methods that return native Python objects. """ @@ -93,168 +108,172 @@ def __init__(self, session): API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession) + super(TeamMembershipsAPI, self).__init__() + self._session = session @generator_container - def list(self, teamId, max=None): + def list(self, teamId, max=None, **request_parameters): """List team memberships for a team, by ID. This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator - container that incrementally yield all team memberships returned by the - query. The generator will automatically request additional 'pages' of - responses from Spark as needed until all responses have been returned. - The container makes the generator safe for reuse. A new API call will - be made, using the same parameters that were specified when the - generator was created, every time a new iterator is requested from the - container. + container that incrementally yields all team memberships returned by + the query. The generator will automatically request additional 'pages' + of responses from Spark as needed until all responses have been + returned. The container makes the generator safe for reuse. A new API + call will be made, using the same parameters that were specified when + the generator was created, every time a new iterator is requested from + the container. Args: - teamId(basestring): List memberships for the team with teamId. - max(int): Limits the maximum number of memberships returned from - the Spark service per request. - + teamId(basestring): List team memberships for a team, by ID. + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the team memberships returned by the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the team memberships returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(teamId, basestring) - assert max is None or isinstance(max, int) - params = {} - params['teamId'] = teamId - if max: - params['max'] = max + check_type(teamId, basestring, may_be_none=False) + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + teamId=teamId, + max=max, + ) + # API request - get items items = self._session.get_items('team/memberships', params=params) + # Yield Person objects created from the returned items JSON objects for item in items: yield TeamMembership(item) def create(self, teamId, personId=None, personEmail=None, - isModerator=False): + isModerator=False, **request_parameters): """Add someone to a team by Person ID or email address. Add someone to a team by Person ID or email address; optionally making them a moderator. Args: - teamId(basestring): ID of the team to which the person will be - added. - personId(basestring): ID of the person to be added to the team. - personEmail(basestring): Email address of the person to be added - to the team. - isModerator(bool): If True, adds the person as a moderator for the - team. If False, adds the person as normal member of the team. + teamId(basestring): The team ID. + personId(basestring): The person ID. + personEmail(basestring): The email address of the person. + isModerator(bool): Set to True to make the person a team moderator. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - TeamMembership: With the details of the created team membership. + TeamMembership: A TeamMembership object with the details of the + created team membership. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If neither a personId or personEmail are - provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(teamId, basestring) - assert personId is None or isinstance(personId, basestring) - assert personEmail is None or isinstance(personEmail, basestring) - assert isModerator is None or isinstance(isModerator, bool) - post_data = {} - post_data['teamId'] = teamId - if personId: - post_data['personId'] = personId - elif personEmail: - post_data['personEmail'] = personEmail - else: - error_message = "personId or personEmail must be provided to " \ - "add a person to a team. Neither were provided." - raise ciscosparkapiException(error_message) - post_data['isModerator'] = isModerator + check_type(teamId, basestring, may_be_none=False) + check_type(personId, basestring) + check_type(personEmail, basestring) + check_type(isModerator, bool) + + post_data = dict_from_items_with_values( + request_parameters, + teamId=teamId, + personId=personId, + personEmail=personEmail, + isModerator=isModerator, + ) + # API request - json_obj = self._session.post('team/memberships', json=post_data) + json_data = self._session.post('team/memberships', json=post_data) + # Return a TeamMembership object created from the response JSON data - return TeamMembership(json_obj) + return TeamMembership(json_data) def get(self, membershipId): - """Get details for a team membership by ID. + """Get details for a team membership, by ID. Args: - membershipId(basestring): The membershipId of the team - membership. + membershipId(basestring): The team membership ID. Returns: - TeamMembership: With the details of the requested team membership. + TeamMembership: A TeamMembership object with the details of the + requested team membership. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(membershipId, basestring) + check_type(membershipId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('team/memberships/' + membershipId) + json_data = self._session.get('team/memberships/' + membershipId) + # Return a TeamMembership object created from the response JSON data - return TeamMembership(json_obj) + return TeamMembership(json_data) - def update(self, membershipId, **update_attributes): - """Update details for a team membership. + def update(self, membershipId, isModerator=None, **request_parameters): + """Update a team membership, by ID. Args: - membershipId(basestring): The membershipId of the team membership - to be updated. - isModerator(bool): If True, sets the person as a moderator for the - team. If False, removes the person as a moderator for the team. + membershipId(basestring): The team membership ID. + isModerator(bool): Set to True to make the person a team moderator. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - TeamMembership: With the updated Spark team membership details. + TeamMembership: A TeamMembership object with the updated Spark team + membership details. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If an update attribute is not provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(membershipId, basestring) - # Process update_attributes keyword arguments - if not update_attributes: - error_message = "At least one **update_attributes keyword " \ - "argument must be specified." - raise ciscosparkapiException(error_message) + check_type(membershipId, basestring, may_be_none=False) + check_type(isModerator, bool) + + put_data = dict_from_items_with_values( + request_parameters, + isModerator=isModerator, + ) + # API request - json_obj = self._session.put('team/memberships/' + membershipId, - json=update_attributes) + json_data = self._session.put('team/memberships/' + membershipId, + json=put_data) + # Return a TeamMembership object created from the response JSON data - return TeamMembership(json_obj) + return TeamMembership(json_data) def delete(self, membershipId): """Delete a team membership, by ID. Args: - membershipId(basestring): The membershipId of the team membership - to be deleted. + membershipId(basestring): The team membership ID. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(membershipId, basestring) + check_type(membershipId, basestring, may_be_none=False) + # API request self._session.delete('team/memberships/' + membershipId) From f300825d2a8c6bad9ba969e8adafcf01575f9005 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sat, 28 Oct 2017 23:53:15 -0400 Subject: [PATCH 22/31] Add WebhookEvent class and Refator Webhooks Add a new `WebhookEvent` class to represent Spark generated webhook events. Apply structural changes to the RoomsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/__init__.py | 2 +- ciscosparkapi/api/webhooks.py | 325 +++++++++++++++++++++++++--------- 2 files changed, 241 insertions(+), 86 deletions(-) diff --git a/ciscosparkapi/__init__.py b/ciscosparkapi/__init__.py index 35d4349..ff5c050 100644 --- a/ciscosparkapi/__init__.py +++ b/ciscosparkapi/__init__.py @@ -30,7 +30,7 @@ TeamMembership, TeamMembershipsAPI ) -from ciscosparkapi.api.webhooks import Webhook, WebhooksAPI +from ciscosparkapi.api.webhooks import Webhook, WebhookEvent, WebhooksAPI from ciscosparkapi.api.organizations import Organization, OrganizationsAPI from ciscosparkapi.api.licenses import License, LicensesAPI from ciscosparkapi.api.roles import Role, RolesAPI diff --git a/ciscosparkapi/api/webhooks.py b/ciscosparkapi/api/webhooks.py index 18fc64f..3c1c8d5 100644 --- a/ciscosparkapi/api/webhooks.py +++ b/ciscosparkapi/api/webhooks.py @@ -3,8 +3,8 @@ Classes: Webhook: Models a Spark 'webhook' JSON object as a native Python object. - WebhooksAPI: Wrappers the Cisco Spark Webhooks-API and exposes the API - calls as Python method calls that return native Python objects. + WebhooksAPI: Wraps the Cisco Spark Webhooks-API and exposes the APIs as + native Python methods that return native Python objects. """ @@ -19,10 +19,16 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.exceptions import ciscosparkapiException -from ciscosparkapi.utils import generator_container +from ciscosparkapi.api.memberships import Membership +from ciscosparkapi.api.messages import Message +from ciscosparkapi.api.rooms import Room from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) __author__ = "Chris Lunsford" @@ -35,10 +41,10 @@ class Webhook(SparkData): """Model a Spark 'webhook' JSON object as a native Python object.""" def __init__(self, json): - """Init a new Webhook data object from a JSON dictionary or string. + """Init a new Webhook data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -81,54 +87,189 @@ def secret(self): """Secret used to generate payload signature.""" return self._json.get('secret') + @property + def orgId(self): + """The ID of the organization that owns the webhook.""" + return self._json.get('orgId') + + @property + def createdBy(self): + """The ID of the person that added the webhook.""" + return self._json.get('createdBy') + + @property + def appId(self): + """Identifies the application that added the webhook.""" + return self._json.get('appId') + + @property + def ownedBy(self): + """Indicates if the webhook is owned by the `org` or the `creator`. + + Webhooks owned by the creator can only receive events that are + accessible to the creator of the webhook. Those owned by the + organization will receive events that are visible to anyone in the + organization. + + """ + return self._json.get('ownedBy') + + @property + def status(self): + """Indicates if the webhook is active. + + A webhook that cannot reach your URL is disabled. + + """ + return self._json.get('status') + @property def created(self): """Creation date and time in ISO8601 format.""" return self._json.get('created') + +class WebhookEvent(SparkData): + """Model a Spark webhook event JSON object as a native Python object.""" + + def __init__(self, json): + """Init a WebhookEvent data object from a JSON dictionary or string. + + Args: + json(dict, basestring): Input dictionary or JSON string. + + Raises: + TypeError: If the input object is not a dictionary or string. + + """ + super(WebhookEvent, self).__init__(json) + + self._data = None + + @property + def id(self): + """Webhook ID.""" + return self._json.get('id') + + @property + def name(self): + """A user-friendly name for this webhook.""" + return self._json.get('name') + + @property + def resource(self): + """The resource type for the webhook.""" + return self._json.get('resource') + + @property + def event(self): + """The event type for the webhook.""" + return self._json.get('event') + + @property + def filter(self): + """The filter that defines the webhook scope.""" + return self._json.get('filter') + + @property + def orgId(self): + """The ID of the organization that owns the webhook.""" + return self._json.get('orgId') + + @property + def createdBy(self): + """The ID of the person that added the webhook.""" + return self._json.get('createdBy') + + @property + def appId(self): + """Identifies the application that added the webhook.""" + return self._json.get('appId') + + @property + def ownedBy(self): + """Indicates if the webhook is owned by the `org` or the `creator`. + + Webhooks owned by the creator can only receive events that are + accessible to the creator of the webhook. Those owned by the + organization will receive events that are visible to anyone in the + organization. + + """ + return self._json.get('ownedBy') + + @property + def status(self): + """Indicates if the webhook is active. + + A webhook that cannot reach your URL is disabled. + + """ + return self._json.get('status') + + @property + def actorId(self): + """The ID of the person that caused the webhook to be sent.""" + return self._json.get('actorId') + @property def data(self): - """The object representation of the resource triggering the webhook. + """The data for the resource that triggered the webhook. + + For example, if you registered a webhook that triggers when messages + are created (i.e. posted into a room) then the data property will + contain the JSON representation for a message resource. - The data property contains the object representation of the resource - that triggered the webhook. For example, if you registered a webhook - that triggers when messages are created (i.e. posted into a room) then - the data property will contain the representation for a message - resource, as specified in the Messages API documentation. + Note: That not all of the details of the resource are included in the + data object. For example, the contents of a message are not included. + You would need to request the details for the message using the message + 'id' (which is in the data object) and the + `CiscoSparkAPI.messages.get()` method. """ - object_data = self._json.get('data', None) - if object_data: - return SparkData(object_data) - else: - return None + if self._data is None and self._json.get('data'): + if self.resource == "memberships": + self._data = Membership(self._json.get('data')) + + elif self.resource == "messages": + self._data = Message(self._json.get('data')) + + elif self.resource == "rooms": + self._data = Room(self._json.get('data')) + + else: + self._data = SparkData(self._json.get('data')) + + return self._data class WebhooksAPI(object): """Cisco Spark Webhooks-API wrapper class. - Wrappers the Cisco Spark Webhooks-API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark Webhooks-API and exposes the APIs as native Python + methods that return native Python objects. """ def __init__(self, session): - """Init a new WebhooksAPI object with the provided RestSession. + """Initialize a new WebhooksAPI object with the provided RestSession. Args: session(RestSession): The RESTful session object to be used for API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession) + super(WebhooksAPI, self).__init__() + self._session = session @generator_container - def list(self, max=None): + def list(self, max=None, **request_parameters): """List all of the authenticated user's webhooks. This method supports Cisco Spark's implementation of RFC5988 Web @@ -142,31 +283,36 @@ def list(self, max=None): container. Args: - max(int): Limits the maximum number of webhooks returned from the - Spark service per request. + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the webhooks returned by the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the webhooks returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert max is None or isinstance(max, int) - params = {} - if max: - params['max'] = max + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + max=max, + ) + # API request - get items items = self._session.get_items('webhooks', params=params) + # Yield Webhook objects created from the returned items JSON objects for item in items: yield Webhook(item) def create(self, name, targetUrl, resource, event, - filter=None, secret=None): + filter=None, secret=None, **request_parameters): """Create a webhook. Args: @@ -176,103 +322,112 @@ def create(self, name, targetUrl, resource, event, resource(basestring): The resource type for the webhook. event(basestring): The event type for the webhook. filter(basestring): The filter that defines the webhook scope. - secret(basestring): secret used to generate payload signature. + secret(basestring): The secret used to generate payload signature. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Webhook: With the details of the created webhook. + Webhook: A Webhook object with the details of the created webhook. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(name, basestring) - assert isinstance(targetUrl, basestring) - assert isinstance(resource, basestring) - assert isinstance(event, basestring) - assert filter is None or isinstance(filter, basestring) - assert secret is None or isinstance(secret, basestring) - post_data = {} - post_data['name'] = name - post_data['targetUrl'] = targetUrl - post_data['resource'] = resource - post_data['event'] = event - if filter: - post_data['filter'] = filter - if secret: - post_data['secret'] = secret + check_type(name, basestring, may_be_none=False) + check_type(targetUrl, basestring, may_be_none=False) + check_type(resource, basestring, may_be_none=False) + check_type(event, basestring, may_be_none=False) + check_type(filter, basestring) + check_type(secret, basestring) + + post_data = dict_from_items_with_values( + request_parameters, + name=name, + targetUrl=targetUrl, + resource=resource, + event=event, + filter=filter, + secret=secret, + ) + # API request - json_obj = self._session.post('webhooks', json=post_data) + json_data = self._session.post('webhooks', json=post_data) + # Return a Webhook object created from the response JSON data - return Webhook(json_obj) + return Webhook(json_data) def get(self, webhookId): """Get the details of a webhook, by ID. Args: - webhookId(basestring): The webhookId of the webhook. + webhookId(basestring): The ID of the webhook to be retrieved. Returns: - Webhook: With the details of the requested webhook. + Webhook: A Webhook object with the details of the requested + webhook. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(webhookId, basestring) + check_type(webhookId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('webhooks/' + webhookId) + json_data = self._session.get('webhooks/' + webhookId) + # Return a Webhook object created from the response JSON data - return Webhook(json_obj) + return Webhook(json_data) - def update(self, webhookId, **update_attributes): - """Update details for a webhook. + def update(self, webhookId, name=None, targetUrl=None, + **request_parameters): + """Update a webhook, by ID. Args: - webhookId(basestring): The webhookId of the webhook to be - updated. + webhookId(basestring): The webhook ID. name(basestring): A user-friendly name for this webhook. targetUrl(basestring): The URL that receives POST requests for each event. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - Webhook: With the updated Spark webhook details. + Webhook: A Webhook object with the updated Spark webhook details. Raises: - AssertionError: If the parameter types are incorrect. - ciscosparkapiException: If an update attribute is not provided. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(webhookId, basestring) - # Process update_attributes keyword arguments - if not update_attributes: - error_message = "At least one **update_attributes keyword " \ - "argument must be specified." - raise ciscosparkapiException(error_message) + check_type(webhookId, basestring, may_be_none=False) + check_type(name, basestring) + check_type(targetUrl, basestring) + + put_data = dict_from_items_with_values( + request_parameters, + name=name, + targetUrl=targetUrl, + ) + # API request - json_obj = self._session.put('webhooks/' + webhookId, - json=update_attributes) + json_data = self._session.put('webhooks/' + webhookId, json=put_data) + # Return a Webhook object created from the response JSON data - return Webhook(json_obj) + return Webhook(json_data) def delete(self, webhookId): - """Delete a webhook. + """Delete a webhook, by ID. Args: - webhookId(basestring): The webhookId of the webhook to be - deleted. + webhookId(basestring): The ID of the webhook to be deleted. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(webhookId, basestring) + check_type(webhookId, basestring, may_be_none=False) + # API request self._session.delete('webhooks/' + webhookId) From bff6a86aaf16a6f6f489068b631c2e34f37673eb Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 00:03:35 -0400 Subject: [PATCH 23/31] Refactor Organizations Apply structural changes to the RoomsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/organizations.py | 74 +++++++++++++++++------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/ciscosparkapi/api/organizations.py b/ciscosparkapi/api/organizations.py index 26bd150..9ddec71 100644 --- a/ciscosparkapi/api/organizations.py +++ b/ciscosparkapi/api/organizations.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -"""Cisco Spark Organizations API wrapper. +"""Cisco Spark Organizations-API wrapper. Classes: Organization: Models a Spark Organization JSON object as a native Python object. - OrganizationsAPI: Wraps the Cisco Spark Organizations API and exposes the - API calls as Python method calls that return native Python objects. + OrganizationsAPI: Wraps the Cisco Spark Organizations-API and exposes the + APIs as native Python methods that return native Python objects. """ @@ -20,9 +20,13 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.utils import generator_container from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) __author__ = "Chris Lunsford" @@ -35,10 +39,10 @@ class Organization(SparkData): """Model a Spark Organization JSON object as a native Python object.""" def __init__(self, json): - """Init a new Organization data object from a dict or JSON string. + """Init a Organization data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -58,15 +62,15 @@ def displayName(self): @property def created(self): - """The date and time the Organization was created.""" + """Creation date and time in ISO8601 format.""" return self._json.get('created') class OrganizationsAPI(object): - """Cisco Spark Organizations API wrapper. + """Cisco Spark Organizations-API wrapper. - Wraps the Cisco Spark Organizations API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark Organizations-API and exposes the APIs as native + Python methods that return native Python objects. """ @@ -78,15 +82,17 @@ def __init__(self, session): API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession, may_be_none=False) + super(OrganizationsAPI, self).__init__() + self._session = session @generator_container - def list(self, max=None): + def list(self, max=None, **request_parameters): """List Organizations. This method supports Cisco Spark's implementation of RFC5988 Web @@ -100,26 +106,30 @@ def list(self, max=None): container. Args: - max(int): Limits the maximum number of entries returned from the - Spark service per request (page size; requesting additional - pages is handled automatically). + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the objects returned from the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the organizations returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert max is None or isinstance(max, int) - params = {} - if max: - params['max'] = max + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + max=max, + ) + # API request - get items items = self._session.get_items('organizations', params=params) + # Yield Organization objects created from the returned JSON objects for item in items: yield Organization(item) @@ -128,19 +138,21 @@ def get(self, orgId): """Get the details of an Organization, by id. Args: - orgId(basestring): The id of the Organization. + orgId(basestring): The ID of the Organization to be retrieved. Returns: - Organization: With the details of the requested Organization. + Organization: An Organization object with the details of the + requested organization. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(orgId, basestring) + check_type(orgId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('organizations/' + orgId) + json_data = self._session.get('organizations/' + orgId) + # Return a Organization object created from the returned JSON object - return Organization(json_obj) + return Organization(json_data) From b4cf840427b1f710a716fd203a4d11d1b2f76126 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 00:14:58 -0400 Subject: [PATCH 24/31] Refactor Licenses Apply structural changes to the RoomsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/licenses.py | 90 +++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/ciscosparkapi/api/licenses.py b/ciscosparkapi/api/licenses.py index 3c8252a..9644447 100644 --- a/ciscosparkapi/api/licenses.py +++ b/ciscosparkapi/api/licenses.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -"""Cisco Spark Licenses API wrapper. +"""Cisco Spark Licenses-API wrapper. Classes: License: Models a Spark License JSON object as a native Python object. - LicensesAPI: Wraps the Cisco Spark Licenses API and exposes the - API calls as Python method calls that return native Python objects. + LicensesAPI: Wraps the Cisco Spark Licenses-API and exposes the APIs as + native Python methods that return native Python objects. """ @@ -19,9 +19,13 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.utils import generator_container from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) __author__ = "Chris Lunsford" @@ -34,10 +38,10 @@ class License(SparkData): """Model a Spark License JSON object as a native Python object.""" def __init__(self, json): - """Init a new License data object from a dict or JSON string. + """Initialize a License data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -67,33 +71,36 @@ def consumedUnits(self): class LicensesAPI(object): - """Cisco Spark Licenses API wrapper. + """Cisco Spark Licenses-API wrapper. - Wraps the Cisco Spark Licenses API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark Licenses-API and exposes the APIs as native Python + methods that return native Python objects. """ def __init__(self, session): - """Init a new LicensesAPI object with the provided RestSession. + """Initialize a new LicensesAPI object with the provided RestSession. Args: session(RestSession): The RESTful session object to be used for API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the input object is not a dictionary or string. """ - assert isinstance(session, RestSession) + check_type(session, RestSession, may_be_none=False) + super(LicensesAPI, self).__init__() + self._session = session @generator_container - def list(self, orgId=None, max=None): - """List Licenses. + def list(self, orgId=None, max=None, **request_parameters): + """List all licenses for a given organization. - Optionally filtered by Organization (orgId parameter). + If no orgId is specified, the default is the organization of the + authenticated user. This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator @@ -106,53 +113,56 @@ def list(self, orgId=None, max=None): container. Args: - orgId(basestring): Filters the returned licenses to only include - those liceses associated with the specified Organization - (orgId). - max(int): Limits the maximum number of entries returned from the - Spark service per request (page size; requesting additional - pages is handled automatically). + orgId(basestring): Specify the organization, by ID. + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the objects returned from the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the licenses returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert orgId is None or isinstance(orgId, basestring) - assert max is None or isinstance(max, int) - params = {} - if orgId: - params['orgId'] = orgId - if max: - params['max'] = max + check_type(orgId, basestring) + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + orgId=orgId, + max=max, + ) + # API request - get items items = self._session.get_items('licenses', params=params) + # Yield License objects created from the returned JSON objects for item in items: yield License(item) def get(self, licenseId): - """Get the details of a License, by id. + """Get the details of a License, by ID. Args: - licenseId(basestring): The id of the License. + licenseId(basestring): The ID of the License to be retrieved. Returns: - License: With the details of the requested License. + License: A License object with the details of the requested + License. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(licenseId, basestring) + check_type(licenseId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('licenses/' + licenseId) + json_data = self._session.get('licenses/' + licenseId) + # Return a License object created from the returned JSON object - return License(json_obj) + return License(json_data) From 968eb9a9c37cacda520e3ba845727cdae633deb7 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 00:22:33 -0400 Subject: [PATCH 25/31] Refactor Roles Apply structural changes to the RoomsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/roles.py | 77 ++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/ciscosparkapi/api/roles.py b/ciscosparkapi/api/roles.py index 5b2d9c8..8e2feaf 100644 --- a/ciscosparkapi/api/roles.py +++ b/ciscosparkapi/api/roles.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -"""Cisco Spark Roles API wrapper. +"""Cisco Spark Roles-API wrapper. Classes: Role: Models a Spark Role JSON object as a native Python object. - RolesAPI: Wraps the Cisco Spark Roles API and exposes the - API calls as Python method calls that return native Python objects. + RolesAPI: Wraps the Cisco Spark Roles-API and exposes the APIs as native + Python methods that return native Python objects. """ @@ -19,9 +19,13 @@ from builtins import * from past.builtins import basestring -from ciscosparkapi.utils import generator_container from ciscosparkapi.restsession import RestSession from ciscosparkapi.sparkdata import SparkData +from ciscosparkapi.utils import ( + check_type, + dict_from_items_with_values, + generator_container, +) __author__ = "Chris Lunsford" @@ -34,10 +38,10 @@ class Role(SparkData): """Model a Spark Role JSON object as a native Python object.""" def __init__(self, json): - """Init a new Role data object from a dict or JSON string. + """Initialize a new Role data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -57,31 +61,33 @@ def name(self): class RolesAPI(object): - """Cisco Spark Roles API wrapper. + """Cisco Spark Roles-API wrapper. - Wraps the Cisco Spark Roles API and exposes the API calls as Python - method calls that return native Python objects. + Wraps the Cisco Spark Roles-API and exposes the APIs as native Python + methods that return native Python objects. """ def __init__(self, session): - """Init a new RolesAPI object with the provided RestSession. + """Initialize a new RolesAPI object with the provided RestSession. Args: session(RestSession): The RESTful session object to be used for API calls to the Cisco Spark service. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(session, RestSession) + check_type(session, RestSession, may_be_none=False) + super(RolesAPI, self).__init__() + self._session = session @generator_container - def list(self, max=None): - """List Roles. + def list(self, max=None, **request_parameters): + """List all roles. This method supports Cisco Spark's implementation of RFC5988 Web Linking to provide pagination support. It returns a generator @@ -94,47 +100,52 @@ def list(self, max=None): container. Args: - max(int): Limits the maximum number of entries returned from the - Spark service per request (page size; requesting additional - pages is handled automatically). + max(int): Limit the maximum number of items returned from the Spark + service per request. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). Returns: - GeneratorContainer: When iterated, the GeneratorContainer, yields - the objects returned from the Cisco Spark query. + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the roles returned by the Cisco Spark query. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert max is None or isinstance(max, int) - params = {} - if max: - params['max'] = max + check_type(max, int) + + params = dict_from_items_with_values( + request_parameters, + max=max, + ) + # API request - get items items = self._session.get_items('roles', params=params) + # Yield Role objects created from the returned JSON objects for item in items: yield Role(item) def get(self, roleId): - """Get the details of a Role, by id. + """Get the details of a Role, by ID. Args: - roleId(basestring): The id of the Role. + roleId(basestring): The ID of the Role to be retrieved. Returns: - Role: With the details of the requested Role. + Role: A Role object with the details of the requested Role. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(roleId, basestring) + check_type(roleId, basestring, may_be_none=False) + # API request - json_obj = self._session.get('roles/' + roleId) + json_data = self._session.get('roles/' + roleId) + # Return a Role object created from the returned JSON object - return Role(json_obj) + return Role(json_data) From 717aef085c7fae67c2754538a2eab1b1c18054db Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 00:45:58 -0400 Subject: [PATCH 26/31] Refactor Access Tokens Apply structural changes to the RoomsAPI and ensure all parameters are up-to-date. --- ciscosparkapi/api/accesstokens.py | 109 ++++++++++++++++-------------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/ciscosparkapi/api/accesstokens.py b/ciscosparkapi/api/accesstokens.py index 07ad05d..c492fff 100644 --- a/ciscosparkapi/api/accesstokens.py +++ b/ciscosparkapi/api/accesstokens.py @@ -4,8 +4,8 @@ Classes: AccessToken: Models a Spark 'access token' JSON object as a native Python object. - AccessTokensAPI: Wrappers the Cisco Spark AccessTokens-API and exposes the - API calls as Python method calls that return native Python objects. + AccessTokensAPI: Wraps the Cisco Spark Access-Tokens-API and exposes the + APIs as native Python methods that return native Python objects. """ @@ -26,13 +26,15 @@ import requests +from ciscosparkapi.responsecodes import EXPECTED_RESPONSE_CODE from ciscosparkapi.sparkdata import SparkData from ciscosparkapi.utils import ( - validate_base_url, check_response_code, + check_type, + dict_from_items_with_values, extract_and_parse_json, + validate_base_url, ) -from ciscosparkapi.responsecodes import EXPECTED_RESPONSE_CODE __author__ = "Chris Lunsford" @@ -48,10 +50,10 @@ class AccessToken(SparkData): """Model a Spark 'access token' JSON object as a native Python object.""" def __init__(self, json): - """Init a new AccessToken data object from a JSON dictionary or string. + """Init a new AccessToken data object from a dictionary or JSON string. Args: - json(dict, basestring): Input JSON object. + json(dict, basestring): Input dictionary or JSON string. Raises: TypeError: If the input object is not a dictionary or string. @@ -61,59 +63,62 @@ def __init__(self, json): @property def access_token(self): - """Cisco Spark access_token.""" + """Cisco Spark access token.""" return self._json.get('access_token') @property def expires_in(self): - """Access token expires_in number of seconds.""" + """Access token expiry time (in seconds).""" return self._json.get('expires_in') @property def refresh_token(self): - """refresh_token used to request a new/refreshed access_token.""" + """Refresh token used to request a new/refreshed access token.""" return self._json.get('refresh_token') @property def refresh_token_expires_in(self): - """refresh_token_expires_in number of seconds.""" + """Refresh token expiry time (in seconds).""" return self._json.get('refresh_token_expires_in') class AccessTokensAPI(object): """Cisco Spark Access-Tokens-API wrapper class. - Wrappers the Cisco Spark Access-Tokens-API and exposes the API calls as - Python method calls that return native Python objects. + Wraps the Cisco Spark Access-Tokens-API and exposes the APIs as native + Python methods that return native Python objects. """ def __init__(self, base_url, timeout=None): - """Init a new AccessTokensAPI object with the provided RestSession. + """Initialize an AccessTokensAPI object with the provided RestSession. Args: base_url(basestring): The base URL the API endpoints. timeout(int): Timeout in seconds for the API requests. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. """ - assert isinstance(base_url, basestring) - assert timeout is None or isinstance(timeout, int) + check_type(base_url, basestring, may_be_none=False) + check_type(timeout, int) + super(AccessTokensAPI, self).__init__() + self._base_url = str(validate_base_url(base_url)) self._timeout = timeout self._endpoint_url = urllib.parse.urljoin(self.base_url, API_ENDPOINT) - self._request_kwargs = {} - self._request_kwargs["timeout"] = timeout + self._request_kwargs = {"timeout": timeout} @property def base_url(self): + """The base URL the API endpoints.""" return self._base_url @property def timeout(self): + """Timeout in seconds for the API requests.""" return self._timeout def get(self, client_id, client_secret, code, redirect_uri): @@ -123,8 +128,7 @@ def get(self, client_id, client_secret, code, redirect_uri): invoke the APIs. Args: - client_id(basestring): Provided when you created your - integration. + client_id(basestring): Provided when you created your integration. client_secret(basestring): Provided when you created your integration. code(basestring): The Authorization Code provided by the user @@ -133,40 +137,41 @@ def get(self, client_id, client_secret, code, redirect_uri): process. Returns: - AccessToken: With the access token provided by the Cisco Spark - cloud. + AccessToken: An AccessToken object with the access token provided + by the Cisco Spark cloud. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(client_id, basestring) - assert isinstance(client_secret, basestring) - assert isinstance(code, basestring) - assert isinstance(redirect_uri, basestring) - # Build request parameters - data = {} - data["grant_type"] = "authorization_code" - data["client_id"] = client_id - data["client_secret"] = client_secret - data["code"] = code - data["redirect_uri"] = redirect_uri + check_type(client_id, basestring, may_be_none=False) + check_type(client_secret, basestring, may_be_none=False) + check_type(code, basestring, may_be_none=False) + check_type(redirect_uri, basestring, may_be_none=False) + + post_data = dict_from_items_with_values( + grant_type="authorization_code", + client_id=client_id, + client_secret=client_secret, + code=code, + redirect_uri=redirect_uri, + ) + # API request - response = requests.post(self._endpoint_url, data=data, + response = requests.post(self._endpoint_url, data=post_data, **self._request_kwargs) check_response_code(response, EXPECTED_RESPONSE_CODE['POST']) json_data = extract_and_parse_json(response) + # Return a AccessToken object created from the response JSON data return AccessToken(json_data) def refresh(self, client_id, client_secret, refresh_token): - """Return a refreshed Access Token via the provided refresh_token. + """Return a refreshed Access Token from the provided refresh_token. Args: - client_id(basestring): Provided when you created your - integration. + client_id(basestring): Provided when you created your integration. client_secret(basestring): Provided when you created your integration. refresh_token(basestring): Provided when you requested the Access @@ -177,24 +182,26 @@ def refresh(self, client_id, client_secret, refresh_token): cloud. Raises: - AssertionError: If the parameter types are incorrect. + TypeError: If the parameter types are incorrect. SparkApiError: If the Cisco Spark cloud returns an error. """ - # Process args - assert isinstance(client_id, basestring) - assert isinstance(client_secret, basestring) - assert isinstance(refresh_token, basestring) - # Build request parameters - data = {} - data["grant_type"] = "refresh_token" - data["client_id"] = client_id - data["client_secret"] = client_secret - data["refresh_token"] = refresh_token + check_type(client_id, basestring, may_be_none=False) + check_type(client_secret, basestring, may_be_none=False) + check_type(refresh_token, basestring, may_be_none=False) + + post_data = dict_from_items_with_values( + grant_type="refresh_token", + client_id=client_id, + client_secret=client_secret, + refresh_token=refresh_token, + ) + # API request - response = requests.post(self._endpoint_url, data=data, + response = requests.post(self._endpoint_url, data=post_data, **self._request_kwargs) check_response_code(response, EXPECTED_RESPONSE_CODE['POST']) json_data = extract_and_parse_json(response) + # Return a AccessToken object created from the response JSON data return AccessToken(json_data) From 3f8a9e074610148c7613304a94d6911e2a659f1f Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 00:46:38 -0400 Subject: [PATCH 27/31] Update Docstrings --- ciscosparkapi/api/licenses.py | 2 +- ciscosparkapi/api/organizations.py | 4 ++-- ciscosparkapi/api/people.py | 4 ++-- ciscosparkapi/api/roles.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ciscosparkapi/api/licenses.py b/ciscosparkapi/api/licenses.py index 9644447..e46e489 100644 --- a/ciscosparkapi/api/licenses.py +++ b/ciscosparkapi/api/licenses.py @@ -51,7 +51,7 @@ def __init__(self, json): @property def id(self): - """The unique id for the License.""" + """The unique ID for the License.""" return self._json.get('id') @property diff --git a/ciscosparkapi/api/organizations.py b/ciscosparkapi/api/organizations.py index 9ddec71..e234807 100644 --- a/ciscosparkapi/api/organizations.py +++ b/ciscosparkapi/api/organizations.py @@ -52,7 +52,7 @@ def __init__(self, json): @property def id(self): - """The unique id for the Organization.""" + """The unique ID for the Organization.""" return self._json.get('id') @property @@ -135,7 +135,7 @@ def list(self, max=None, **request_parameters): yield Organization(item) def get(self, orgId): - """Get the details of an Organization, by id. + """Get the details of an Organization, by ID. Args: orgId(basestring): The ID of the Organization to be retrieved. diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index 3342728..f77e6fc 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -176,7 +176,7 @@ def list(self, email=None, displayName=None, id=None, orgId=None, max=None, the displayName to be searched. id(basestring): List people by ID. Accepts up to 85 person IDs separated by commas. - orgId(basestring): The organization id. + orgId(basestring): The organization ID. max(int): Limit the maximum number of items returned from the Spark service per request. **request_parameters: Additional request parameters (provides @@ -365,7 +365,7 @@ def delete(self, personId): Only an admin can remove a person. Args: - personId(basestring): The 'id' of the person to be deleted. + personId(basestring): The ID of the person to be deleted. Raises: AssertionError: If the parameter types are incorrect. diff --git a/ciscosparkapi/api/roles.py b/ciscosparkapi/api/roles.py index 8e2feaf..648f217 100644 --- a/ciscosparkapi/api/roles.py +++ b/ciscosparkapi/api/roles.py @@ -51,7 +51,7 @@ def __init__(self, json): @property def id(self): - """The unique id for the Role.""" + """The unique ID for the Role.""" return self._json.get('id') @property From 60a5dccd8804c2189ec6decf22e47ea58e575933 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 00:58:43 -0400 Subject: [PATCH 28/31] Update Response Codes --- ciscosparkapi/responsecodes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ciscosparkapi/responsecodes.py b/ciscosparkapi/responsecodes.py index 618bd30..ff24cea 100644 --- a/ciscosparkapi/responsecodes.py +++ b/ciscosparkapi/responsecodes.py @@ -24,6 +24,8 @@ 404: "The URI requested is invalid or the resource requested, such as a " "user, does not exist. Also returned when the requested format is " "not supported by the requested method.", + 405: "The request was made to a resource using an HTTP request method " + "that is not supported.", 409: "The request could not be processed because it conflicts with some " "established rule of the system. For example, a person may not be " "added to a room more than once.", @@ -32,6 +34,8 @@ "present that specifies how many seconds you need to wait before a " "successful request can be made.", 500: "Something went wrong on the server.", + 503: "The server received an invalid response from an upstream server " + "while processing the request. Try again later.", 503: "Server is overloaded with requests. Try again later." } From e3f011329e22cc0183497fcebe3f1fec644b132c Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 00:58:59 -0400 Subject: [PATCH 29/31] Update Docs --- docs/user/api.rst | 10 ++++++++++ docs/user/quickstart.rst | 7 +++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/user/api.rst b/docs/user/api.rst index 306e0aa..e47a16e 100644 --- a/docs/user/api.rst +++ b/docs/user/api.rst @@ -117,6 +117,8 @@ Exceptions .. autoexception:: SparkApiError +.. autoexception:: SparkRateLimitError + .. _Spark Data Objects: @@ -180,6 +182,14 @@ Webhook .. autoclass:: Webhook() +.. _WebhookEvent: + +Webhook Event +------------- + +.. autoclass:: WebhookEvent() + + .. _Organization: Organization diff --git a/docs/user/quickstart.rst b/docs/user/quickstart.rst index 138e755..e178cf7 100644 --- a/docs/user/quickstart.rst +++ b/docs/user/quickstart.rst @@ -303,10 +303,9 @@ You can catch any errors returned by the Cisco Spark cloud by catching >>> ciscosparkapi will also raise a number of other standard errors -(:exc:`AssertionError`, :exc:`TypeError`, etc.) and other package errors like -:exc:`ciscosparkapiException`; however, these errors are usually caused by -incorrect use of the package or methods and should be sorted while debugging -your app. +(:exc:`TypeError`, :exc:`ValueError`, etc.); however, these errors are usually +caused by incorrect use of the package or methods and should be sorted while +debugging your app. Working with Returned Objects From d91b27efaa1cbbba5e515f8cfe969e0e0b1e5b9d Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 01:05:27 -0400 Subject: [PATCH 30/31] Update mentionedPeople test to use type string mentionedPeople should be a string not a list. --- tests/api/test_messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api/test_messages.py b/tests/api/test_messages.py index 47a6ac3..ca15c9f 100644 --- a/tests/api/test_messages.py +++ b/tests/api/test_messages.py @@ -207,7 +207,7 @@ def test_list_messages_with_paging(self, api, group_room, def test_list_messages_mentioning_me(self, api, group_room, group_room_markdown_message): - messages = list_messages(api, group_room.id, mentionedPeople=["me"]) + messages = list_messages(api, group_room.id, mentionedPeople="me") messages_list = list(messages) assert len(messages_list) >= 1 assert are_valid_messages(messages_list) From 6fa7215fbf039ec7c6be34d45f2c78f630d327e3 Mon Sep 17 00:00:00 2001 From: Chris Lunsford Date: Sun, 29 Oct 2017 01:20:25 -0400 Subject: [PATCH 31/31] PEP8 --- .flake8 | 2 +- ciscosparkapi/api/organizations.py | 2 +- ciscosparkapi/api/people.py | 36 +++++++++++++++--------------- ciscosparkapi/api/rooms.py | 10 ++++----- ciscosparkapi/api/teams.py | 2 +- ciscosparkapi/exceptions.py | 8 +++---- ciscosparkapi/responsecodes.py | 2 +- 7 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.flake8 b/.flake8 index 2807825..4e6f7e1 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -ignore = E402,F401,F403 +ignore = E402,F401,F403,F405,W503 exclude = .git, __pycache__, diff --git a/ciscosparkapi/api/organizations.py b/ciscosparkapi/api/organizations.py index e234807..760182e 100644 --- a/ciscosparkapi/api/organizations.py +++ b/ciscosparkapi/api/organizations.py @@ -92,7 +92,7 @@ def __init__(self, session): self._session = session @generator_container - def list(self, max=None, **request_parameters): + def list(self, max=None, **request_parameters): """List Organizations. This method supports Cisco Spark's implementation of RFC5988 Web diff --git a/ciscosparkapi/api/people.py b/ciscosparkapi/api/people.py index f77e6fc..e327523 100644 --- a/ciscosparkapi/api/people.py +++ b/ciscosparkapi/api/people.py @@ -254,15 +254,15 @@ def create(self, emails, displayName=None, firstName=None, lastName=None, check_type(licenses, list) post_data = dict_from_items_with_values( - request_parameters, - emails=emails, - displayName=displayName, - firstName=firstName, - lastName=lastName, - avatar=avatar, - orgId=orgId, - roles=roles, - licenses=licenses, + request_parameters, + emails=emails, + displayName=displayName, + firstName=firstName, + lastName=lastName, + avatar=avatar, + orgId=orgId, + roles=roles, + licenses=licenses, ) # API request @@ -342,15 +342,15 @@ def update(self, personId, emails=None, displayName=None, firstName=None, check_type(licenses, list) put_data = dict_from_items_with_values( - request_parameters, - emails=emails, - displayName=displayName, - firstName=firstName, - lastName=lastName, - avatar=avatar, - orgId=orgId, - roles=roles, - licenses=licenses, + request_parameters, + emails=emails, + displayName=displayName, + firstName=firstName, + lastName=lastName, + avatar=avatar, + orgId=orgId, + roles=roles, + licenses=licenses, ) # API request diff --git a/ciscosparkapi/api/rooms.py b/ciscosparkapi/api/rooms.py index ac45b5f..77591c5 100644 --- a/ciscosparkapi/api/rooms.py +++ b/ciscosparkapi/api/rooms.py @@ -199,9 +199,9 @@ def create(self, title, teamId=None, **request_parameters): check_type(teamId, basestring) post_data = dict_from_items_with_values( - request_parameters, - title=title, - teamId=teamId, + request_parameters, + title=title, + teamId=teamId, ) # API request @@ -253,8 +253,8 @@ def update(self, roomId, title=None, **request_parameters): check_type(roomId, basestring) put_data = dict_from_items_with_values( - request_parameters, - title=title, + request_parameters, + title=title, ) # API request diff --git a/ciscosparkapi/api/teams.py b/ciscosparkapi/api/teams.py index d325a53..d3e16d9 100644 --- a/ciscosparkapi/api/teams.py +++ b/ciscosparkapi/api/teams.py @@ -138,7 +138,7 @@ def list(self, max=None, **request_parameters): for item in items: yield Team(item) - def create(self, name, **request_parameters): + def create(self, name, **request_parameters): """Create a team. The authenticated user is automatically added as a member of the team. diff --git a/ciscosparkapi/exceptions.py b/ciscosparkapi/exceptions.py index e9b8cc4..94a632b 100644 --- a/ciscosparkapi/exceptions.py +++ b/ciscosparkapi/exceptions.py @@ -68,11 +68,11 @@ def response_to_string(response): # Prepare request components req = textwrap.fill("{} {}".format(request.method, request.url), width=79, - subsequent_indent=' ' * (len(request.method)+1)) + subsequent_indent=' ' * (len(request.method) + 1)) req_headers = [ textwrap.fill("{}: {}".format(*sanitize(header)), width=79, - subsequent_indent=' '*4) + subsequent_indent=' ' * 4) for header in request.headers.items() ] req_body = (textwrap.fill(to_unicode(request.body), width=79) @@ -83,10 +83,10 @@ def response_to_string(response): response.reason if response.reason else ""), width=79, - subsequent_indent=' ' * (len(request.method)+1)) + subsequent_indent=' ' * (len(request.method) + 1)) resp_headers = [ textwrap.fill("{}: {}".format(*header), width=79, - subsequent_indent=' '*4) + subsequent_indent=' ' * 4) for header in response.headers.items() ] resp_body = textwrap.fill(response.text, width=79) if response.text else "" diff --git a/ciscosparkapi/responsecodes.py b/ciscosparkapi/responsecodes.py index ff24cea..ef4129c 100644 --- a/ciscosparkapi/responsecodes.py +++ b/ciscosparkapi/responsecodes.py @@ -34,7 +34,7 @@ "present that specifies how many seconds you need to wait before a " "successful request can be made.", 500: "Something went wrong on the server.", - 503: "The server received an invalid response from an upstream server " + 502: "The server received an invalid response from an upstream server " "while processing the request. Try again later.", 503: "Server is overloaded with requests. Try again later." }