├── .circleci └── config.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── calendly ├── __init__.py ├── calendly.py ├── tests │ ├── __init__.py │ ├── calendly_test.py │ ├── conftest.py │ └── fixtures.py └── utils │ ├── constants.py │ └── requests.py ├── requirements.txt └── setup.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/python:latest 6 | steps: 7 | - checkout # checkout source code to working directory 8 | - run: 9 | name: Install dependencies 10 | command: | 11 | python -m virtualenv venv 12 | . venv/bin/activate 13 | pip install --no-cache-dir -r requirements.txt 14 | - run: 15 | name: Running tests 16 | command: | 17 | . venv/bin/activate 18 | pytest 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | /dist 4 | *.egg-info 5 | /build 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt ./ 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . . 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/kevteg/calendly-python/tree/master.svg?style=svg)](https://circleci.com/gh/kevteg/calendly-python/tree/master) 2 | [![Downloads](https://pepy.tech/badge/calendly)](https://pepy.tech/project/calendly) 3 | # calendly-python 🐍 4 | 5 | Python package to use the [Calendly](http://calendly.com) API 🚀 6 | 7 | 8 | ## Installation 9 | 10 | pip install calendly 11 | 12 | ## Usage 13 | 14 | ### Set your Authentication token 15 | See [Calendly docs](https://developer.calendly.com/docs/getting-your-authentication-token) to get your auth token 16 | 17 | from calendly import Calendly 18 | calendly = Calendly(api_key) 19 | 20 | #### Test the auth token 21 | 22 | calendly.echo() 23 | 24 | #### Get list of events 25 | 26 | calendly.events() 27 | 28 | #### Webhooks 29 | ##### Create A Webhook Subscription 30 | 31 | calendly.create_webhook('https://your-webhook.com', events=['canceled', 'invited']) 32 | 33 | - **Note:** the `events` variable is a list 34 | - **Note:** possible values are: `canceled` and `invited` 35 | - **Note:** by default the `events` list contains the 2 possible values 36 | 37 | ##### Get Webhook Subscription 38 | 39 | calendly.get_webhook('webhook_id') 40 | 41 | ##### Get List of Webhook Subscriptions 42 | 43 | calendly.list_webhooks() 44 | 45 | ##### Delete Webhook Subscription 46 | 47 | calendly.remove_webhook('webhook_id') 48 | 49 | - **Note**: the response will be `{'success': True}` if the webhook was successfully removed, otherwise it will be `{'success': False, "type": "calendly type", "message": "reason it failed"}` 50 | 51 | #### User Event Types 52 | 53 | calendly.event_types() 54 | 55 | #### About Me 56 | 57 | calendly.about() 58 | 59 | #### Important 60 | - **Note:** All the responses are dictionaries with the calendly response, except for the remove webhook method that also contains the `success` key. Check their [docs](https://developer.calendly.com/docs/) to know the possible responses! 61 | 62 | ### TODOs: 63 | Next steps for this package: 64 | - [ ] Improve how the methods return the responses: proccess Calendly responses and make them objects, so that users can manage the information in an easier way 65 | - [ ] Support for version 2 <3 66 | - [ ] Creating reusable exceptions for error messages 67 | -------------------------------------------------------------------------------- /calendly/__init__.py: -------------------------------------------------------------------------------- 1 | """ CALENDLY """ 2 | from .calendly import Calendly 3 | __all__ = [Calendly] 4 | -------------------------------------------------------------------------------- /calendly/calendly.py: -------------------------------------------------------------------------------- 1 | from requests.auth import HTTPBasicAuth 2 | from json import JSONDecodeError 3 | from calendly.utils.constants import WEBHOOK, ME, ECHO, EVENTS 4 | from calendly.utils.requests import CaRequest 5 | 6 | 7 | class Calendly(object): 8 | 9 | event_types_def = { 10 | "canceled": "invitee.canceled", 11 | "created": "invitee.created" 12 | } 13 | 14 | def __init__(self, api_key): 15 | self.request = CaRequest(api_key) 16 | 17 | def create_webhook(self, user_url, event_types=["canceled", "created"]): 18 | events = [self.event_types_def[event_type] for event_type in event_types] 19 | data = {'url': user_url, 'events': events} 20 | response = self.request.post(WEBHOOK, data) 21 | return response.json() 22 | 23 | def list_webhooks(self): 24 | response = self.request.get(WEBHOOK) 25 | return response.json() 26 | 27 | def remove_webhook(self, id): 28 | dict_response = {'success': True} 29 | response = self.request.delete(f'{WEBHOOK}/{id}') 30 | dict_response['success'] = response.status_code == 200 31 | try: 32 | json_response = response.json() 33 | except JSONDecodeError: 34 | json_response = {} 35 | dict_response.update(json_response) 36 | return dict_response 37 | 38 | def get_webhook(self, id): 39 | response = self.request.get(f'{WEBHOOK}/{id}') 40 | return response.json() 41 | 42 | def about(self): 43 | response = self.request.get(ME) 44 | return response.json() 45 | 46 | def event_types(self): 47 | response = self.request.get(f'{ME}/event_types') 48 | return response.json() 49 | 50 | def echo(self): 51 | response = self.request.get(ECHO) 52 | return response.json() 53 | 54 | def events(self): 55 | response = self.request.get(EVENTS) 56 | return response.json() 57 | -------------------------------------------------------------------------------- /calendly/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevteg/calendly-python/54dd6d38de243926f48d3fb9af17b6e8bd929000/calendly/tests/__init__.py -------------------------------------------------------------------------------- /calendly/tests/calendly_test.py: -------------------------------------------------------------------------------- 1 | 2 | def test_about(fake_about, calendly_client): 3 | r = calendly_client.about() 4 | assert r['data']['type'] == 'users' 5 | 6 | 7 | def test_echo(fake_echo, calendly_client): 8 | r = calendly_client.echo() 9 | assert 'email' in r 10 | 11 | 12 | def test_event_types(fake_events_type, calendly_client): 13 | r = calendly_client.event_types() 14 | assert 'type' in r['data'][0] 15 | 16 | 17 | def test_get_webhook(fake_get_webhook, calendly_client): 18 | id = '1234' 19 | r = calendly_client.get_webhook(id) 20 | assert r['data'][0]['id'] == id 21 | 22 | 23 | def test_list_webhooks(fake_get_webhook, calendly_client): 24 | r = calendly_client.list_webhooks() 25 | assert r['data'] is not None 26 | 27 | 28 | def test_create_webhook(fake_create_webhook, calendly_client): 29 | r = calendly_client.create_webhook('https://test.com') 30 | assert r['id'] is not None 31 | 32 | 33 | def test_remove_webhook(fake_requests, calendly_client): 34 | fake_requests() 35 | r = calendly_client.remove_webhook('1234') 36 | assert r['success'] == True 37 | 38 | -------------------------------------------------------------------------------- /calendly/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from calendly.tests.fixtures import * 2 | -------------------------------------------------------------------------------- /calendly/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | from calendly import Calendly 4 | 5 | 6 | about_me_payload = { 7 | "data":{ 8 | "type":"users", 9 | "id":"XXXXXXXXXXXXXXXX", 10 | "attributes":{ 11 | "name":"Jane Doe", 12 | "slug":"janedoe", 13 | "email":"janedoe30305@gmail.com", 14 | "url":"https://calendly.com/janedoe", 15 | "timezone":"America/New_York", 16 | "avatar":{ 17 | "url":"https://d3v0px0pttie1i.cloudfront.net/uploads/user/avatar/68272/78fb9f5e.jpg" 18 | }, 19 | "created_at":"2015-06-16T18:46:52Z", 20 | "updated_at":"2016-08-23T19:40:07Z" 21 | } 22 | } 23 | } 24 | 25 | echo_payload = { 26 | 'email': 'test@example.com' 27 | } 28 | 29 | events_types_payload = { 30 | "data":[ 31 | { 32 | "type":"event_types", 33 | "id":"AAAAAAAAAAAAAAAA", 34 | "attributes":{ 35 | "name":"15 Minute Meeting", 36 | "description":"", 37 | "duration":30, 38 | "slug":"15min", 39 | "color":"#fff200", 40 | "active":True, 41 | "created_at":"2015-06-16T18:46:53Z", 42 | "updated_at":"2016-08-23T19:27:52Z", 43 | "url":"https://calendly.com/janedoe/15min" 44 | } 45 | }, 46 | { 47 | "type":"event_types", 48 | "id":"BBBBBBBBBBBBBBBB", 49 | "attributes":{ 50 | "name":"30 Minute Meeting", 51 | "description":"", 52 | "duration":30, 53 | "slug":"30min", 54 | "color":"#74daed", 55 | "active":True, 56 | "created_at":"2015-06-16T18:46:53Z", 57 | "updated_at":"2016-06-02T16:26:44Z", 58 | "url":"https://calendly.com/janedoe/30min" 59 | } 60 | }, 61 | { 62 | "type":"event_types", 63 | "id":"CCCCCCCCCCCCCCCC", 64 | "attributes":{ 65 | "name":"Sales call", 66 | "description":None, 67 | "duration":30, 68 | "slug":"sales-call", 69 | "color":"#c5c1ff", 70 | "active":False, 71 | "created_at":"2016-06-23T20:13:17Z", 72 | "updated_at":"2016-06-23T20:13:22Z", 73 | "url":"https://calendly.com/acme-team/sales-call" 74 | } 75 | } 76 | ] 77 | } 78 | 79 | webhook_id = { 80 | 'id': '1234' 81 | } 82 | 83 | 84 | def single_webhook_payload(id): 85 | return { 86 | "type":"hooks", 87 | "id":id, 88 | "attributes":{ 89 | "url":"http://foo.bar/1", 90 | "created_at":"2016-08-23T19:15:24Z", 91 | "state":"active", 92 | "events":[ 93 | "invitee.created", 94 | "invitee.canceled" 95 | ] 96 | } 97 | } 98 | 99 | 100 | def get_webhook_payload(ids=[1234]): 101 | response = { 102 | "data":[ 103 | single_webhook_payload(id) for id in ids 104 | ] 105 | } 106 | return response 107 | 108 | 109 | def get_response(payload): 110 | class Data: 111 | status_code = 200 112 | text = json.dumps(payload) 113 | 114 | def json(self): 115 | return payload 116 | return Data() 117 | 118 | 119 | @pytest.fixture 120 | def fake_requests(monkeypatch): 121 | def method_payload(payload={}): 122 | def fake_method(url, json, headers): 123 | return get_response(payload) 124 | 125 | monkeypatch.setattr('requests.get', fake_method) 126 | monkeypatch.setattr('requests.post', fake_method) 127 | monkeypatch.setattr('requests.delete', fake_method) 128 | return method_payload 129 | 130 | 131 | @pytest.fixture 132 | def fake_about(fake_requests): 133 | return fake_requests(about_me_payload) 134 | 135 | 136 | @pytest.fixture 137 | def fake_echo(fake_requests): 138 | return fake_requests(echo_payload) 139 | 140 | 141 | @pytest.fixture 142 | def fake_events_type(fake_requests): 143 | return fake_requests(events_types_payload) 144 | 145 | 146 | @pytest.fixture 147 | def fake_get_webhook(monkeypatch): 148 | def fake_get(url, json, headers): 149 | id = url.split('/')[-1] 150 | ids = ['1234', '12345'] if id == 'hooks' else [id] 151 | payload = get_webhook_payload(ids) 152 | return get_response(payload) 153 | monkeypatch.setattr('requests.get', fake_get) 154 | 155 | 156 | @pytest.fixture 157 | def fake_create_webhook(fake_requests): 158 | return fake_requests(webhook_id) 159 | 160 | 161 | @pytest.fixture 162 | def calendly_client(): 163 | api_key = '123abc' 164 | return Calendly(api_key) 165 | 166 | -------------------------------------------------------------------------------- /calendly/utils/constants.py: -------------------------------------------------------------------------------- 1 | BASE="https://calendly.com/api/v1" 2 | WEBHOOK=f"{BASE}/hooks" 3 | ME=f"{BASE}/users/me" 4 | ECHO=f"{BASE}/echo" 5 | EVENTS=f"{BASE}/events" 6 | -------------------------------------------------------------------------------- /calendly/utils/requests.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | 5 | class CaRequest(object): 6 | 7 | def __init__(self, token): 8 | self.headers = {'X-TOKEN': token} 9 | 10 | def process_request(self, method, url, data=None): 11 | request_method = getattr(requests, method) 12 | return request_method(url, json=data, headers=self.headers) 13 | 14 | def get(self, url, data=None): 15 | return self.process_request('get', url, data) 16 | 17 | def post(self, url, data=None): 18 | return self.process_request('post', url, data) 19 | 20 | def delete(self, url, data=None): 21 | return self.process_request('delete', url, data) 22 | 23 | def put(self, url, data=None): 24 | return self.process_request('put', url, data) 25 | 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | pytest==4.6.2 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys 3 | import os 4 | 5 | 6 | def readme(): 7 | with open('README.md') as f: 8 | return f.read() 9 | 10 | setup( 11 | name='calendly', 12 | version='1.1.1', 13 | description="A Python wrapper for the Calendly API (https://developer.calendly.com/docs/)", 14 | long_description=readme(), 15 | long_description_content_type="text/markdown", 16 | classifiers=[ 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 3.4", 19 | "Programming Language :: Python :: 3.5", 20 | "Programming Language :: Python :: 3.6", 21 | "Programming Language :: Python :: 3.7" 22 | ], 23 | keywords='Calendly python api', 24 | url='https://github.com/kevteg/calendly-python', 25 | author='kevteg', 26 | author_email='kevteg05@gmail.com', 27 | license='MIT', 28 | packages=[ 29 | 'calendly', 30 | 'calendly.utils', 31 | ], 32 | install_requires=[ 33 | 'requests' 34 | ], 35 | test_suite='nose.collector', 36 | tests_require=['nose'], 37 | include_package_data=True, 38 | zip_safe=False 39 | ) 40 | --------------------------------------------------------------------------------