Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<Personal Access Token>"
calendly = Calendly(api_key)
calendly = CalendlyAPI(api_key)
```
### Webhooks
- `create_webhook` - Create new Webhook subscription
Expand Down
4 changes: 2 additions & 2 deletions calendly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .calendly import Calendly
from .calendly import CalendlyAPI

__all__ = [Calendly]
__all__ = [CalendlyAPI]
155 changes: 112 additions & 43 deletions calendly/calendly.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -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:
Expand All @@ -171,33 +170,33 @@ 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

Args:
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

Expand All @@ -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.

Expand All @@ -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.

Expand All @@ -239,3 +238,73 @@ def list_event_invitees(self, uuid):
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

33 changes: 33 additions & 0 deletions calendly/tests/get_event_details_response.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
Loading