diff --git a/docs/user/api.rst b/docs/user/api.rst index 59781ff..f4ec2b3 100644 --- a/docs/user/api.rst +++ b/docs/user/api.rst @@ -95,6 +95,15 @@ organizations .. autoclass:: webexpythonsdk.api.organizations.OrganizationsAPI() +.. _org_contacts: +.. _organization_contacts: + +organization_contacts +--------------------- + +.. autoclass:: webexpythonsdk.api.organization_contacts.OrganizationContactsAPI() + + .. _people: people @@ -230,6 +239,15 @@ Organization :inherited-members: +.. _OrganizationContact: + +Organization Contact +-------------------- + +.. autoclass:: OrganizationContact() + :inherited-members: + + .. _Person: Person diff --git a/docs/user/api_structure_table.rst b/docs/user/api_structure_table.rst index 23084a5..f17d976 100644 --- a/docs/user/api_structure_table.rst +++ b/docs/user/api_structure_table.rst @@ -30,6 +30,13 @@ | | :ref:`organizations` | :meth:`list() ` | | | | :meth:`create() ` | +------------------------+---------------------------+---------------------------------------------------------------------------------+ +| | :ref:`org_contacts` | :meth:`list() `| +| | | :meth:`search() `| +| | | :meth:`create() `| +| | | :meth:`get() `| +| | | :meth:`update() `| +| | | :meth:`delete() `| ++------------------------+---------------------------+---------------------------------------------------------------------------------+ | | :ref:`people` | :meth:`list() ` | | | | :meth:`create() ` | | | | :meth:`get() ` | diff --git a/src/webexpythonsdk/__init__.py b/src/webexpythonsdk/__init__.py index 43ae48a..cd91dd9 100644 --- a/src/webexpythonsdk/__init__.py +++ b/src/webexpythonsdk/__init__.py @@ -61,6 +61,7 @@ MeetingTemplate, Membership, Message, + OrganizationContact, Organization, Person, Recording, diff --git a/src/webexpythonsdk/api/__init__.py b/src/webexpythonsdk/api/__init__.py index d1e9d40..c7ee1b2 100644 --- a/src/webexpythonsdk/api/__init__.py +++ b/src/webexpythonsdk/api/__init__.py @@ -39,6 +39,7 @@ from .licenses import LicensesAPI from .memberships import MembershipsAPI from .messages import MessagesAPI +from .organization_contacts import OrganizationContactsAPI from .organizations import OrganizationsAPI from .people import PeopleAPI from .roles import RolesAPI @@ -219,6 +220,10 @@ def __init__( self.licenses = LicensesAPI(self._session, object_factory) self.memberships = MembershipsAPI(self._session, object_factory) self.messages = MessagesAPI(self._session, object_factory) + self.organization_contacts = OrganizationContactsAPI( + self._session, + object_factory, + ) self.organizations = OrganizationsAPI(self._session, object_factory) self.people = PeopleAPI(self._session, object_factory) self.roles = RolesAPI(self._session, object_factory) diff --git a/src/webexpythonsdk/api/organization_contacts.py b/src/webexpythonsdk/api/organization_contacts.py new file mode 100644 index 0000000..1e7ba42 --- /dev/null +++ b/src/webexpythonsdk/api/organization_contacts.py @@ -0,0 +1,518 @@ +"""Webex Organization Contacts API wrapper. + +Copyright (c) 2016-2024 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from ..generator_containers import generator_container +from ..exceptions import MalformedResponse +from ..restsession import RestSession +from ..utils import ( + check_type, + dict_from_items_with_values, + extract_and_parse_json, +) + + +API_ENDPOINT = "contacts/organizations" +OBJECT_TYPE = "organization_contact" +DEFAULT_CONTACT_TYPE = "CUSTOM" +DEFAULT_SCHEMAS = "urn:cisco:codev:identity:contact:core:1.0" +UPDATE_MUTABLE_FIELDS = { + "emails", + "phoneNumbers", + "extension", + "firstName", + "lastName", + "avatar", + "department", + "manager", + "managerId", + "title", + "addresses", + "customAttributes", + "displayName", +} + + +class OrganizationContactsAPI(object): + """Webex Organization Contacts API. + + Wraps the Webex Organization Contacts API and exposes the API as native + Python methods that return native Python objects. + + """ + + def __init__(self, session, object_factory): + """Init a new OrganizationContactsAPI object with RestSession. + + Args: + session(RestSession): The RESTful session object to be used for + API calls to the Webex service. + + Raises: + TypeError: If the parameter types are incorrect. + + """ + check_type(session, RestSession) + + super(OrganizationContactsAPI, self).__init__() + + self._session = session + self._object_factory = object_factory + + @staticmethod + def _base_endpoint(orgId): + """Build base endpoint for organization contacts.""" + return "{}/{}/contacts".format(API_ENDPOINT, orgId) + + @staticmethod + def _yield_result_items(pages): + """Yield contact items from paged responses.""" + # The organization contacts endpoints return "result" as top-level + # list key. For empty results, the API may omit "result" and return + # only metadata (for example: start/limit/total). + for json_page in pages: + assert isinstance(json_page, dict) + items = json_page.get("result") + if items is None: + if json_page.get("total") == 0: + continue + error_message = ( + "'result' key not found in JSON data: {!r}".format( + json_page + ) + ) + raise MalformedResponse(error_message) + + for item in items: + yield item + + def _search_pages(self, endpoint, params=None): + """Yield pages for org contacts search endpoints. + + Organization Contacts pagination is driven by ``start``, ``limit``, + and ``total`` fields in the response body, not by RFC5988 Link + headers. + """ + check_type(endpoint, str) + check_type(params, dict, optional=True) + + params = (params or {}).copy() + + while True: + json_page = self._session.get(endpoint, params=params) + assert isinstance(json_page, dict) + yield json_page + + total = json_page.get("total") + start = json_page.get("start") + limit = json_page.get("limit") + + if not all(isinstance(v, int) for v in [total, start, limit]): + break + if total <= 0 or limit <= 0: + break + + next_start = start + limit + if next_start >= total or next_start <= start: + break + + params["start"] = next_start + + @generator_container + def list( + self, + orgId, + keyword=None, + source=None, + limit=None, + groupIds=None, + **request_parameters, + ): + """List all contacts for a given organization. + + Args: + orgId(str): List contacts for this organization, by ID. + keyword(str): List contacts with a keyword. + source(str): List contacts with source. + limit(int): Limit the maximum number of contacts in the response. + groupIds(`list`): Filter contacts based on groups. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). + + Returns: + GeneratorContainer: A GeneratorContainer which, when iterated, + yields the contacts returned by the Webex query. + + Raises: + TypeError: If the parameter types are incorrect. + ApiError: If the Webex cloud returns an error. + + """ + check_type(orgId, str) + check_type(keyword, str, optional=True) + check_type(source, str, optional=True) + check_type(limit, int, optional=True) + check_type(groupIds, list, optional=True) + + params = dict_from_items_with_values( + request_parameters, + keyword=keyword, + source=source, + limit=limit, + ) + if groupIds: + params["groupIds"] = ",".join(groupIds) + + # API request - get pages from /search endpoint + pages = self._search_pages( + self._base_endpoint(orgId) + "/search", + params=params, + ) + + for item in self._yield_result_items(pages): + yield self._object_factory(OBJECT_TYPE, item) + + @generator_container + def search( + self, + orgId, + keyword=None, + source=None, + limit=None, + groupIds=None, + email=None, + displayName=None, + id=None, + max=None, # Backwards-compat alias for `limit`. + **request_parameters, + ): + """Search contacts. + + For most users, either the ``email`` or ``displayName`` parameter is + required. Admin users can omit these fields and list all contacts in + their organization. + + This method uses ``start``/``limit``/``total`` response fields to + provide pagination support. + + Args: + orgId(str): List contacts for this organization, by ID. + keyword(str): List contacts with a keyword. + source(str): List contacts with source. + limit(int): Limit the maximum number of contacts in the response. + groupIds(`list`): Filter contacts based on groups. + email(str): The e-mail address of the contact to be found. + displayName(str): The complete or beginning portion of the + displayName to be searched. + id(str): List contacts by ID. Accepts up to 85 contact IDs + separated by commas. + max(int): Backwards-compat alias for `limit`. + **request_parameters: Additional request parameters (provides + support for parameters that may be added in the future). + + Returns: + GeneratorContainer: A GeneratorContainer which, when iterated, + yields contacts returned by the Webex query. + + Raises: + TypeError: If the parameter types are incorrect. + ApiError: If the Webex cloud returns an error. + + """ + check_type(orgId, str) + check_type(keyword, str, optional=True) + check_type(source, str, optional=True) + check_type(limit, int, optional=True) + check_type(groupIds, list, optional=True) + check_type(id, str, optional=True) + check_type(email, str, optional=True) + check_type(displayName, str, optional=True) + check_type(max, int, optional=True) + + # Backwards compatibility with older People-like parameters. + if keyword is None: + keyword = email or displayName or id + if limit is None: + limit = max + + params = dict_from_items_with_values( + request_parameters, + keyword=keyword, + source=source, + limit=limit, + ) + if groupIds: + params["groupIds"] = ",".join(groupIds) + + # API request - get pages + pages = self._search_pages( + self._base_endpoint(orgId) + "/search", params=params + ) + + for item in self._yield_result_items(pages): + yield self._object_factory(OBJECT_TYPE, item) + + def get(self, orgId, contactId): + """Get a contact's details, by ID. + + Args: + orgId(str): The organization ID. + contactId(str): The ID of the contact to be retrieved. + + Returns: + OrganizationContact: Contact details as an object. + + Raises: + TypeError: If the parameter types are incorrect. + ApiError: If the Webex cloud returns an error. + + """ + check_type(orgId, str) + check_type(contactId, str) + + # API request + json_data = self._session.get( + self._base_endpoint(orgId) + "/" + contactId + ) + + # Return a contact object created from the returned JSON object + return self._object_factory(OBJECT_TYPE, json_data) + + def create( + self, + orgId, + emails, + contactType=DEFAULT_CONTACT_TYPE, + schemas=DEFAULT_SCHEMAS, + phoneNumbers=None, + extension=None, + firstName=None, + lastName=None, + avatar=None, + department=None, + manager=None, + managerId=None, + title=None, + addresses=None, + customAttributes=None, + displayName=None, + **request_parameters, + ): + """Create a contact. + + Args: + orgId(str): ID of the organization to which this contact belongs. + emails(`list`): Email address(es) of the contact. + contactType(str): Type of contact, e.g. ``CUSTOM`` or ``CLOUD``. + schemas(str): Contact schema identifier. + phoneNumbers(`list`): Phone numbers for the contact. + extension(str): Calling extension for the contact. + firstName(str): First name of the contact. + lastName(str): Last name of the contact. + avatar(str): URL to the contact avatar in PNG format. + department(str): The business department the contact belongs to. + manager(str): A manager identifier. + managerId(str): Person ID of the manager. + title(str): The contact's title. + addresses(`list`): Contact addresses. + customAttributes(`list`): Contact custom attributes. + displayName(str): Full name of the contact. + **request_parameters: Additional request parameters. + + Returns: + OrganizationContact: The created organization contact. + + Raises: + TypeError: If the parameter types are incorrect. + ApiError: If the Webex cloud returns an error. + + """ + check_type(orgId, str) + check_type(emails, list) + check_type(contactType, str, optional=True) + check_type(schemas, str, optional=True) + check_type(phoneNumbers, list, optional=True) + check_type(extension, str, optional=True) + check_type(firstName, str, optional=True) + check_type(lastName, str, optional=True) + check_type(avatar, str, optional=True) + check_type(department, str, optional=True) + check_type(manager, str, optional=True) + check_type(managerId, str, optional=True) + check_type(title, str, optional=True) + check_type(addresses, list, optional=True) + check_type(customAttributes, list, optional=True) + check_type(displayName, str, optional=True) + + post_data = dict_from_items_with_values( + request_parameters, + emails=emails, + contactType=contactType or DEFAULT_CONTACT_TYPE, + schemas=schemas or DEFAULT_SCHEMAS, + phoneNumbers=phoneNumbers, + extension=extension, + firstName=firstName, + lastName=lastName, + avatar=avatar, + department=department, + manager=manager, + managerId=managerId, + title=title, + addresses=addresses, + customAttributes=customAttributes, + displayName=displayName, + ) + + # API request + json_data = self._session.post( + self._base_endpoint(orgId), json=post_data, erc=201 + ) + + # Return a contact object created from the returned JSON object + return self._object_factory(OBJECT_TYPE, json_data) + + def update( + self, + orgId, + contactId, + emails=None, + schemas=None, + phoneNumbers=None, + extension=None, + firstName=None, + lastName=None, + avatar=None, + department=None, + manager=None, + managerId=None, + title=None, + addresses=None, + customAttributes=None, + displayName=None, + **request_parameters, + ): + """Update details for a contact. + + Args: + orgId(str): ID of the organization to which this contact belongs. + contactId(str): Unique ID for the contact. + emails(`list`): Email address(es) of the contact. + schemas(str): Contact schema identifier. + phoneNumbers(`list`): Phone numbers for the contact. + extension(str): Calling extension for the contact. + firstName(str): First name of the contact. + lastName(str): Last name of the contact. + avatar(str): URL to the contact avatar in PNG format. + department(str): The business department the contact belongs to. + manager(str): A manager identifier. + managerId(str): Person ID of the manager. + title(str): The contact's title. + addresses(`list`): Contact addresses. + customAttributes(`list`): Contact custom attributes. + displayName(str): Full name of the contact. + **request_parameters: Additional request parameters. + + Returns: + OrganizationContact: The updated organization contact. + + Raises: + TypeError: If the parameter types are incorrect. + ApiError: If the Webex cloud returns an error. + + """ + check_type(orgId, str) + check_type(contactId, str) + check_type(emails, list, optional=True) + check_type(schemas, str, optional=True) + check_type(phoneNumbers, list, optional=True) + check_type(extension, str, optional=True) + check_type(firstName, str, optional=True) + check_type(lastName, str, optional=True) + check_type(avatar, str, optional=True) + check_type(department, str, optional=True) + check_type(manager, str, optional=True) + check_type(managerId, str, optional=True) + check_type(title, str, optional=True) + check_type(addresses, list, optional=True) + check_type(customAttributes, list, optional=True) + check_type(displayName, str, optional=True) + + # Filter out read-only/unsupported fields (for example: contactType, + # schemas, created, lastModified) from user-provided kwargs. + filtered_request_parameters = { + key: value + for key, value in request_parameters.items() + if key in UPDATE_MUTABLE_FIELDS + } + + if not schemas: + schemas = DEFAULT_SCHEMAS + + put_data = dict_from_items_with_values( + filtered_request_parameters, + emails=emails, + schemas=schemas, + phoneNumbers=phoneNumbers, + extension=extension, + firstName=firstName, + lastName=lastName, + avatar=avatar, + department=department, + manager=manager, + managerId=managerId, + title=title, + addresses=addresses, + customAttributes=customAttributes, + displayName=displayName, + ) + + print("PUT data: {}".format(put_data)) + + # API request (PATCH) + response = self._session.request( + "PATCH", + self._base_endpoint(orgId) + "/" + contactId, + erc=200, + json=put_data, + ) + json_data = extract_and_parse_json(response) + + # Return a contact object created from the returned JSON object + return self._object_factory(OBJECT_TYPE, json_data) + + def delete(self, orgId, contactId): + """Delete a contact. + + Args: + orgId(str): The organization ID. + contactId(str): The ID of the contact to be deleted. + + Raises: + TypeError: If the parameter types are incorrect. + ApiError: If the Webex cloud returns an error. + + """ + check_type(orgId, str) + check_type(contactId, str) + + # API request + self._session.delete(self._base_endpoint(orgId) + "/" + contactId) diff --git a/src/webexpythonsdk/models/immutable.py b/src/webexpythonsdk/models/immutable.py index 909897d..41bfd85 100644 --- a/src/webexpythonsdk/models/immutable.py +++ b/src/webexpythonsdk/models/immutable.py @@ -44,6 +44,9 @@ from .mixins.membership import MembershipBasicPropertiesMixin from .mixins.message import MessageBasicPropertiesMixin from .mixins.organization import OrganizationBasicPropertiesMixin +from .mixins.organization_contact import ( + OrganizationContactBasicPropertiesMixin, +) from .mixins.person import PersonBasicPropertiesMixin from .mixins.role import RoleBasicPropertiesMixin from .mixins.room import RoomBasicPropertiesMixin @@ -231,6 +234,12 @@ class Organization(ImmutableData, OrganizationBasicPropertiesMixin): """Webex Organization data model.""" +class OrganizationContact( + ImmutableData, OrganizationContactBasicPropertiesMixin +): + """Webex Organization Contact data model.""" + + class Person(ImmutableData, PersonBasicPropertiesMixin): """Webex Person data model.""" @@ -306,6 +315,7 @@ class MeetingRegistrant(ImmutableData, MeetingRegistrantBasicPropertiesMixin): membership=Membership, message=Message, organization=Organization, + organization_contact=OrganizationContact, person=Person, role=Role, room=Room, diff --git a/src/webexpythonsdk/models/mixins/organization_contact.py b/src/webexpythonsdk/models/mixins/organization_contact.py new file mode 100644 index 0000000..c5a6a0b --- /dev/null +++ b/src/webexpythonsdk/models/mixins/organization_contact.py @@ -0,0 +1,131 @@ +"""Webex Organization Contact data model. + +Copyright (c) 2016-2024 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +from webexpythonsdk.utils import WebexDateTime + + +class OrganizationContactBasicPropertiesMixin(object): + """Organization contact basic properties.""" + + @property + def id(self): + """A unique identifier for the contact.""" + return self._json_data.get("id") + + @property + def emails(self): + """The email addresses of the contact.""" + return self._json_data.get("emails") + + @property + def contactType(self): + """The contact type.""" + return self._json_data.get("contactType") + + @property + def schemas(self): + """The schema identifier for the contact.""" + return self._json_data.get("schemas") + + @property + def phoneNumbers(self): + """Phone numbers for the contact.""" + return self._json_data.get("phoneNumbers") + + @property + def extension(self): + """The Webex Calling extension for the contact.""" + return self._json_data.get("extension") + + @property + def displayName(self): + """The full name of the contact.""" + return self._json_data.get("displayName") + + @property + def firstName(self): + """The first name of the contact.""" + return self._json_data.get("firstName") + + @property + def lastName(self): + """The last name of the contact.""" + return self._json_data.get("lastName") + + @property + def avatar(self): + """The URL to the contact's avatar in PNG format.""" + return self._json_data.get("avatar") + + @property + def orgId(self): + """The ID of the organization to which this contact belongs.""" + return self._json_data.get("orgId") + + @property + def department(self): + """The business department the contact belongs to.""" + return self._json_data.get("department") + + @property + def manager(self): + """A manager identifier.""" + return self._json_data.get("manager") + + @property + def managerId(self): + """Person ID of the manager.""" + return self._json_data.get("managerId") + + @property + def title(self): + """The contact's title.""" + return self._json_data.get("title") + + @property + def addresses(self): + """A contact's addresses.""" + return self._json_data.get("addresses") + + @property + def customAttributes(self): + """A contact's custom attributes.""" + return self._json_data.get("customAttributes") + + @property + def created(self): + """The date and time the contact was created.""" + created = self._json_data.get("created") + if created: + return WebexDateTime.strptime(created) + else: + return None + + @property + def lastModified(self): + """The date and time the contact was last changed.""" + last_modified = self._json_data.get("lastModified") + if last_modified: + return WebexDateTime.strptime(last_modified) + else: + return None diff --git a/tests/api/__init__.py b/tests/api/__init__.py index 69aeff4..f06d284 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -33,6 +33,7 @@ from webexpythonsdk.api.licenses import LicensesAPI from webexpythonsdk.api.memberships import MembershipsAPI from webexpythonsdk.api.messages import MessagesAPI +from webexpythonsdk.api.organization_contacts import OrganizationContactsAPI from webexpythonsdk.api.organizations import OrganizationsAPI from webexpythonsdk.api.people import PeopleAPI from webexpythonsdk.api.roles import RolesAPI @@ -178,6 +179,10 @@ def test_organizations_api_object_creation(api): assert isinstance(api.organizations, OrganizationsAPI) +def test_organization_contacts_api_object_creation(api): + assert isinstance(api.organization_contacts, OrganizationContactsAPI) + + def test_people_api_object_creation(api): assert isinstance(api.people, PeopleAPI) diff --git a/tests/api/test_organization_contacts.py b/tests/api/test_organization_contacts.py new file mode 100644 index 0000000..417dea6 --- /dev/null +++ b/tests/api/test_organization_contacts.py @@ -0,0 +1,247 @@ +"""WebexAPI Organization Contacts API fixtures and tests. + +Copyright (c) 2016-2024 Cisco and/or its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import itertools + +import pytest + +import webexpythonsdk + + +# Helper Functions +def get_contact_by_email(api, org_id, email): + list_of_contacts = list( + api.organization_contacts.search(org_id, email=email) + ) + if list_of_contacts: + assert len(list_of_contacts) == 1 + return list_of_contacts[0] + else: + return None + + +def update_contact(api, contact, **contact_attributes): + # Get a copy of the contact's current attributes + new_attributes = contact.json_data + + # Merge in attribute updates + for attribute, value in contact_attributes.items(): + new_attributes[attribute] = value + + return api.organization_contacts.update( + contact.orgId, contact.id, **new_attributes + ) + + +def delete_contact(api, contact): + """Delete a contact and swallow any API error.""" + try: + api.organization_contacts.delete(contact.orgId, contact.id) + except webexpythonsdk.ApiError: + pass + + +def is_valid_organization_contact(obj): + return ( + isinstance(obj, webexpythonsdk.OrganizationContact) + and obj.id is not None + ) + + +def are_valid_organization_contacts(iterable): + return all([is_valid_organization_contact(obj) for obj in iterable]) + + +# Fixtures +@pytest.fixture(scope="session") +def get_test_contact(api, get_new_email_address, me): + def inner_function(): + contact_email = get_new_email_address() + contact = get_contact_by_email(api, me.orgId, contact_email) + if contact: + return contact + else: + contact = api.organization_contacts.create( + me.orgId, + emails=[contact_email], + displayName="webexpythonsdk", + firstName="webexpythonsdk", + lastName="webexpythonsdk", + ) + return contact + + return inner_function + + +class OrganizationContactsManager(object): + """Creates, tracks and manages test contacts used by the tests.""" + + def __init__(self, api, get_test_contact): + super(OrganizationContactsManager, self).__init__() + self._api = api + self._get_new_test_contact = get_test_contact + self.test_contacts = {} + + def __getitem__(self, item): + if self.test_contacts.get(item): + return self.test_contacts[item] + else: + new_test_contact = self._get_new_test_contact() + self.test_contacts[item] = new_test_contact + return new_test_contact + + @property + def list(self): + return self.test_contacts.values() + + def len(self): + return len(self.list) + + def __iter__(self): + return iter(self.list) + + def __del__(self): + for contact in self.test_contacts.values(): + delete_contact(self._api, contact) + + +@pytest.fixture(scope="session") +def test_organization_contacts(api, get_test_contact): + test_contacts = OrganizationContactsManager(api, get_test_contact) + + yield test_contacts + + del test_contacts + + +@pytest.fixture() +def temp_contact(api, get_random_email_address, me): + # Get an e-mail address not currently used on Webex + contact_email = None + contact = True + while contact: + contact_email = get_random_email_address() + contact = get_contact_by_email(api, me.orgId, contact_email) + + # Create the contact + contact = api.organization_contacts.create( + me.orgId, + emails=[contact_email], + displayName="webexpythonsdk", + firstName="webexpythonsdk", + lastName="webexpythonsdk", + ) + + yield contact + + try: + api.organization_contacts.delete(me.orgId, contact.id) + except webexpythonsdk.ApiError: + pass + + +# Tests +def test_list_organization_contacts(api, me, test_organization_contacts): + # Ensure we have at least one contact + _ = test_organization_contacts["test_contact"] + + list_of_contacts = list(api.organization_contacts.list(me.orgId)) + assert len(list_of_contacts) >= 1 + assert are_valid_organization_contacts(list_of_contacts) + + +def test_search_organization_contacts_by_email( + api, me, test_organization_contacts +): + email = test_organization_contacts["not_a_member"].emails[0] + list_of_contacts = list( + api.organization_contacts.search(me.orgId, email=email) + ) + assert len(list_of_contacts) >= 1 + assert are_valid_organization_contacts(list_of_contacts) + + +def test_search_organization_contacts_by_display_name( + api, me, test_organization_contacts +): + display_name = test_organization_contacts["not_a_member"].displayName + list_of_contacts = list( + api.organization_contacts.search(me.orgId, displayName=display_name) + ) + assert len(list_of_contacts) >= 1 + assert are_valid_organization_contacts(list_of_contacts) + + +def test_search_organization_contacts_by_id( + api, me, test_organization_contacts +): + contact_id = test_organization_contacts["not_a_member"].id + list_of_contacts = list( + api.organization_contacts.search(me.orgId, id=contact_id) + ) + assert len(list_of_contacts) == 1 + assert are_valid_organization_contacts(list_of_contacts) + + +def test_search_organization_contacts_with_paging( + api, me, test_organization_contacts +): + page_size = 1 + pages = 3 + num_contacts = pages * page_size + + for i in range(num_contacts): + _ = test_organization_contacts[f"contact_{i}"] + + contacts = api.organization_contacts.search(me.orgId, max=page_size) + contacts_list = list(itertools.islice(contacts, num_contacts)) + + assert len(contacts_list) == num_contacts + assert are_valid_organization_contacts(contacts_list) + + +def test_create_organization_contact(test_organization_contacts): + contact = test_organization_contacts["not_a_member"] + assert is_valid_organization_contact(contact) + + +def test_get_organization_contact_details(api, me, test_organization_contacts): + contact_id = test_organization_contacts["not_a_member"].id + contact = api.organization_contacts.get(me.orgId, contact_id) + assert is_valid_organization_contact(contact) + + +def test_update_organization_contact(api, temp_contact): + update_attributes = { + "displayName": temp_contact.displayName + " Updated", + "firstName": temp_contact.firstName + " Updated", + "lastName": temp_contact.lastName + " Updated", + } + updated_contact = update_contact(api, temp_contact, **update_attributes) + assert is_valid_organization_contact(updated_contact) + for attribute, value in update_attributes.items(): + assert getattr(updated_contact, attribute) == value + + +def test_delete_organization_contact(api, temp_contact): + api.organization_contacts.delete(temp_contact.orgId, temp_contact.id) diff --git a/tests/conftest.py b/tests/conftest.py index 9aa9318..9580d88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,6 +44,7 @@ "tests.api.test_memberships", "tests.api.test_messages", "tests.api.test_organizations", + "tests.api.test_organization_contacts", "tests.api.test_people", "tests.api.test_roles", "tests.api.test_rooms", diff --git a/tests/test_webexpythonsdk.py b/tests/test_webexpythonsdk.py index 4dd4f81..926cecd 100644 --- a/tests/test_webexpythonsdk.py +++ b/tests/test_webexpythonsdk.py @@ -48,6 +48,7 @@ def test_package_contents(self): assert hasattr(webexpythonsdk, "Membership") assert hasattr(webexpythonsdk, "Message") assert hasattr(webexpythonsdk, "Organization") + assert hasattr(webexpythonsdk, "OrganizationContact") assert hasattr(webexpythonsdk, "Person") assert hasattr(webexpythonsdk, "Role") assert hasattr(webexpythonsdk, "Room")