From 4e8dbeae0a25be84b1c5707b89dbf14cb5765dc5 Mon Sep 17 00:00:00 2001 From: Yoav Tepper Date: Tue, 4 Jan 2022 16:27:59 +0200 Subject: [PATCH 1/4] Setup: Refactor naming --- README.md | 4 ++-- calendly/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6ef4294..4377d45 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ $ pip install PyCalendly See [Getting Started with Calendly API](https://developer.calendly.com/getting-started) and get a Personal Access token. ``` -from calendly import Calendly +from calendly import CalendlyAPI api_key = "" -calendly = Calendly(api_key) +calendly = CalendlyAPI(api_key) ``` ### Webhooks - `create_webhook` - Create new Webhook subscription diff --git a/calendly/__init__.py b/calendly/__init__.py index c07ccbc..74f0f61 100644 --- a/calendly/__init__.py +++ b/calendly/__init__.py @@ -1,3 +1,3 @@ -from .calendly import Calendly +from .calendly import CalendlyAPI -__all__ = [Calendly] \ No newline at end of file +__all__ = [CalendlyAPI] \ No newline at end of file From fb99698aaca70247bffadb06d4bfec705aff3217 Mon Sep 17 00:00:00 2001 From: Yoav Tepper Date: Tue, 4 Jan 2022 16:29:35 +0200 Subject: [PATCH 2/4] Typing: Declare type hints on all methods --- calendly/calendly.py | 85 +++++++++++++++++++------------------- calendly/utils/requests.py | 18 ++++---- requirements.txt | 1 + 3 files changed, 51 insertions(+), 53 deletions(-) diff --git a/calendly/calendly.py b/calendly/calendly.py index dae10a9..4cecb4b 100644 --- a/calendly/calendly.py +++ b/calendly/calendly.py @@ -1,28 +1,27 @@ -from json import JSONDecodeError -from calendly.utils.constants import WEBHOOK, USERS, ME, EVENT_TYPE, DATA_COMPLIANCE -from calendly.utils.constants import EVENTS, WEBHOOK_SUBSCRIPTIONS, SCHEDULING_LINKS -from calendly.utils.constants import ORGANIZATIONS, ORGANIZATION_MEMBERSHIPS - +from calendly.utils.constants import WEBHOOK, EVENTS, ME, EVENT_TYPE from calendly.utils.requests import CalendlyReq, CalendlyException +from typing import List, MutableMapping +import json - -class Calendly(object): +class CalendlyAPI(object): event_types_def = { "canceled": "invitee.canceled", "created": "invitee.created" } - def __init__(self, api_key): + def __init__(self, token: str): """ - Constructor + Constructor. Uses Bearer Token for Authentication. - Args: - api_key (str): Calendly Personal Access Token + Parameters + ---------- + token : str + Personal Access Token """ - self.request = CalendlyReq(api_key) + self.request = CalendlyReq(token) - def create_webhook(self, url, scope, organization, user=None, event_types=["canceled", "created"]): + def create_webhook(self, url: str, scope: str, organization: str, signing_key: str=None, user: str=None, event_types: List[str]=["canceled", "created"]) -> MutableMapping: """ Create a Webhook Subscription @@ -42,20 +41,20 @@ def create_webhook(self, url, scope, organization, user=None, event_types=["canc data = {'url': url, 'events': events, 'organization': organization, - 'scope': scope} + 'scope': scope, + 'signing_key': signing_key} - if(scope == 'user'): - if(user == None): + if (scope == 'user'): + if (user == None): raise CalendlyException data['user'] = user response = self.request.post(WEBHOOK, data) return response.json() - def list_webhooks(self, organization, scope, user=None, count=20, sort=None): + def list_webhooks(self, organization: str, scope: str, user: str=None, count: int=20, sort: str=None) -> List[MutableMapping]: """ Get a List of Webhook subscriptions for an Organization or User with a UUID. - Reference: https://calendly.stoplight.io/docs/api-docs/reference/calendly-api/openapi.yaml/paths/~1webhook_subscriptions/get @@ -78,18 +77,18 @@ def list_webhooks(self, organization, scope, user=None, count=20, sort=None): 'scope': scope, 'count': count} - if(sort != None): + if (sort != None): data['sort'] = sort - if(scope == 'user'): - if(user == None): + if (scope == 'user'): + if (user == None): raise CalendlyException data['user'] = user response = self.request.get(WEBHOOK, data) return response.json() - def delete_webhook(self, id): + def delete_webhook(self, id: str) -> MutableMapping: """ Delete a Webhook subscription for an Organization or User with a specified UUID. @@ -104,12 +103,12 @@ def delete_webhook(self, id): dict_response['success'] = response.status_code == 200 try: json_response = response.json() - except JSONDecodeError: + except json.JSONDecodeError: json_response = {} dict_response.update(json_response) return dict_response - def get_webhook(self, uuid): + def get_webhook(self, uuid: str) -> MutableMapping: """ Get a Webhook Subscription for an Organization or User with specified UUID. @@ -122,7 +121,7 @@ def get_webhook(self, uuid): response = self.request.get(f'{WEBHOOK}/{uuid}') return response.json() - def about(self): + def about(self) -> MutableMapping: """ Returns basic information about the user account. @@ -132,7 +131,7 @@ def about(self): response = self.request.get(ME) return response.json() - def event_types(self, count="20", organization=None, page_token=None, sort=None, user=None): + def list_event_types(self, count: int=20, organization: str=None, page_token: str=None, sort: str=None, user_uri: str=None) -> List[MutableMapping]: """ Returns all Event Types associated with a specified user. @@ -141,24 +140,24 @@ def event_types(self, count="20", organization=None, page_token=None, sort=None, organization (str, optional): View available personal, team and organization events type assosicated with the organization's URI. Defaults to None. page_token (str, optional): Toke to pass the next portion of the collection. Defaults to None. sort (str, optional): Order results by specified field and direction. Defaults to None. - user (str, optional): user's URI. Defaults to None. + user_uri (str, optional): user's URI. Defaults to None. Returns: dict: json decoded response with list of event types """ data = {"count": count} - if(organization): + if (organization): data['organization'] = organization - if(page_token): + if (page_token): data['page_token'] = page_token - if(sort): + if (sort): data['sort'] = sort - if(user): - data['user'] = user + if (user_uri): + data['user'] = user_uri response = self.request.get(EVENT_TYPE, data) return response.json() - def get_event_type(self, uuid): + def get_event_type(self, uuid: str) -> MutableMapping: """Returns event type associated with the specified UUID Args: @@ -171,7 +170,7 @@ def get_event_type(self, uuid): response = self.request.get(f'{EVENT_TYPE}/' + uuid, data) return response.json() - def list_events(self, count="20", organization=None, sort=None, user=None, status=None): + def list_events(self, count: int=20, organization: str=None, sort: str=None, user_uri: str=None, status: str=None) -> List[MutableMapping]: """ Returns a List of Events @@ -179,25 +178,25 @@ def list_events(self, count="20", organization=None, sort=None, user=None, statu count (str, optional): Number of rows to return. Defaults to "20". organization (str, optional): Organization URI. Defaults to None. sort (str, optional): comma seperated list of {field}:{direction} values. Defaults to None. - user (str, optional): User URI. Defaults to None. + user_uri (str, optional): User URI. Defaults to None. status (str, optional): 'active' or 'canceled'. Defaults to None. Returns: dict: json decoded response of list of events. """ data = {'count': count} - if(organization): + if (organization): data['organization'] = organization - if(sort): + if (sort): data['sort'] = sort - if(user): - data['user'] = user - if(status): + if (user_uri): + data['user'] = user_uri + if (status): data['status'] = status response = self.request.get(EVENTS, data) return response.json() - def get_event_invitee(self, event_uuid, invitee_uuid): + def get_event_invitee(self, event_uuid: str, invitee_uuid: str) -> MutableMapping: """ Returns information about an invitee associated with a URI @@ -212,7 +211,7 @@ def get_event_invitee(self, event_uuid, invitee_uuid): response = self.request.get(url) return response.json() - def get_event_details(self, uuid): + def get_event_details(self, uuid: str) -> MutableMapping: """ Get information about an Event associated with a URI. @@ -226,7 +225,7 @@ def get_event_details(self, uuid): response = self.request.get(url) return response.json() - def list_event_invitees(self, uuid): + def list_event_invitees(self, uuid: str) -> List[MutableMapping]: """ Returns a list of Invitees for an Event. diff --git a/calendly/utils/requests.py b/calendly/utils/requests.py index d7ea2a9..e468819 100644 --- a/calendly/utils/requests.py +++ b/calendly/utils/requests.py @@ -1,14 +1,12 @@ import requests -import json +from typing import List, MutableMapping __author__ = "laxmena " __license__ = "MIT" - class CalendlyException(Exception): """Errors corresponding to a misuse of Calendly API""" - class CalendlyReq(object): """ Private class wrapping the Calendly API v2. Decodes responses from Calendly and returns it @@ -18,7 +16,7 @@ class CalendlyReq(object): https://calendly.stoplight.io/docs/api-docs/ """ - def __init__(self, token): + def __init__(self, token: str): """ Constructor. Uses Bearer Token Authentication. @@ -29,7 +27,7 @@ def __init__(self, token): """ self.headers = {'authorization': 'Bearer ' + token} - def process_request(self, method, url, data=None): + def process_request(self, method: str, url: str, data: MutableMapping=None) -> requests.Response: """ Make requests to Calendly API by appending requried headers. @@ -45,7 +43,7 @@ def process_request(self, method, url, data=None): request_method = getattr(requests, method) return request_method(url, json=data, headers=self.headers) - def get(self, url, data=None): + def get(self, url: str, data: MutableMapping=None) -> requests.Response: """ Send GET request to the Calendly URL. @@ -58,7 +56,7 @@ def get(self, url, data=None): """ return self.process_request('get', url, data) - def post(self, url, data=None): + def post(self, url: str, data: MutableMapping=None) -> requests.Response: """ Send POST request to the Calendly URL. @@ -71,7 +69,7 @@ def post(self, url, data=None): """ return self.process_request('post', url, data) - def delete(self, url, data=None): + def delete(self, url: str, data: MutableMapping=None) -> requests.Response: """ Send DELETE request to the Calendly URL. @@ -84,7 +82,7 @@ def delete(self, url, data=None): """ return self.process_request('delete', url, data) - def put(self, url, data=None): + def put(self, url: str, data: MutableMapping=None) -> requests.Response: """ Send PUT request to the Calendly URL. @@ -95,4 +93,4 @@ def put(self, url, data=None): data : dict, optional additional data to be passed to the API """ - return self.process_request('put', url, data) + return self.process_request('put', url, data) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d4d4b98..ffe8742 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests==2.26.0 pytest==6.2.4 +typing==3.7.4.3 \ No newline at end of file From 17b27c194edba5c88f27c4eac6c37a0d54d62f23 Mon Sep 17 00:00:00 2001 From: Yoav Tepper Date: Tue, 4 Jan 2022 16:33:24 +0200 Subject: [PATCH 3/4] Client: Expand functionality + unit testing --- calendly/calendly.py | 70 ++++++ .../tests/get_event_details_response.json | 33 +++ .../tests/list_event_details_response.json | 88 +++++++ calendly/tests/list_event_types_response.json | 88 +++++++ calendly/tests/list_events_response.json | 39 ++++ calendly/tests/run_tests.py | 214 ++++++++++++++++++ 6 files changed, 532 insertions(+) create mode 100644 calendly/tests/get_event_details_response.json create mode 100644 calendly/tests/list_event_details_response.json create mode 100644 calendly/tests/list_event_types_response.json create mode 100644 calendly/tests/list_events_response.json create mode 100644 calendly/tests/run_tests.py diff --git a/calendly/calendly.py b/calendly/calendly.py index 4cecb4b..b267c68 100644 --- a/calendly/calendly.py +++ b/calendly/calendly.py @@ -238,3 +238,73 @@ def list_event_invitees(self, uuid: str) -> List[MutableMapping]: url = f'{EVENTS}/' + uuid + '/invitees' response = self.request.get(url) return response.json() + + def get_all_event_types(self, user_uri: str) -> List[str]: + """ + Get all event types by recursively crawling on all result pages. + + Args: + user_uri (str, optional): User URI. + + Returns: + list: json event type objects + """ + first = self.list_event_types(user_uri=user_uri, count=100) + next_page = first['pagination']['next_page'] + + data = first['collection'] + + while (next_page): + page = self.request.get(next_page).json() + data += page['collection'] + next_page = page['pagination']['next_page'] + + return data + + def get_all_scheduled_events(self, user_uri: str) -> List[str]: + """ + Get all scheduled events by recursively crawling on all result pages. + + Args: + user_uri (str, optional): User URI. + + Returns: + list: json scheduled event objects + """ + first = self.list_events(user_uri=user_uri, count=100) + next_page = first['pagination']['next_page'] + + data = first['collection'] + + while (next_page): + page = self.request.get(next_page).json() + data += page['collection'] + next_page = page['pagination']['next_page'] + + return data + + def convert_event_to_original_url(self, event_uri: str, user_uri: str) -> str: + """ + Convert event url from calendly's inner API convention to the original public url of the event. + + Args: + event_uri (str): Event URI. + user_uri (str, optional): User URI. + + Returns: + string: the public convention of the event's url + """ + event_type_uri = self.get_event_details(event_uri)['resource']['event_type'] + page = self.list_event_types(user_uri=user_uri) + + while True: + filtered_result = next(filter(lambda event: event['uri'] == event_type_uri, page['collection']), None) + if filtered_result: + return filtered_result['scheduling_url'] + + next_page = page['pagination']['next_page'] + if not next_page: + break + page = self.request.get(next_page).json() + return + \ No newline at end of file diff --git a/calendly/tests/get_event_details_response.json b/calendly/tests/get_event_details_response.json new file mode 100644 index 0000000..6d8e02e --- /dev/null +++ b/calendly/tests/get_event_details_response.json @@ -0,0 +1,33 @@ +{ + "resource": { + "uri": "https://api.calendly.com/scheduled_events/MOCK_URI", + "name": "15 Minute Meeting", + "status": "active", + "start_time": "2019-08-24T14:15:22Z", + "end_time": "2019-08-24T14:15:22Z", + "event_type": "https://api.calendly.com/event_types/MOCK_URI", + "location": { + "type": "physical", + "location": "Calendly Office" + }, + "invitees_counter": { + "total": 0, + "active": 0, + "limit": 0 + }, + "created_at": "2019-01-02T03:04:05.678Z", + "updated_at": "2019-01-02T03:04:05.678Z", + "event_memberships": [ + { + "user": "https://api.calendly.com/users/MOCK_URI" + } + ], + "event_guests": [ + { + "email": "user@example.com", + "created_at": "2019-08-24T14:15:22Z", + "updated_at": "2019-08-24T14:15:22Z" + } + ] + } + } \ No newline at end of file diff --git a/calendly/tests/list_event_details_response.json b/calendly/tests/list_event_details_response.json new file mode 100644 index 0000000..293f66b --- /dev/null +++ b/calendly/tests/list_event_details_response.json @@ -0,0 +1,88 @@ +{ + "collection": [ + { + "uri": "https://api.calendly.com/event_types/MOCK_URI", + "name": "15 Minute Meeting", + "active": true, + "slug": "acmesales", + "scheduling_url": "https://calendly.com/acmesales", + "duration": 30, + "kind": "solo", + "pooling_type": "round_robin", + "type": "StandardEventType", + "color": "#fff200", + "created_at": "2019-01-02T03:04:05.678Z", + "updated_at": "2019-08-07T06:05:04.321Z", + "internal_note": "Internal note", + "description_plain": "15 Minute Meeting", + "description_html": "

15 Minute Meeting

", + "profile": { + "type": "User", + "name": "Tamara Jones", + "owner": "https://api.calendly.com/users/AAAAAAAAAAAAAAAA" + }, + "secret": true, + "custom_questions": [ + { + "name": "Company Name", + "type": "string", + "position": 0, + "enabled": true, + "required": true, + "answer_choices": [], + "include_other": false + }, + { + "name": "What would you like to discuss?", + "type": "text", + "position": 0, + "enabled": true, + "required": true, + "answer_choices": [], + "include_other": false + }, + { + "name": "Number of employees", + "answer_choices": [ + "1", + "2-10", + "11-20", + "20+" + ], + "enabled": true, + "include_other": true, + "position": 2, + "required": false, + "type": "single_select" + }, + { + "name": "Multi-Select Question", + "answer_choices": [ + "Answer 1", + "Answer 2", + "Answer 3", + "Answer 4" + ], + "enabled": true, + "include_other": true, + "position": 2, + "required": false, + "type": "multi_select" + }, + { + "name": "Phone Number", + "type": "phone_number", + "position": 0, + "enabled": true, + "required": true, + "answer_choices": [], + "include_other": false + } + ] + } + ], + "pagination": { + "count": 1, + "next_page": "https://api.calendly.com/event_types?count=1&page_token=MOCK_PAGE_TOKEN" + } + } \ No newline at end of file diff --git a/calendly/tests/list_event_types_response.json b/calendly/tests/list_event_types_response.json new file mode 100644 index 0000000..293f66b --- /dev/null +++ b/calendly/tests/list_event_types_response.json @@ -0,0 +1,88 @@ +{ + "collection": [ + { + "uri": "https://api.calendly.com/event_types/MOCK_URI", + "name": "15 Minute Meeting", + "active": true, + "slug": "acmesales", + "scheduling_url": "https://calendly.com/acmesales", + "duration": 30, + "kind": "solo", + "pooling_type": "round_robin", + "type": "StandardEventType", + "color": "#fff200", + "created_at": "2019-01-02T03:04:05.678Z", + "updated_at": "2019-08-07T06:05:04.321Z", + "internal_note": "Internal note", + "description_plain": "15 Minute Meeting", + "description_html": "

15 Minute Meeting

", + "profile": { + "type": "User", + "name": "Tamara Jones", + "owner": "https://api.calendly.com/users/AAAAAAAAAAAAAAAA" + }, + "secret": true, + "custom_questions": [ + { + "name": "Company Name", + "type": "string", + "position": 0, + "enabled": true, + "required": true, + "answer_choices": [], + "include_other": false + }, + { + "name": "What would you like to discuss?", + "type": "text", + "position": 0, + "enabled": true, + "required": true, + "answer_choices": [], + "include_other": false + }, + { + "name": "Number of employees", + "answer_choices": [ + "1", + "2-10", + "11-20", + "20+" + ], + "enabled": true, + "include_other": true, + "position": 2, + "required": false, + "type": "single_select" + }, + { + "name": "Multi-Select Question", + "answer_choices": [ + "Answer 1", + "Answer 2", + "Answer 3", + "Answer 4" + ], + "enabled": true, + "include_other": true, + "position": 2, + "required": false, + "type": "multi_select" + }, + { + "name": "Phone Number", + "type": "phone_number", + "position": 0, + "enabled": true, + "required": true, + "answer_choices": [], + "include_other": false + } + ] + } + ], + "pagination": { + "count": 1, + "next_page": "https://api.calendly.com/event_types?count=1&page_token=MOCK_PAGE_TOKEN" + } + } \ No newline at end of file diff --git a/calendly/tests/list_events_response.json b/calendly/tests/list_events_response.json new file mode 100644 index 0000000..aa8b701 --- /dev/null +++ b/calendly/tests/list_events_response.json @@ -0,0 +1,39 @@ +{ + "collection": [ + { + "uri": "https://api.calendly.com/scheduled_events/MOCK_URI", + "name": "15 Minute Meeting", + "status": "active", + "start_time": "2019-08-24T14:15:22Z", + "end_time": "2019-08-24T14:15:22Z", + "event_type": "https://api.calendly.com/event_types/MOCK_URI", + "location": { + "type": "physical", + "location": "Calendly Office" + }, + "invitees_counter": { + "total": 0, + "active": 0, + "limit": 0 + }, + "created_at": "2019-01-02T03:04:05.678Z", + "updated_at": "2019-01-02T03:04:05.678Z", + "event_memberships": [ + { + "user": "https://api.calendly.com/users/MOCK_URI" + } + ], + "event_guests": [ + { + "email": "user@example.com", + "created_at": "2019-08-24T14:15:22Z", + "updated_at": "2019-08-24T14:15:22Z" + } + ] + } + ], + "pagination": { + "count": 1, + "next_page": "https://api.calendly.com/scheduled_events?count=1&page_token=MOCK_PAGE_TOKEN" + } + } \ No newline at end of file diff --git a/calendly/tests/run_tests.py b/calendly/tests/run_tests.py new file mode 100644 index 0000000..730b0d3 --- /dev/null +++ b/calendly/tests/run_tests.py @@ -0,0 +1,214 @@ +from calendly.calendly import CalendlyReq, CalendlyAPI +from calendly.utils import constants +from unittest.mock import MagicMock +import unittest +import json +import copy + +# Init test objects +mock_token = 'mock_token' +calendly_client = CalendlyAPI(mock_token) +calendly_request = CalendlyReq(mock_token) +calendly_client.request = calendly_request + +# Set HTTP mock response class +class MockResponse(object): + def __init__(self, content, status_code, headers=None): + if isinstance(content, bytes): + self.content = content + else: + self.content = content.encode('utf-8') + self.status_code = status_code + self.headers = headers or {} + + @property + def text(self): + return self.content.decode('utf-8') + + def json(self): + return json.loads(self.content) + +# Test endpoints +class TestEndpoints(unittest.TestCase): + def test_create_webhook(self): + # Arrange + calendly_request.post = MagicMock(return_value=MockResponse('{}', 200)) + + mock_url = 'mock_url' + mock_scope = 'mock_scope' + mock_organization = 'mock_organization' + mock_signing_key = 'mock_signing_key' + + expected_payload = { + 'url': mock_url, + 'organization': mock_organization, + 'scope': mock_scope, + 'signing_key': mock_signing_key, + 'events': ['invitee.canceled', 'invitee.created'] + } + + # Act + calendly_client.create_webhook(url=mock_url, scope=mock_scope, organization=mock_organization, signing_key=mock_signing_key) + + # Assert + calendly_request.post.assert_called_once() + calendly_request.post.assert_called_with(f'{constants.WEBHOOK}', expected_payload) + + def test_delete_webhook(self): + # Arrange + calendly_request.get = MagicMock(return_value=MockResponse('{}', 204)) + + mock_uuid = 'mock_uuid' + + # Act + calendly_client.get_webhook(mock_uuid) + + # Assert + calendly_request.get.assert_called_once() + calendly_request.get.assert_called_with(f'{constants.WEBHOOK}/{mock_uuid}') + + def test_get_event_details(self): + # Arrange + with open('./calendly/tests/get_event_details_response.json', 'r') as file: + calendly_request.get = MagicMock(return_value=MockResponse(file.read(), 200)) + + mock_uuid = 'mock_uuid' + + # Act + response = calendly_client.get_event_details(mock_uuid) + + # Assert + calendly_request.get.assert_called_once() + calendly_request.get.assert_called_with(f'{constants.EVENTS}/{mock_uuid}') + self.assertEqual(response['resource']['uri'], 'https://api.calendly.com/scheduled_events/MOCK_URI') + + def test_list_event_types(self): + # Arrange + with open('./calendly/tests/list_event_types_response.json', 'r') as file: + calendly_request.get = MagicMock(return_value=MockResponse(file.read(), 200)) + + mock_uuid = 'mock_uuid' + expected_payload = { + 'count': 20, + 'user': 'mock_uuid' + } + + # Act + response = calendly_client.list_event_types(user_uri=mock_uuid) + + # Assert + calendly_request.get.assert_called_once() + calendly_request.get.assert_called_with(f'{constants.EVENT_TYPE}', expected_payload) + self.assertEqual(response['collection'][0]['uri'], 'https://api.calendly.com/event_types/MOCK_URI') + + def test_list_events(self): + # Arrange + with open('./calendly/tests/list_events_response.json', 'r') as file: + calendly_request.get = MagicMock(return_value=MockResponse(file.read(), 200)) + + mock_uuid = 'mock_uuid' + expected_payload = { + 'count': 20, + 'user': 'mock_uuid' + } + + # Act + response = calendly_client.list_events(user_uri=mock_uuid) + + # Assert + calendly_request.get.assert_called_once() + calendly_request.get.assert_called_with(f'{constants.EVENTS}', expected_payload) + self.assertEqual(response['collection'][0]['uri'], 'https://api.calendly.com/scheduled_events/MOCK_URI') + +# Test endpoints +class TestLogicalFunctions(unittest.TestCase): + def test_get_all_items_with_pagination(self): + # Arrange + first_uri = 'https://api.calendly.com/event_types/A' + second_uri = 'https://api.calendly.com/event_types/B' + next_page_uri = 'https://api.calendly.com/uri_to_next_page' + + first_page = '{"collection": [{"uri": "'+first_uri+'"}],"pagination": {"next_page": "'+next_page_uri+'"}}' + second_page = '{"collection": [{"uri": "'+second_uri+'"}],"pagination": {"next_page": null}}' + + calendly_client.list_event_types = MagicMock(return_value=json.loads(first_page)) + calendly_client.list_events = MagicMock(return_value=json.loads(first_page)) + + # Return next page only for the correct "next_page" uri + def handle_get_request(uri): + if uri == next_page_uri: + return MockResponse(second_page, 200) + return MockResponse('Not Found', 400) + + calendly_request.get = MagicMock(side_effect=handle_get_request) + + # Act (testing both get_all_event_types & get_all_scheduled_events) + event_types = calendly_client.get_all_event_types('mock_user_uri') + scheduled_events = calendly_client.get_all_scheduled_events('mock_user_uri') + + # Assert + self.assertEqual(len(event_types), 2) + self.assertEqual(event_types[0]['uri'], first_uri) + self.assertEqual(event_types[1]['uri'], second_uri) + + self.assertEqual(len(scheduled_events), 2) + self.assertEqual(scheduled_events[0]['uri'], first_uri) + self.assertEqual(scheduled_events[1]['uri'], second_uri) + + def test_convert_event_to_original_url_match_on_first_page(self): + # Arrange + with open('./calendly/tests/get_event_details_response.json', 'r') as file: + calendly_client.get_event_details = MagicMock(return_value=json.loads(file.read())) + + with open('./calendly/tests/list_event_types_response.json', 'r') as file: + calendly_client.list_event_types = MagicMock(return_value=json.loads(file.read())) + + mock_event_uri = 'mock_event_uri' + mock_user_uri = 'mock_user_uri' + + # Act + original_url = calendly_client.convert_event_to_original_url(mock_event_uri, mock_user_uri) + + # Assert + self.assertEqual(original_url, 'https://calendly.com/acmesales') + + def test_convert_event_to_original_url_paging_required(self): + # Arrange + mock_event_uri = 'mock_event_uri' + mock_user_uri = 'mock_user_uri' + next_page_uri = 'https://api.calendly.com/uri_to_next_page' + + with open('./calendly/tests/get_event_details_response.json', 'r') as file: + content = file.read() + calendly_client.get_event_details = MagicMock(side_effect=lambda x: json.loads(content) if x == mock_event_uri else None) + + with open('./calendly/tests/list_event_types_response.json', 'r') as file: + # Result won't be found in first page + mock_first_page = json.loads(file.read()) + mock_first_page['collection'][0]['uri'] = 'NOT_THE_URI_WE_WERE_LOOKING_FOR' + mock_first_page['pagination']['next_page'] = next_page_uri + + # Result will be found only on second page + mock_second_page = copy.deepcopy(mock_first_page) + mock_second_page['collection'][0]['uri'] = 'https://api.calendly.com/event_types/MOCK_URI' + mock_second_page['pagination']['next_page'] = 'null' + + calendly_client.list_event_types = MagicMock(return_value=mock_first_page) + + # Return next page only for the correct "next_page" uri + def handle_get_request(uri): + if uri == next_page_uri: + return MockResponse(json.dumps(mock_second_page), 200) + return MockResponse('Not Found', 400) + + calendly_request.get = MagicMock(side_effect=handle_get_request) + + + # Act + original_url = calendly_client.convert_event_to_original_url(mock_event_uri, mock_user_uri) + + # Assert + self.assertEqual(original_url, 'https://calendly.com/acmesales') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From d64a21d517d7bff68ccd813f754a181f9a6e4031 Mon Sep 17 00:00:00 2001 From: Yoav Tepper Date: Tue, 4 Jan 2022 16:33:56 +0200 Subject: [PATCH 4/4] Workflows: Add repo test pipeline --- .github/workflows/test.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0e031b1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python application + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest ./calendly/tests/run_tests.py \ No newline at end of file