├── .bumpversion.cfg ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev_requirements.txt ├── dialpad ├── __init__.py ├── client.py └── resources │ ├── __init__.py │ ├── app_settings.py │ ├── blocked_number.py │ ├── call.py │ ├── call_router.py │ ├── callback.py │ ├── callcenter.py │ ├── company.py │ ├── contact.py │ ├── department.py │ ├── event_subscription.py │ ├── number.py │ ├── office.py │ ├── resource.py │ ├── room.py │ ├── sms.py │ ├── stats.py │ ├── subscription.py │ ├── transcript.py │ ├── user.py │ ├── userdevice.py │ └── webhook.py ├── requirements.txt ├── setup.py ├── test ├── __init__.py ├── test_resource_sanity.py └── utils.py ├── tools └── create_release.sh └── tox.ini /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.2.3 3 | commit = False 4 | tag = False 5 | 6 | [bumpversion:file:setup.py] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | .venv/ 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | test/.resources/ 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Dialpad, Inc. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Dialpad API Client 2 | 3 | A python wrapper around the Dialpad REST API 4 | 5 | This document describes the installation, usage, and development practices of this python library. 6 | For information about the API itself, head on over to our 7 | [API Documentation](https://developers.dialpad.com/reference) page! 8 | 9 | 10 | ## Installation 11 | 12 | Just use everyone's favourite python package installer: `pip` 13 | 14 | ```bash 15 | pip install python-dialpad 16 | ``` 17 | 18 | ## Usage 19 | 20 | ### The Short Version 21 | 22 | TL;DR, this library provides a `DialpadClient` class, which can be instantiated with an API token 23 | and a dialpad URL. 24 | 25 | Once a `DialpadClient` object has been constructed, it can be used to call our API endpoints: 26 | 27 | ```python 28 | from dialpad import DialpadClient 29 | 30 | dp_client = DialpadClient(sandbox=True, token='API_TOKEN_HERE') 31 | 32 | print(dp_client.user.get(user_id='1234567')) 33 | ``` 34 | 35 | ### Client Constructor Arguments 36 | 37 | - `token (required)` The API token that will be used to authenticate API requests. 38 | - `sandbox (optional)` If the `sandbox` argument is set to `True`, then API calls will be 39 | routed to `https://sandbox.dialpad.com`. 40 | - `base_url (optional)` Routes requests to a specific url. 41 | 42 | 43 | ### API Resources 44 | 45 | In general, each resource that we support in our public API will be exposed as properties of the 46 | client object. For example, the `User` resource can be accessed using the `user` property (as 47 | demonstrated above). 48 | 49 | Each of these resource properties will expose related HTTP methods as methods of that resource 50 | property. 51 | 52 | For example, `GET /api/v2/users/{id}` translates to `dp_client.user.get('the_user_id')`. 53 | 54 | 55 | ### API Responses 56 | 57 | In cases where our API responds with a single JSON object, the client method will return a Python 58 | dict (as demonstrated above) 59 | 60 | In cases where our API responds with a paginated list of many JSON objects, the client method will 61 | return an iterator which will lazily request the next page as the iterator is iterated upon. 62 | 63 | ```python 64 | from dialpad import DialpadClient 65 | 66 | dp_client = DialpadClient(sandbox=True, token='API_TOKEN_HERE') 67 | 68 | for user in dp_client.user.list(): 69 | print(user) 70 | ``` 71 | 72 | 73 | ## Development 74 | 75 | ### Testing 76 | 77 | That's right, the testing section is first in line! Before you start diving in, let's just make sure your environment is set up properly, and that the tests are running buttery-smooth. 78 | 79 | Assuming you've already cloned the repository, all you'll need to do is install `tox`, and run the command against the appropriate environment. 80 | 81 | * Install the `tox` package. 82 | ```shell 83 | $ pip install tox 84 | ``` 85 | 86 | * Run the tests 87 | ```shell 88 | $ tox 89 | ``` 90 | Optionaly, you can specify an environment to run the tests against. For eg: 91 | ```shell 92 | $ tox -e py3 93 | ``` 94 | That was easy :) 95 | 96 | Neato! 97 | 98 | ### Adding New Resources 99 | 100 | Most of the changes to this library will probably just be adding support for additional resources 101 | and endpoints that we expose in the API, so let's start with how to add a new resource. 102 | 103 | Each resource exposed by this library should have its own python file under the `dialpad/resources` 104 | directory, and should define a single `class` that inherits from `DialpadResource`. 105 | 106 | The class itself should set the `_resource_path` class property to a list of strings such 107 | that `'/api/v2/' + '/'.join(_resource_path)` corresponds to the API path for that resource. 108 | 109 | Once the `_resource_path` is defined, the resource class can define instance methods to expose 110 | functionality related to the resource that it represents, and can use the `self.request` helper 111 | method to make authenticated requests to API paths under the `_resource_path`. For example, 112 | if `_resource_path` is set to `['users']`, then calling `self.request(method='POST')` would make 113 | a `POST` request to `/api/v2/users`. (A more precise description of the `request` method is given 114 | in the following section) 115 | 116 | With that in mind, most methods that the developer chooses to add to a resource class will probably 117 | just be a very thin method that passes the appropriate arguments into `self.request`, and returns 118 | the result. 119 | 120 | 121 | #### The `request` Helper Method 122 | 123 | `self.request` is a helper method that handles the details of authentication, response parsing, and 124 | pagination, such that the caller only needs to specify the API path, HTTP method, and request data. 125 | The method arguments are as follows: 126 | 127 | - `path (optional)` Any additional path elements that should be added after the `_resource_path` 128 | - `method (optional, default: 'GET')` The HTTP method 129 | - `data (optional)` A python dict defining either the query params or the JSON payload, depending on 130 | which HTTP method is specified 131 | - `headers (optional)` Any additional headers that should be included in the request (the API key 132 | is automatically included) 133 | 134 | If the request succeeds, then `self.request` will either return a python dict, or an iterator of 135 | python dicts, depending on whether the server responds with a pagenated response. Pagenated 136 | responses will be detected automatically, so the caller does not need to worry about it. 137 | 138 | If the request fails, then a `requests.HTTPError` exception will be raised, and it'll be up to the 139 | consumer of this library to deal with it 😎 140 | 141 | 142 | #### The `resources/__init__.py` File 143 | 144 | When a new file is added to the `resources` directory, a new import statement should also be added 145 | to `__init__.py` to expose the newly defined resource class as a direct property of the `resources` 146 | module. 147 | 148 | 149 | #### `DialpadClient` Resource Properties 150 | 151 | In addition to adding the new class to the `__init__.py` file, the new resource class should also 152 | be added as a cached property of the `DialpadClient` class. 153 | 154 | 155 | #### Recap 156 | 157 | To add a new resource to this client library, simply: 158 | - Create a new file under the `resources` directory 159 | - Define a new subclass of `DialpadResource` within said file 160 | - Expose methods related to that resource as methods on your new class 161 | - Add a new import statement in `resources/__init__.py` 162 | - Add a new property to the `DialpadClient` class 163 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | coverage == 5.5 2 | pytest 3 | pipenv == 2021.5.29 4 | pytest-sugar == 0.9.4 5 | pyrsistent == 0.16.1 6 | pytest-cov 7 | bump2version 8 | swagger-spec-validator == 2.7.3 9 | swagger-stub == 0.2.1 10 | jsonschema<4.0 11 | wheel 12 | cython<3.0.0 13 | pyyaml==5.4.1 14 | py 15 | git+https://github.com/jakedialpad/swagger-parser@v1.0.1b#egg=swagger-parser 16 | -------------------------------------------------------------------------------- /dialpad/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import DialpadClient 2 | -------------------------------------------------------------------------------- /dialpad/client.py: -------------------------------------------------------------------------------- 1 | 2 | import requests 3 | 4 | from cached_property import cached_property 5 | 6 | from .resources import ( 7 | AppSettingsResource, 8 | SMSResource, 9 | RoomResource, 10 | UserResource, 11 | CallResource, 12 | NumberResource, 13 | OfficeResource, 14 | WebhookResource, 15 | CompanyResource, 16 | ContactResource, 17 | CallbackResource, 18 | CallCenterResource, 19 | CallRouterResource, 20 | DepartmentResource, 21 | TranscriptResource, 22 | UserDeviceResource, 23 | StatsExportResource, 24 | SubscriptionResource, 25 | BlockedNumberResource, 26 | EventSubscriptionResource 27 | ) 28 | 29 | 30 | hosts = dict( 31 | live='https://dialpad.com', 32 | sandbox='https://sandbox.dialpad.com' 33 | ) 34 | 35 | 36 | class DialpadClient(object): 37 | def __init__(self, token, sandbox=False, base_url=None, company_id=None): 38 | self._token = token 39 | self._session = requests.Session() 40 | self._base_url = base_url or hosts.get('sandbox' if sandbox else 'live') 41 | self._company_id = company_id 42 | 43 | @property 44 | def company_id(self): 45 | return self._company_id 46 | 47 | @company_id.setter 48 | def company_id(self, value): 49 | self._company_id = value 50 | 51 | @company_id.deleter 52 | def company_id(self): 53 | del self._company_id 54 | 55 | def _url(self, *path): 56 | path = ['%s' % p for p in path] 57 | return '/'.join([self._base_url, 'api', 'v2'] + path) 58 | 59 | def _cursor_iterator(self, response_json, path, method, data, headers): 60 | for i in response_json['items']: 61 | yield i 62 | 63 | data = dict(data or {}) 64 | 65 | while 'cursor' in response_json: 66 | data['cursor'] = response_json['cursor'] 67 | response = self._raw_request(path, method, data, headers) 68 | response.raise_for_status() 69 | response_json = response.json() 70 | for i in response_json.get('items', []): 71 | yield i 72 | 73 | def _raw_request(self, path, method='GET', data=None, headers=None): 74 | url = self._url(*path) 75 | headers = headers or dict() 76 | if self.company_id: 77 | headers.update({'DP-Company-ID': str(self.company_id)}) 78 | 79 | headers.update({'Authorization': 'Bearer %s' % self._token}) 80 | if str(method).upper() in ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']: 81 | return getattr(self._session, str(method).lower())( 82 | url, 83 | headers=headers, 84 | json=data if method != 'GET' else None, 85 | params=data if method == 'GET' else None, 86 | ) 87 | raise ValueError('Unsupported method "%s"' % method) 88 | 89 | def request(self, path, method='GET', data=None, headers=None): 90 | response = self._raw_request(path, method, data, headers) 91 | response.raise_for_status() 92 | 93 | if response.status_code == 204: # No Content 94 | return None 95 | 96 | response_json = response.json() 97 | response_keys = set(k for k in response_json) 98 | # If the response contains the 'items' key, (and maybe 'cursor'), then this is a cursorized 99 | # list response. 100 | if 'items' in response_keys and not response_keys - {'cursor', 'items'}: 101 | return self._cursor_iterator( 102 | response_json, path=path, method=method, data=data, headers=headers) 103 | return response_json 104 | 105 | @cached_property 106 | def app_settings(self): 107 | return AppSettingsResource(self) 108 | 109 | @cached_property 110 | def blocked_number(self): 111 | return BlockedNumberResource(self) 112 | 113 | @cached_property 114 | def call(self): 115 | return CallResource(self) 116 | 117 | @cached_property 118 | def call_router(self): 119 | return CallRouterResource(self) 120 | 121 | @cached_property 122 | def callback(self): 123 | return CallbackResource(self) 124 | 125 | @cached_property 126 | def callcenter(self): 127 | return CallCenterResource(self) 128 | 129 | @cached_property 130 | def company(self): 131 | return CompanyResource(self) 132 | 133 | @cached_property 134 | def contact(self): 135 | return ContactResource(self) 136 | 137 | @cached_property 138 | def department(self): 139 | return DepartmentResource(self) 140 | 141 | @cached_property 142 | def event_subscription(self): 143 | return EventSubscriptionResource(self) 144 | 145 | @cached_property 146 | def number(self): 147 | return NumberResource(self) 148 | 149 | @cached_property 150 | def office(self): 151 | return OfficeResource(self) 152 | 153 | @cached_property 154 | def room(self): 155 | return RoomResource(self) 156 | 157 | @cached_property 158 | def sms(self): 159 | return SMSResource(self) 160 | 161 | @cached_property 162 | def stats(self): 163 | return StatsExportResource(self) 164 | 165 | @cached_property 166 | def subscription(self): 167 | return SubscriptionResource(self) 168 | 169 | @cached_property 170 | def transcript(self): 171 | return TranscriptResource(self) 172 | 173 | @cached_property 174 | def user(self): 175 | return UserResource(self) 176 | 177 | @cached_property 178 | def userdevice(self): 179 | return UserDeviceResource(self) 180 | 181 | @cached_property 182 | def webhook(self): 183 | return WebhookResource(self) 184 | -------------------------------------------------------------------------------- /dialpad/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .app_settings import AppSettingsResource 2 | from .blocked_number import BlockedNumberResource 3 | from .call import CallResource 4 | from .call_router import CallRouterResource 5 | from .callback import CallbackResource 6 | from .callcenter import CallCenterResource 7 | from .company import CompanyResource 8 | from .contact import ContactResource 9 | from .department import DepartmentResource 10 | from .event_subscription import EventSubscriptionResource 11 | from .number import NumberResource 12 | from .office import OfficeResource 13 | from .room import RoomResource 14 | from .sms import SMSResource 15 | from .stats import StatsExportResource 16 | from .subscription import SubscriptionResource 17 | from .transcript import TranscriptResource 18 | from .user import UserResource 19 | from .userdevice import UserDeviceResource 20 | from .webhook import WebhookResource 21 | -------------------------------------------------------------------------------- /dialpad/resources/app_settings.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class AppSettingsResource(DialpadResource): 4 | """AppSettingsResource implements python bindings for the Dialpad API's app-settings 5 | endpoints. 6 | 7 | See https://developers.dialpad.com/reference/appsettingsapi_getappsettings for additional 8 | documentation. 9 | """ 10 | _resource_path = ['app', 'settings'] 11 | 12 | def get(self, target_id=None, target_type=None): 13 | """Gets the app settings of the oauth app that is associated with the API key. 14 | 15 | If a target is specified, it will fetch the settings for that target, 16 | otherwise it will fetch the company-level settings. 17 | 18 | Args: 19 | target_id(int, optional): The target's id. 20 | target_type(str, optional): The target's type. 21 | 22 | See Also: 23 | https://developers.dialpad.com/reference/appsettingsapi_getappsettings 24 | """ 25 | return self.request(method='GET', data=dict(target_id=target_id, target_type=target_type)) 26 | -------------------------------------------------------------------------------- /dialpad/resources/blocked_number.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class BlockedNumberResource(DialpadResource): 4 | """BlockedNumberResource implements python bindings for the Dialpad API's blocked-number 5 | endpoints. 6 | 7 | See https://developers.dialpad.com/reference#blockednumbers for additional documentation. 8 | """ 9 | _resource_path = ['blockednumbers'] 10 | 11 | def list(self, limit=25, **kwargs): 12 | """List all numbers that have been flagged as "blocked" via the API. 13 | 14 | Args: 15 | limit (int, optional): The number of numbers to fetch per request. 16 | 17 | See Also: 18 | https://developers.dialpad.com/reference#blockednumberapi_listnumbers 19 | """ 20 | return self.request(method='GET', data=dict(limit=limit, **kwargs)) 21 | 22 | def block_numbers(self, numbers): 23 | """Blocks inbound calls from the specified numbers. 24 | 25 | Args: 26 | numbers (list, required): A list of e164-formatted numbers to block. 27 | 28 | See Also: 29 | https://developers.dialpad.com/reference#blockednumberapi_addnumbers 30 | """ 31 | return self.request(['add'], method='POST', data={'numbers': numbers}) 32 | 33 | def unblock_numbers(self, numbers): 34 | """Unblocks inbound calls from the specified numbers. 35 | 36 | Args: 37 | numbers (list, required): A list of e164-formatted numbers to unblock. 38 | 39 | See Also: 40 | https://developers.dialpad.com/reference#blockednumberapi_removenumbers 41 | """ 42 | return self.request(['remove'], method='POST', data={'numbers': numbers}) 43 | 44 | def get(self, number): 45 | """Gets a number object, provided it has been blocked by the API. 46 | 47 | Note: 48 | This API call will 404 if the number is not blocked, and return {"number": } if the 49 | number is blocked. 50 | 51 | Args: 52 | number (str, required): An e164-formatted number. 53 | 54 | See Also: 55 | https://developers.dialpad.com/reference#blockednumberapi_getnumber 56 | """ 57 | return self.request([number], method='GET') 58 | -------------------------------------------------------------------------------- /dialpad/resources/call.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class CallResource(DialpadResource): 4 | """CallResource implements python bindings for the Dialpad API's call endpoints. 5 | 6 | See https://developers.dialpad.com/reference#call for additional documentation. 7 | """ 8 | _resource_path = ['call'] 9 | 10 | def initiate_call(self, phone_number, user_id, **kwargs): 11 | """Initiates an oubound call to the specified phone number on behalf of the specified user. 12 | 13 | Note: 14 | This API will initiate the call by ringing the user's devices as well as ringing the specified 15 | number. When the user answers on their device, they will be connected with the call that is 16 | ringing the specified number. 17 | 18 | Optionally, group_type and group_id can be specified to cause the call to be routed through 19 | the specified group. This would be equivelant to the User initiating the call by selecting the 20 | specified group in the "New Call As" dropdown in the native app, or calling a contact that 21 | belongs to that group via the native app. 22 | 23 | In particular, the call will show up in that group's section of the app, and the external 24 | party will receive a call from the primary number of the specified group. 25 | 26 | Additionally, a specific device_id can be specified to cause that specific user-device to 27 | ring, rather than all of the user's devices. 28 | 29 | Args: 30 | phone_number (str, required): The e164-formatted number that should be called. 31 | user_id (int, required): The ID of the user that should be taking the call. 32 | group_id (int, optional): The ID of the call center, department, or office that should be used 33 | to initiate the call. 34 | group_type (str, optional): One of "office", "department", or "callcenter", corresponding to 35 | the type of ID passed into group_type. 36 | device_id (str, optional): The ID of the specific user device that should ring. 37 | custom_data (str, optional): Free-form extra data to associate with the call. 38 | 39 | See Also: 40 | https://developers.dialpad.com/reference#callapi_call 41 | """ 42 | return self.request(method='POST', data=dict(phone_number=phone_number, user_id=user_id, 43 | **kwargs)) 44 | 45 | def get_info(self, call_id): 46 | """Gets call status and other information. 47 | 48 | Args: 49 | call_id (int, required): The ID of the call. 50 | 51 | See Also: 52 | https://developers.dialpad.com/reference/callapi_getcallinfo 53 | """ 54 | return self.request([call_id], method='GET') 55 | -------------------------------------------------------------------------------- /dialpad/resources/call_router.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class CallRouterResource(DialpadResource): 4 | """CallRouterResource implements python bindings for the Dialpad API's call router endpoints. 5 | 6 | See https://developers.dialpad.com/reference#callrouters for additional documentation. 7 | """ 8 | _resource_path = ['callrouters'] 9 | 10 | def list(self, office_id, **kwargs): 11 | """Initiates an oubound call to the specified phone number on behalf of the specified user. 12 | 13 | Args: 14 | office_id (int, required): The ID of the office to which the routers belong. 15 | limit (int, optional): The number of routers to fetch per request. 16 | 17 | See Also: 18 | https://developers.dialpad.com/reference#callrouterapi_listcallrouters 19 | """ 20 | return self.request(method='GET', data=dict(office_id=office_id, **kwargs)) 21 | 22 | def create(self, name, default_target_id, default_target_type, office_id, routing_url, **kwargs): 23 | """Creates a new API-based call router. 24 | 25 | Args: 26 | name (str, required): human-readable display name for the router. 27 | default_target_id (int, required): The ID of the target that should be used as a fallback 28 | destination for calls if the call router is disabled. 29 | default_target_type (str, required): The entity type of the default target. 30 | office_id (int, required): The ID of the office to which the router should belong. 31 | routing_url (str, required): The URL that should be used to drive call routing decisions. 32 | secret (str, optional): The call router's signature secret. This is a plain text string that 33 | you should generate with a minimum length of 32 characters. 34 | enabled (bool, optional): If set to False, the call router will skip the routing url and 35 | instead forward calls straight to the default target. 36 | 37 | See Also: 38 | https://developers.dialpad.com/reference#callrouterapi_createcallrouter 39 | """ 40 | return self.request(method='POST', data=dict( 41 | name=name, 42 | default_target_id=default_target_id, 43 | default_target_type=default_target_type, 44 | office_id=office_id, 45 | routing_url=routing_url, 46 | **kwargs) 47 | ) 48 | 49 | def delete(self, router_id): 50 | """Deletes the API call router with the given ID. 51 | 52 | Args: 53 | router_id (str, required): The ID of the router to delete. 54 | 55 | See Also: 56 | https://developers.dialpad.com/reference#callrouterapi_deletecallrouter 57 | """ 58 | return self.request([router_id], method='DELETE') 59 | 60 | def get(self, router_id): 61 | """Fetches the API call router with the given ID. 62 | 63 | Args: 64 | router_id (str, required): The ID of the router to fetch. 65 | 66 | See Also: 67 | https://developers.dialpad.com/reference#callrouterapi_getcallrouter 68 | """ 69 | return self.request([router_id], method='GET') 70 | 71 | def patch(self, router_id, **kwargs): 72 | """Updates the API call router with the given ID. 73 | 74 | Args: 75 | router_id (str, required): The ID of the router to update. 76 | name (str, required): human-readable display name for the router. 77 | default_target_id (int, required): The ID of the target that should be used as a fallback 78 | destination for calls if the call router is disabled. 79 | default_target_type (str, required): The entity type of the default target. 80 | office_id (int, required): The ID of the office to which the router should belong. 81 | routing_url (str, required): The URL that should be used to drive call routing decisions. 82 | secret (str, optional): The call router's signature secret. This is a plain text string that 83 | you should generate with a minimum length of 32 characters. 84 | enabled (bool, optional): If set to False, the call router will skip the routing url and 85 | instead forward calls straight to the default target 86 | reset_error_count (bool, optional): Sets the auto-disablement routing error count back to 87 | zero. (See API docs for more details) 88 | 89 | See Also: 90 | https://developers.dialpad.com/reference#callrouterapi_updatecallrouter 91 | """ 92 | return self.request([router_id], method='PATCH', data=kwargs) 93 | 94 | def assign_number(self, router_id, **kwargs): 95 | """Assigns a number to the call router. 96 | 97 | Args: 98 | router_id (str, required): The ID of the router to assign the number. 99 | area_code (str, optional): An area code to attempt to use if a reserved pool number is not 100 | provided. If no area code is provided, the office's area code will 101 | be used. 102 | number (str, optional): A phone number from the reserved pool to attempt to assign. 103 | 104 | See Also: 105 | https://developers.dialpad.com/reference#numberapi_assignnumbertocallrouter 106 | """ 107 | return self.request([router_id, 'assign_number'], method='POST', data=kwargs) 108 | 109 | -------------------------------------------------------------------------------- /dialpad/resources/callback.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class CallbackResource(DialpadResource): 4 | """CallbackResource implements python bindings for the Dialpad API's callback endpoints. 5 | 6 | See https://developers.dialpad.com/reference#callback for additional documentation. 7 | """ 8 | _resource_path = ['callback'] 9 | 10 | def enqueue_callback(self, call_center_id, phone_number): 11 | """Requests a call-back for the specified number by adding it to the callback queue for the 12 | specified call center. 13 | 14 | The call back is added to the queue for the call center like a regular call, and a call is 15 | initiated when the next operator becomes available. This API respects all existing call center 16 | settings, e.g. business / holiday hours and queue settings. This API currently does not allow 17 | international call backs. Duplicate call backs for a given number and call center are not 18 | allowed. 19 | 20 | Args: 21 | call_center_id (str, required): The ID of the call center for which the callback should be 22 | enqueued. 23 | phone_number (str, required): The e164-formatted number that should be added to the callback 24 | queue. 25 | 26 | See Also: 27 | https://developers.dialpad.com/reference#callapi_callback 28 | """ 29 | return self.request(method='POST', data=dict(call_center_id=call_center_id, 30 | phone_number=phone_number)) 31 | 32 | def validate_callback(self, call_center_id, phone_number): 33 | """Performs a dry-run of creating a callback request, but does not add it to the call center 34 | queue. 35 | 36 | This performs the same validation logic as when actually enqueuing a callback request, allowing 37 | early identification of problems which would prevent a successful callback request. 38 | 39 | Args: 40 | call_center_id (str, required): The ID of the call center for which the callback would be 41 | enqueued. 42 | phone_number (str, required): The e164-formatted number that would be added to the callback 43 | queue. 44 | 45 | See Also: 46 | https://developers.dialpad.com/reference/callapi_validatecallback 47 | """ 48 | return self.request(['validate'], method='POST', data=dict(call_center_id=call_center_id, 49 | phone_number=phone_number)) 50 | -------------------------------------------------------------------------------- /dialpad/resources/callcenter.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class CallCenterResource(DialpadResource): 4 | """CallCenterResource implements python bindings for the Dialpad API's call center endpoints. 5 | See https://developers.dialpad.com/reference#callcenters for additional documentation. 6 | """ 7 | 8 | _resource_path = ['callcenters'] 9 | 10 | def get(self, call_center_id): 11 | """Gets a call center by ID. 12 | 13 | Args: 14 | call_center_id (int, required): The ID of the call center to retrieve. 15 | 16 | See Also: 17 | https://developers.dialpad.com/reference#callcenterapi_getcallcenter 18 | """ 19 | return self.request([call_center_id], method='GET') 20 | 21 | def get_operators(self, call_center_id): 22 | """Gets the list of users who are operators for the specified call center. 23 | 24 | Args: 25 | call_center_id (int, required): The ID of the call center. 26 | 27 | See Also: 28 | https://developers.dialpad.com/reference#callcenterapi_listoperators 29 | """ 30 | return self.request([call_center_id, 'operators'], method='GET') 31 | 32 | def add_operator(self, call_center_id, user_id, **kwargs): 33 | """Adds the specified user as an operator of the specified call center. 34 | 35 | Args: 36 | call_center_id (int, required): The ID of the call center. 37 | user_id (int, required): The ID of the user to add as an operator. 38 | skill_level (int, optional): Skill level of the operator. Integer value in range 1 - 100. 39 | Default 100 40 | role (str, optional): The role of the new operator ('operator', 'supervisor', or 'admin'). 41 | Default 'operator' 42 | license_type (str, optional): The type of license to assign to the new operator if a license 43 | is required ('agents', or 'lite_support_agents'). 44 | Default 'agents' 45 | keep_paid_numbers (bool, optional): If the operator is currently on a license that provides 46 | paid numbers and `license_type` is set to 47 | `lite_support_agents`, this option will determine if the 48 | operator keeps those numbers. Set to False for the 49 | numbers to be removed. 50 | Default True 51 | 52 | See Also: 53 | https://developers.dialpad.com/reference#callcenterapi_addoperator 54 | """ 55 | kwargs['user_id'] = user_id 56 | return self.request([call_center_id, 'operators'], method='POST', data=kwargs) 57 | 58 | def remove_operator(self, call_center_id, user_id): 59 | """Removes the specified user from the specified call center. 60 | 61 | Args: 62 | call_center_id (int, required): The ID of the call center. 63 | user_id (int, required): The ID of the user to remove. 64 | 65 | See Also: 66 | https://developers.dialpad.com/reference#callcenterapi_removeoperator 67 | """ 68 | return self.request([call_center_id, 'operators'], method='DELETE', data={'user_id': user_id}) 69 | -------------------------------------------------------------------------------- /dialpad/resources/company.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class CompanyResource(DialpadResource): 4 | """CompanyResource implements python bindings for the Dialpad API's company endpoints. 5 | 6 | See https://developers.dialpad.com/reference#company for additional documentation. 7 | """ 8 | _resource_path = ['company'] 9 | 10 | def get(self): 11 | """Gets the company resource. 12 | 13 | See Also: 14 | https://developers.dialpad.com/reference#companyapi_getcompany 15 | """ 16 | return self.request(method='GET') 17 | -------------------------------------------------------------------------------- /dialpad/resources/contact.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class ContactResource(DialpadResource): 4 | """ContactResource implements python bindings for the Dialpad API's contact endpoints. 5 | 6 | See https://developers.dialpad.com/reference#contacts for additional documentation. 7 | """ 8 | _resource_path = ['contacts'] 9 | 10 | def list(self, limit=25, **kwargs): 11 | """Lists contacts in the company. 12 | 13 | Args: 14 | limit (int, optional): The number of contacts to fetch per request. 15 | owner_id (int, optional): A specific user who's contacts should be listed. 16 | 17 | See Also: 18 | https://developers.dialpad.com/reference#contactapi_listcontacts 19 | """ 20 | return self.request(method='GET', data=dict(limit=limit, **kwargs)) 21 | 22 | def create(self, first_name, last_name, **kwargs): 23 | """Creates a new contact. 24 | 25 | Args: 26 | first_name (str, required): The contact's first name. 27 | last_name (str, required): The contact's family name. 28 | company_name (str, optional): The name of the contact's company. 29 | emails (list, optional): A list of email addresses associated with the contact. 30 | extension (str, optional): The contact's extension number. 31 | job_title (str, optional): The contact's job title. 32 | owner_id (str, optional): The ID of the user who should own this contact. If no owner_id is 33 | specified, then a company-level shared contact will be created. 34 | phones (list, optional): A list of e164 numbers that belong to this contact. 35 | trunk_group (str, optional): The contact's trunk group. 36 | urls (list, optional): A list of urls that pertain to this contact. 37 | 38 | See Also: 39 | https://developers.dialpad.com/reference#contactapi_createcontact 40 | """ 41 | return self.request(method='POST', data=dict(first_name=first_name, last_name=last_name, 42 | **kwargs)) 43 | 44 | def create_with_uid(self, first_name, last_name, uid, **kwargs): 45 | """Creates a new contact with a prescribed unique identifier. 46 | 47 | Args: 48 | first_name (str, required): The contact's first name. 49 | last_name (str, required): The contact's family name. 50 | uid (str, required): A unique identifier that should be included in the contact's ID. 51 | company_name (str, optional): The name of the contact's company. 52 | emails (list, optional): A list of email addresses associated with the contact. 53 | extension (str, optional): The contact's extension number. 54 | job_title (str, optional): The contact's job title. 55 | phones (list, optional): A list of e164 numbers that belong to this contact. 56 | trunk_group (str, optional): The contact's trunk group. 57 | urls (list, optional): A list of urls that pertain to this contact. 58 | 59 | See Also: 60 | https://developers.dialpad.com/reference#contactapi_createcontactwithuid 61 | """ 62 | return self.request(method='PUT', data=dict(first_name=first_name, last_name=last_name, uid=uid, 63 | **kwargs)) 64 | 65 | def delete(self, contact_id): 66 | """Deletes the specified contact. 67 | 68 | Args: 69 | contact_id (str, required): The ID of the contact to delete. 70 | 71 | See Also: 72 | https://developers.dialpad.com/reference#contactapi_deletecontact 73 | """ 74 | return self.request([contact_id], method='DELETE') 75 | 76 | def get(self, contact_id): 77 | """Gets a contact by ID. 78 | 79 | Args: 80 | contact_id (str, required): The ID of the contact. 81 | 82 | See Also: 83 | https://developers.dialpad.com/reference#contactapi_getcontact 84 | """ 85 | return self.request([contact_id], method='GET') 86 | 87 | def patch(self, contact_id, **kwargs): 88 | """Updates an existing contact. 89 | 90 | Args: 91 | contact_id (str, required): The ID of the contact. 92 | first_name (str, optional): The contact's first name. 93 | last_name (str, optional): The contact's family name. 94 | company_name (str, optional): The name of the contact's company. 95 | emails (list, optional): A list of email addresses associated with the contact. 96 | extension (str, optional): The contact's extension number. 97 | job_title (str, optional): The contact's job title. 98 | phones (list, optional): A list of e164 numbers that belong to this contact. 99 | trunk_group (str, optional): The contact's trunk group. 100 | urls (list, optional): A list of urls that pertain to this contact. 101 | 102 | See Also: 103 | https://developers.dialpad.com/reference#contactapi_updatecontact 104 | """ 105 | return self.request([contact_id], method='PATCH', data=kwargs) 106 | -------------------------------------------------------------------------------- /dialpad/resources/department.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class DepartmentResource(DialpadResource): 4 | """DepartmentResource implements python bindings for the Dialpad API's department endpoints. 5 | See https://developers.dialpad.com/reference#departments for additional documentation. 6 | """ 7 | 8 | _resource_path = ['departments'] 9 | 10 | def get(self, department_id): 11 | """Gets a department by ID. 12 | 13 | Args: 14 | department_id (int, required): The ID of the department to retrieve. 15 | 16 | See Also: 17 | https://developers.dialpad.com/reference#departmentapi_getdepartment 18 | """ 19 | return self.request([department_id], method='GET') 20 | 21 | def get_operators(self, department_id): 22 | """Gets the list of users who are operators for the specified department. 23 | 24 | Args: 25 | department_id (int, required): The ID of the department. 26 | 27 | See Also: 28 | https://developers.dialpad.com/reference#departmentapi_listoperators 29 | """ 30 | return self.request([department_id, 'operators'], method='GET') 31 | 32 | def add_operator(self, department_id, operator_id, operator_type, role='operator'): 33 | """Adds the specified user as an operator of the specified department. 34 | 35 | Args: 36 | department_id (int, required): The ID of the department. 37 | operator_id (int, required): The ID of the operator to add. 38 | operator_type (str, required): Type of the operator to add ('user' or 'room'). 39 | role (str, optional): The role of the new operator ('operator' or 'admin'). 40 | Default 'operator' 41 | 42 | See Also: 43 | https://developers.dialpad.com/reference#departmentapi_addoperator 44 | """ 45 | return self.request([department_id, 'operators'], method='POST', data={ 46 | 'operator_id': operator_id, 47 | 'operator_type': operator_type, 48 | 'role': role, 49 | }) 50 | 51 | def remove_operator(self, department_id, operator_id, operator_type): 52 | """Removes the specified user from the specified department. 53 | 54 | Args: 55 | department_id (int, required): The ID of the department. 56 | operator_id (int, required): The ID of the operator to remove. 57 | operator_type (str, required): Type of the operator to remove ('user' or 'room'). 58 | 59 | See Also: 60 | https://developers.dialpad.com/reference#departmentapi_removeoperator 61 | """ 62 | return self.request([department_id, 'operators'], method='DELETE', data={ 63 | 'operator_id': operator_id, 64 | 'operator_type': operator_type, 65 | }) 66 | -------------------------------------------------------------------------------- /dialpad/resources/event_subscription.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class EventSubscriptionResource(DialpadResource): 4 | """EventSubscriptionResource implements python bindings for the Dialpad API's event subscription 5 | endpoints. 6 | 7 | See https://developers.dialpad.com/reference#event for additional documentation. 8 | """ 9 | _resource_path = ['event-subscriptions'] 10 | 11 | def list_call_event_subscriptions(self, limit=25, **kwargs): 12 | """Lists call event subscriptions. 13 | 14 | Args: 15 | limit (int, optional): The number of subscriptions to fetch per request 16 | target_id (str, optional): The ID of a specific target to use as a filter 17 | target_type (str, optional): The type of the target (one of "department", "office", 18 | "callcenter", "user", "room", "staffgroup", "callrouter", 19 | "channel", "coachinggroup", or "unknown") 20 | 21 | See Also: 22 | https://developers.dialpad.com/reference#calleventsubscriptionapi_listcalleventsubscriptions 23 | """ 24 | return self.request(['call'], method='GET', data=dict(limit=limit, **kwargs)) 25 | 26 | def get_call_event_subscription(self, subscription_id): 27 | """Gets a specific call event subscription. 28 | 29 | Args: 30 | subscription_id (str, required): The ID of the subscription 31 | 32 | See Also: 33 | https://developers.dialpad.com/reference#calleventsubscriptionapi_getcalleventsubscription 34 | """ 35 | return self.request(['call', subscription_id], method='GET') 36 | 37 | def put_call_event_subscription(self, subscription_id, url, enabled=True, group_calls_only=False, 38 | **kwargs): 39 | """Update or create a call event subscription. 40 | 41 | The subscription_id is required. If the ID exists, then this call will update the subscription 42 | resource. If the ID does not exist, then it will create a new subscription with that ID. 43 | 44 | Args: 45 | subscription_id (str, required): The ID of the subscription 46 | url (str, required): The URL which should be called when the subscription fires 47 | secret (str, optional): A secret to use to encrypt subscription event payloads 48 | enabled (bool, optional): Whether or not the subscription should actually fire 49 | group_calls_only (bool, optional): Whether to limit the subscription to only fire if the call 50 | is a group call 51 | target_id (str, optional): The ID of a specific target to use as a filter 52 | target_type (str, optional): The type of the target (one of "department", "office", 53 | "callcenter", "user", "room", "staffgroup", "callrouter", 54 | "channel", "coachinggroup", or "unknown") 55 | call_states (list, optional): The specific types of call events that should trigger the 56 | subscription (any of "preanswer", "calling", "ringing", 57 | "connected", "merged", "hold", "queued", "voicemail", 58 | "eavesdrop", "monitor", "barge", "hangup", "blocked", 59 | "admin", "parked", "takeover", "all", "postcall", 60 | "transcription", or "recording") 61 | 62 | See Also: 63 | https://developers.dialpad.com/reference#calleventsubscriptionapi_createorupdatecalleventsubscription 64 | """ 65 | 66 | return self.request(['call', subscription_id], method='PUT', 67 | data=dict(url=url, enabled=enabled, group_calls_only=group_calls_only, 68 | **kwargs)) 69 | 70 | def delete_call_event_subscription(self, subscription_id): 71 | """Deletes a specific call event subscription. 72 | 73 | Args: 74 | subscription_id (str, required): The ID of the subscription 75 | 76 | See Also: 77 | https://developers.dialpad.com/reference#calleventsubscriptionapi_deletecalleventsubscription 78 | """ 79 | return self.request(['call', subscription_id], method='DELETE') 80 | 81 | 82 | def list_sms_event_subscriptions(self, limit=25, **kwargs): 83 | """Lists sms event subscriptions. 84 | 85 | Args: 86 | limit (int, optional): The number of subscriptions to fetch per request 87 | target_id (str, optional): The ID of a specific target to use as a filter 88 | target_type (str, optional): The type of the target (one of "department", "office", 89 | "callcenter", "user", "room", "staffgroup", "callrouter", 90 | "channel", "coachinggroup", or "unknown") 91 | 92 | See Also: 93 | https://developers.dialpad.com/reference#smseventsubscriptionapi_listsmseventsubscriptions 94 | """ 95 | return self.request(['sms'], method='GET', data=dict(limit=limit, **kwargs)) 96 | 97 | def get_sms_event_subscription(self, subscription_id): 98 | """Gets a specific sms event subscription. 99 | 100 | Args: 101 | subscription_id (str, required): The ID of the subscription 102 | 103 | See Also: 104 | https://developers.dialpad.com/reference#smseventsubscriptionapi_getsmseventsubscription 105 | """ 106 | return self.request(['sms', subscription_id], method='GET') 107 | 108 | def put_sms_event_subscription(self, subscription_id, url, direction, enabled=True, 109 | **kwargs): 110 | """Update or create an sms event subscription. 111 | 112 | The subscription_id is required. If the ID exists, then this call will update the subscription 113 | resource. If the ID does not exist, then it will create a new subscription with that ID. 114 | 115 | Args: 116 | subscription_id (str, required): The ID of the subscription 117 | url (str, required): The URL which should be called when the subscription fires 118 | direction (str, required): The SMS direction that should fire the subscripion ("inbound", 119 | "outbound", or "all") 120 | enabled (bool, optional): Whether or not the subscription should actually fire 121 | secret (str, optional): A secret to use to encrypt subscription event payloads 122 | target_id (str, optional): The ID of a specific target to use as a filter 123 | target_type (str, optional): The type of the target (one of "department", "office", 124 | "callcenter", "user", "room", "staffgroup", "callrouter", 125 | "channel", "coachinggroup", or "unknown") 126 | 127 | See Also: 128 | https://developers.dialpad.com/reference#smseventsubscriptionapi_createorupdatesmseventsubscription 129 | """ 130 | 131 | return self.request(['sms', subscription_id], method='PUT', 132 | data=dict(url=url, enabled=enabled, direction=direction, 133 | **kwargs)) 134 | 135 | def delete_sms_event_subscription(self, subscription_id): 136 | """Deletes a specific sms event subscription. 137 | 138 | Args: 139 | subscription_id (str, required): The ID of the subscription 140 | 141 | See Also: 142 | https://developers.dialpad.com/reference#smseventsubscriptionapi_deletesmseventsubscription 143 | """ 144 | return self.request(['sms', subscription_id], method='DELETE') 145 | 146 | -------------------------------------------------------------------------------- /dialpad/resources/number.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class NumberResource(DialpadResource): 4 | """NumberResource implements python bindings for the Dialpad API's number endpoints. 5 | 6 | See https://developers.dialpad.com/reference#numbers for additional documentation. 7 | """ 8 | _resource_path = ['numbers'] 9 | 10 | def list(self, limit=25, **kwargs): 11 | """List all phone numbers in the company. 12 | 13 | Args: 14 | limit (int, optional): The number of numbers to fetch per request 15 | status (str, optional): If provided, the results will only contain numbers with the specified 16 | status. Must be one of: "available", "pending", "office", 17 | "department", "call_center", "user", "room", "porting", "call_router", 18 | or "dynamic_caller" 19 | 20 | See Also: 21 | https://developers.dialpad.com/reference#numberapi_listnumbers 22 | """ 23 | return self.request(method='GET', data=dict(limit=limit, **kwargs)) 24 | 25 | def get(self, number): 26 | """Gets a specific number object. 27 | 28 | Args: 29 | number (str, required): An e164-formatted number. 30 | 31 | See Also: 32 | https://developers.dialpad.com/reference#numberapi_getnumber 33 | """ 34 | return self.request([number], method='GET') 35 | 36 | def unassign(self, number, release=False): 37 | """Unassigns the specified number. 38 | 39 | Args: 40 | number (str, required): An e164-formatted number. 41 | release (bool, optional): If the "release" flag is omitted or set to False, the number will 42 | be returned to the company pool (i.e. your company will still own 43 | the number, but it will no longer be assigned to any targets). 44 | If the "release" flag is set, then the number will be beamed back 45 | to the Dialpad mothership. 46 | 47 | See Also: 48 | https://developers.dialpad.com/reference#numberapi_unassignnumber 49 | """ 50 | return self.request([number], method='DELETE', data={'release': release}) 51 | 52 | def assign(self, number, target_id, target_type, primary=True): 53 | """Assigns the specified number to the specified target. 54 | 55 | Args: 56 | number (str, required): The e164-formatted number that should be assigned. 57 | target_id (int, required): The ID of the target to which the number should be assigned. 58 | target_type (str, required): The type corresponding to the provided target ID. 59 | primary (bool, optional): (Defaults to True) If the "primary" flag is set, then the assigned 60 | number will become the primary number of the specified target. 61 | 62 | See Also: 63 | https://developers.dialpad.com/reference#numberapi_assignnumber 64 | """ 65 | return self.request(['assign'], method='POST', data={ 66 | 'number': number, 67 | 'target_id': target_id, 68 | 'target_type': target_type, 69 | 'primary': primary 70 | }) 71 | 72 | def format(self, number, country_code=None): 73 | """Converts local number to E.164 or E.164 to local format. 74 | 75 | Args: 76 | number (str, required): The phone number in local or E.164 format. 77 | country_code (str, optional): Country code in ISO 3166-1 alpha-2 format such as "US". 78 | Required when sending a local formatted phone number. 79 | 80 | See Also: 81 | https://developers.dialpad.com/reference#formatapi_formatnumber 82 | """ 83 | return self.request(['format'], method='POST', data={ 84 | 'number': number, 85 | 'country_code': country_code 86 | }) 87 | -------------------------------------------------------------------------------- /dialpad/resources/office.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class OfficeResource(DialpadResource): 4 | """OfficeResource implements python bindings for the Dialpad API's office endpoints. 5 | See https://developers.dialpad.com/reference#offices for additional documentation. 6 | """ 7 | 8 | _resource_path = ['offices'] 9 | 10 | def list(self, limit=25, **kwargs): 11 | """Lists the company's offices. 12 | 13 | Args: 14 | limit (int, optional): the number of offices to fetch per request. 15 | 16 | See Also: 17 | https://developers.dialpad.com/reference#officeapi_listoffices 18 | """ 19 | return self.request(method='GET', data=dict(limit=limit, **kwargs)) 20 | 21 | def get(self, office_id): 22 | """Gets an office by ID. 23 | 24 | Args: 25 | office_id (int, required): The ID of the office to retrieve. 26 | 27 | See Also: 28 | https://developers.dialpad.com/reference#officeapi_getoffice 29 | """ 30 | return self.request([office_id], method='GET') 31 | 32 | def assign_number(self, office_id, **kwargs): 33 | """Assigns a phone number to the specified office 34 | 35 | Args: 36 | office_id (int, required): The ID of the office to which the number should be assigned. 37 | number (str, optional): An e164 number that has already been allocated to the company's 38 | reserved number pool that should be re-assigned to this office. 39 | area_code (str, optional): The area code to use to filter the set of available numbers to be 40 | assigned to this office. 41 | 42 | See Also: 43 | https://developers.dialpad.com/reference#numberapi_assignnumbertooffice 44 | """ 45 | return self.request([office_id, 'assign_number'], method='POST', data=kwargs) 46 | 47 | def get_operators(self, office_id): 48 | """Gets the list of users who are operators for the specified office. 49 | 50 | Args: 51 | office_id (int, required): The ID of the office. 52 | 53 | See Also: 54 | https://developers.dialpad.com/reference#officeapi_listoperators 55 | """ 56 | return self.request([office_id, 'operators'], method='GET') 57 | 58 | def unassign_number(self, office_id, number): 59 | """Unassigns the specified number from the specified office. 60 | 61 | Args: 62 | office_id (int, required): The ID of the office. 63 | number (str, required): The e164-formatted number that should be unassigned from the office. 64 | 65 | See Also: 66 | https://developers.dialpad.com/reference#numberapi_unassignnumberfromoffice 67 | """ 68 | return self.request([office_id, 'unassign_number'], method='POST', data={'number': number}) 69 | 70 | def get_call_centers(self, office_id, limit=25, **kwargs): 71 | """Lists the call centers under the specified office. 72 | 73 | Args: 74 | office_id (int, required): The ID of the office. 75 | limit (int, optional): the number of call centers to fetch per request. 76 | 77 | See Also: 78 | https://developers.dialpad.com/reference#callcenterapi_listcallcenters 79 | """ 80 | return self.request([office_id, 'callcenters'], method='GET', data=dict(limit=limit, **kwargs)) 81 | 82 | def get_departments(self, office_id, limit=25, **kwargs): 83 | """Lists the departments under the specified office. 84 | 85 | Args: 86 | office_id (int, required): The ID of the office. 87 | limit (int, optional): the number of departments to fetch per request. 88 | 89 | See Also: 90 | https://developers.dialpad.com/reference#departmentapi_listdepartments 91 | """ 92 | return self.request([office_id, 'departments'], method='GET', data=dict(limit=limit, **kwargs)) 93 | 94 | def get_plan(self, office_id): 95 | """Gets the plan associated with the office. 96 | 97 | (i.e. a breakdown of the licenses that have been purchased for the specified office) 98 | 99 | Args: 100 | office_id (int, required): The ID of the office. 101 | 102 | See Also: 103 | https://developers.dialpad.com/reference#planapi_getplan 104 | """ 105 | return self.request([office_id, 'plan'], method='GET') 106 | -------------------------------------------------------------------------------- /dialpad/resources/resource.py: -------------------------------------------------------------------------------- 1 | class DialpadResource(object): 2 | _resource_path = None 3 | 4 | def __init__(self, client, basepath=None): 5 | self._client = client 6 | 7 | def request(self, path=None, *args, **kwargs): 8 | if self._resource_path is None: 9 | raise NotImplementedError('DialpadResource subclasses must have a _resource_path property') 10 | path = path or [] 11 | return self._client.request(self._resource_path + path, *args, **kwargs) 12 | -------------------------------------------------------------------------------- /dialpad/resources/room.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class RoomResource(DialpadResource): 4 | """RoomResource implements python bindings for the Dialpad API's room endpoints. 5 | See https://developers.dialpad.com/reference#rooms for additional documentation. 6 | """ 7 | 8 | _resource_path = ['rooms'] 9 | 10 | def list(self, limit=25, **kwargs): 11 | """Lists rooms in the company. 12 | 13 | Args: 14 | limit (int, optional): The number of rooms to fetch per request. 15 | office_id (int, optional): If specified, only rooms associated with that office will be 16 | returned. 17 | 18 | See Also: 19 | https://developers.dialpad.com/reference#roomapi_listrooms 20 | """ 21 | return self.request(method='GET', data=dict(limit=limit, **kwargs)) 22 | 23 | def create(self, name, office_id): 24 | """Creates a new room with the specified name within the specified office. 25 | 26 | Args: 27 | name (str, required): A human-readable name for the room. 28 | office_id (int, required): The ID of the office. 29 | 30 | See Also: 31 | https://developers.dialpad.com/reference#roomapi_createroom 32 | """ 33 | return self.request(method='POST', data={'name': name, 'office_id': office_id}) 34 | 35 | def generate_international_pin(self, customer_ref): 36 | """Creates a PIN to allow an international call to be made from a room phone. 37 | 38 | Args: 39 | customer_ref (str, required): An identifier to be printed in the usage summary. Typically used 40 | for identifying the person who requested the PIN 41 | 42 | See Also: 43 | https://developers.dialpad.com/reference#deskphoneapi_createinternationalpin 44 | """ 45 | return self.request(['international_pin'], method='POST', data={'customer_ref': customer_ref}) 46 | 47 | def delete(self, room_id): 48 | """Deletes a room by ID. 49 | 50 | Args: 51 | room_id (str, required): The ID of the room to be deleted. 52 | 53 | See Also: 54 | https://developers.dialpad.com/reference#roomapi_deleteroom 55 | """ 56 | return self.request([room_id], method='DELETE') 57 | 58 | def get(self, room_id): 59 | """Gets a room by ID. 60 | 61 | Args: 62 | room_id (str, required): The ID of the room to be fetched. 63 | 64 | See Also: 65 | https://developers.dialpad.com/reference#roomapi_getroom 66 | """ 67 | return self.request([room_id], method='GET') 68 | 69 | def update(self, room_id, **kwargs): 70 | """Updates the specified room. 71 | 72 | Args: 73 | room_id (str, required): The ID of the room to be updated. 74 | name (str, optional): A human-readable name for the room. 75 | phone_numbers (list, optional): The list of e164-formatted phone numbers that should be 76 | associated with this room. New numbers will be assigned, 77 | and omitted numbers will be unassigned. 78 | 79 | See Also: 80 | https://developers.dialpad.com/reference#roomapi_updateroom 81 | """ 82 | return self.request([room_id], method='PATCH', data=kwargs) 83 | 84 | def assign_number(self, room_id, **kwargs): 85 | """Assigns a phone number to the specified room 86 | 87 | Args: 88 | room_id (int, required): The ID of the room to which the number should be assigned. 89 | number (str, optional): An e164 number that has already been allocated to the company's 90 | reserved number pool that should be re-assigned to this office. 91 | area_code (str, optional): The area code to use to filter the set of available numbers to be 92 | assigned to this office. 93 | 94 | See Also: 95 | https://developers.dialpad.com/reference#numberapi_assignnumbertoroom 96 | """ 97 | return self.request([room_id, 'assign_number'], method='POST', data=kwargs) 98 | 99 | def unassign_number(self, room_id, number): 100 | """Unassigns the specified number from the specified room. 101 | 102 | Args: 103 | room_id (int, required): The ID of the room. 104 | number (str, required): The e164-formatted number that should be unassigned from the room. 105 | 106 | See Also: 107 | https://developers.dialpad.com/reference#numberapi_unassignnumberfromroom 108 | """ 109 | return self.request([room_id, 'unassign_number'], method='POST', data={'number': number}) 110 | 111 | def get_deskphones(self, room_id): 112 | """Lists the phones that are assigned to the specified room. 113 | 114 | Args: 115 | room_id (int, required): The ID of the room. 116 | 117 | See Also: 118 | https://developers.dialpad.com/reference#deskphoneapi_listroomdeskphones 119 | """ 120 | return self.request([room_id, 'deskphones'], method='GET') 121 | 122 | def create_deskphone(self, room_id, mac_address, name, phone_type): 123 | """Creates a desk phone belonging to the specified room. 124 | 125 | Args: 126 | room_id (int, required): The ID of the room. 127 | mac_address (str, required): MAC address of the desk phone. 128 | name (str, required): A human-readable name for the desk phone. 129 | phone_type (str, required): Type (vendor) of the desk phone. One of "obi", "polycom", "sip", 130 | "mini", "audiocodes", "yealink". Use "sip" for generic types. 131 | 132 | See Also: 133 | https://developers.dialpad.com/reference#deskphoneapi_createroomdeskphone 134 | """ 135 | return self.request([room_id, 'deskphones'], method='POST', data={ 136 | 'mac_address': mac_address, 137 | 'name': name, 138 | 'type': phone_type, 139 | }) 140 | 141 | def delete_deskphone(self, room_id, deskphone_id): 142 | """Deletes the specified desk phone. 143 | 144 | Args: 145 | room_id (int, required): The ID of the room. 146 | deskphone_id (str, required): The ID of the desk phone. 147 | 148 | See Also: 149 | https://developers.dialpad.com/reference#deskphoneapi_deleteroomdeskphone 150 | """ 151 | return self.request([room_id, 'deskphones', deskphone_id], method='DELETE') 152 | 153 | def get_deskphone(self, room_id, deskphone_id): 154 | """Gets the specified desk phone. 155 | 156 | Args: 157 | room_id (int, required): The ID of the room. 158 | deskphone_id (str, required): The ID of the desk phone. 159 | 160 | See Also: 161 | https://developers.dialpad.com/reference#deskphoneapi_getroomdeskphone 162 | """ 163 | return self.request([room_id, 'deskphones', deskphone_id], method='GET') 164 | -------------------------------------------------------------------------------- /dialpad/resources/sms.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class SMSResource(DialpadResource): 4 | """SMSResource implements python bindings for the Dialpad API's sms endpoints. 5 | See https://developers.dialpad.com/reference#sms for additional documentation. 6 | """ 7 | _resource_path = ['sms'] 8 | 9 | def send_sms(self, user_id, to_numbers, text, **kwargs): 10 | """Sends an SMS message on behalf of the specified user. 11 | 12 | Args: 13 | user_id (int, required): The ID of the user that should be sending the SMS. 14 | to_numbers (list, required): A list of one-or-more e164-formatted phone numbers which 15 | should receive the SMS. 16 | text (str, required): The content of the SMS message. 17 | infer_country_code (bool, optional): If set, the e164-contraint will be relaxed on to_numbers, 18 | and potentially ambiguous numbers will be assumed to be 19 | numbers in the specified user's country. 20 | sender_group_id (int, optional): The ID of an office, department, or call center that the user 21 | should send the SMS on behalf of. 22 | sender_group_type (str, optional): The ID type (i.e. office, department, or callcenter). 23 | 24 | See Also: 25 | https://developers.dialpad.com/reference#roomapi_listrooms 26 | """ 27 | return self.request(method='POST', data=dict(text=text, user_id=user_id, to_numbers=to_numbers, 28 | **kwargs)) 29 | -------------------------------------------------------------------------------- /dialpad/resources/stats.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class StatsExportResource(DialpadResource): 4 | """StatsExportResource implements python bindings for the Dialpad API's stats endpoints. 5 | See https://developers.dialpad.com/reference#stats for additional documentation. 6 | """ 7 | _resource_path = ['stats'] 8 | 9 | def post(self, coaching_group=False, days_ago_start=1, days_ago_end=30, is_today=False, 10 | export_type='stats', stat_type='calls', **kwargs): 11 | """Initiate a stats export. 12 | 13 | Args: 14 | coaching_group (bool, optional): Whether or not the the statistics should be for trainees of 15 | the coach with the given target_id. 16 | days_ago_start (int, optional): Start of the date range to get statistics for. This is the 17 | number of days to look back relative to the current day. Used 18 | in conjunction with days_ago_end to specify a range. 19 | days_ago_end (int, optional): End of the date range to get statistics for. This is the number 20 | of days to look back relative to the current day. Used in 21 | conjunction with days_ago_start to specify a range. 22 | is_today (bool, optional): Whether or not the statistics are for the current day. 23 | days_ago_start and days_ago_end are ignored if this is passed in 24 | export_type ("stats" or "records", optional): Whether to return aggregated statistics (stats), 25 | or individual rows for each record (records). 26 | stat_type (str, optional): One of "calls", "texts", "voicemails", "recordings", "onduty", 27 | "csat", "dispositions". The type of statistics to be returned. 28 | office_id (int, optional): ID of the office to get statistics for. If a target_id and 29 | target_type are passed in this value is ignored and instead the 30 | target is used. 31 | target_id (int, optional): The ID of the target for which to return statistics. 32 | target_type (type, optional): One of "department", "office", "callcenter", "user", "room", 33 | "staffgroup", "callrouter", "channel", "coachinggroup", 34 | "unknown". The type corresponding to the target_id. 35 | timezone (str, optional): Timezone using a tz database name. 36 | 37 | See Also: 38 | https://developers.dialpad.com/reference#statsapi_processstats 39 | """ 40 | 41 | data = { 42 | 'coaching_group': coaching_group, 43 | 'days_ago_start': str(days_ago_start), 44 | 'days_ago_end': str(days_ago_end), 45 | 'is_today': is_today, 46 | 'export_type': export_type, 47 | 'stat_type': stat_type, 48 | } 49 | 50 | data.update(kwargs) 51 | 52 | data = {k: v for k, v in data.items() if v is not None} 53 | return self.request(method='POST', data=data) 54 | 55 | def get(self, export_id): 56 | """Retrieves the results of a stats export. 57 | 58 | Args: 59 | export_id (str, required): The export ID returned by the post method. 60 | 61 | See Also: 62 | https://developers.dialpad.com/reference#statsapi_getstats 63 | """ 64 | return self.request([export_id]) 65 | -------------------------------------------------------------------------------- /dialpad/resources/subscription.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class SubscriptionResource(DialpadResource): 4 | """SubscriptionResource implements python bindings for the Dialpad API's subscription 5 | endpoints. 6 | 7 | See https://developers.dialpad.com/reference#subscriptions for additional documentation. 8 | """ 9 | _resource_path = ['subscriptions'] 10 | 11 | def list_agent_status_event_subscriptions(self, limit=25, **kwargs): 12 | """Lists agent status event subscriptions. 13 | 14 | Args: 15 | limit (int, optional): The number of subscriptions to fetch per request 16 | 17 | See Also: 18 | https://developers.dialpad.com/reference#webhookagentstatuseventsubscriptionapi_listagentstatuseventsubscriptions 19 | """ 20 | return self.request(['agent_status'], method='GET', data=dict(limit=limit, **kwargs)) 21 | 22 | def get_agent_status_event_subscription(self, subscription_id): 23 | """Gets a specific agent status event subscription. 24 | 25 | Args: 26 | subscription_id (str, required): The ID of the subscription 27 | 28 | See Also: 29 | https://developers.dialpad.com/reference#webhookagentstatuseventsubscriptionapi_getagentstatuseventsubscription 30 | """ 31 | return self.request(['agent_status', subscription_id], method='GET') 32 | 33 | def create_agent_status_event_subscription(self, webhook_id, agent_type, enabled=True, **kwargs): 34 | """Create a new agent status event subscription. 35 | 36 | Args: 37 | webhook_id (str, required): The ID of the webhook which should be called when the 38 | subscription fires 39 | agent_type (str, required): The type of agent to subscribe to updates to 40 | enabled (bool, optional): Whether or not the subscription should actually fire 41 | 42 | See Also: 43 | https://developers.dialpad.com/reference#webhookagentstatuseventsubscriptionapi_createagentstatuseventsubscription 44 | """ 45 | 46 | return self.request(['agent_status'], method='POST', 47 | data=dict(webhook_id=webhook_id, enabled=enabled, agent_type=agent_type, 48 | **kwargs)) 49 | 50 | def update_agent_status_event_subscription(self, subscription_id, **kwargs): 51 | """Update an existing agent status event subscription. 52 | 53 | Args: 54 | subscription_id (str, required): The ID of the subscription 55 | webhook_id (str, optional): The ID of the webhook which should be called when the 56 | subscription fires 57 | agent_type (str, optional): The type of agent to subscribe to updates to 58 | enabled (bool, optional): Whether or not the subscription should actually fire 59 | 60 | See Also: 61 | https://developers.dialpad.com/reference#webhookagentstatuseventsubscriptionapi_updateagentstatuseventsubscription 62 | """ 63 | 64 | return self.request(['agent_status', subscription_id], method='PATCH', data=kwargs) 65 | 66 | def delete_agent_status_event_subscription(self, subscription_id): 67 | """Deletes a specific agent status event subscription. 68 | 69 | Args: 70 | subscription_id (str, required): The ID of the subscription 71 | 72 | See Also: 73 | https://developers.dialpad.com/reference#webhookagentstatuseventsubscriptionapi_deleteagentstatuseventsubscription 74 | """ 75 | return self.request(['agent_status', subscription_id], method='DELETE') 76 | 77 | 78 | def list_call_event_subscriptions(self, limit=25, **kwargs): 79 | """Lists call event subscriptions. 80 | 81 | Args: 82 | limit (int, optional): The number of subscriptions to fetch per request 83 | target_id (str, optional): The ID of a specific target to use as a filter 84 | target_type (str, optional): The type of the target (one of "department", "office", 85 | "callcenter", "user", "room", "staffgroup", "callrouter", 86 | "channel", "coachinggroup", or "unknown") 87 | 88 | See Also: 89 | https://developers.dialpad.com/reference#webhookcalleventsubscriptionapi_listcalleventsubscriptions 90 | """ 91 | return self.request(['call'], method='GET', data=dict(limit=limit, **kwargs)) 92 | 93 | def get_call_event_subscription(self, subscription_id): 94 | """Gets a specific call event subscription. 95 | 96 | Args: 97 | subscription_id (str, required): The ID of the subscription 98 | 99 | See Also: 100 | https://developers.dialpad.com/reference#webhookcalleventsubscriptionapi_getcalleventsubscription 101 | """ 102 | return self.request(['call', subscription_id], method='GET') 103 | 104 | def create_call_event_subscription(self, webhook_id, enabled=True, group_calls_only=False, 105 | **kwargs): 106 | """Create a new call event subscription. 107 | 108 | Args: 109 | webhook_id (str, required): The ID of the webhook which should be called when the 110 | subscription fires 111 | enabled (bool, optional): Whether or not the subscription should actually fire 112 | group_calls_only (bool, optional): Whether to limit the subscription to only fire if the call 113 | is a group call 114 | target_id (str, optional): The ID of a specific target to use as a filter 115 | target_type (str, optional): The type of the target (one of "department", "office", 116 | "callcenter", "user", "room", "staffgroup", "callrouter", 117 | "channel", "coachinggroup", or "unknown") 118 | call_states (list, optional): The specific types of call events that should trigger the 119 | subscription (any of "preanswer", "calling", "ringing", 120 | "connected", "merged", "hold", "queued", "voicemail", 121 | "eavesdrop", "monitor", "barge", "hangup", "blocked", 122 | "admin", "parked", "takeover", "all", "postcall", 123 | "transcription", or "recording") 124 | 125 | See Also: 126 | https://developers.dialpad.com/reference#webhookcalleventsubscriptionapi_createcalleventsubscription 127 | """ 128 | 129 | return self.request(['call'], method='POST', 130 | data=dict(webhook_id=webhook_id, enabled=enabled, group_calls_only=group_calls_only, 131 | **kwargs)) 132 | 133 | def update_call_event_subscription(self, subscription_id, **kwargs): 134 | """Update an existing call event subscription. 135 | 136 | Args: 137 | subscription_id (str, required): The ID of the subscription 138 | webhook_id (str, optional): The ID of the webhook which should be called when the 139 | subscription fires 140 | enabled (bool, optional): Whether or not the subscription should actually fire 141 | group_calls_only (bool, optional): Whether to limit the subscription to only fire if the call 142 | is a group call 143 | target_id (str, optional): The ID of a specific target to use as a filter 144 | target_type (str, optional): The type of the target (one of "department", "office", 145 | "callcenter", "user", "room", "staffgroup", "callrouter", 146 | "channel", "coachinggroup", or "unknown") 147 | call_states (list, optional): The specific types of call events that should trigger the 148 | subscription (any of "preanswer", "calling", "ringing", 149 | "connected", "merged", "hold", "queued", "voicemail", 150 | "eavesdrop", "monitor", "barge", "hangup", "blocked", 151 | "admin", "parked", "takeover", "all", "postcall", 152 | "transcription", or "recording") 153 | 154 | See Also: 155 | https://developers.dialpad.com/reference#webhookcalleventsubscriptionapi_updatecalleventsubscription 156 | """ 157 | return self.request(['call', subscription_id], method='PATCH', data=kwargs) 158 | 159 | def delete_call_event_subscription(self, subscription_id): 160 | """Deletes a specific call event subscription. 161 | 162 | Args: 163 | subscription_id (str, required): The ID of the subscription 164 | 165 | See Also: 166 | https://developers.dialpad.com/reference#webhookcalleventsubscriptionapi_deletecalleventsubscription 167 | """ 168 | return self.request(['call', subscription_id], method='DELETE') 169 | 170 | 171 | def list_contact_event_subscriptions(self, limit=25, **kwargs): 172 | """Lists contact event subscriptions. 173 | 174 | Args: 175 | limit (int, optional): The number of subscriptions to fetch per request 176 | 177 | See Also: 178 | https://developers.dialpad.com/reference#webhookcontacteventsubscriptionapi_listcontacteventsubscriptions 179 | """ 180 | return self.request(['contact'], method='GET', data=dict(limit=limit, **kwargs)) 181 | 182 | def get_contact_event_subscription(self, subscription_id): 183 | """Gets a specific contact event subscription. 184 | 185 | Args: 186 | subscription_id (str, required): The ID of the subscription 187 | 188 | See Also: 189 | https://developers.dialpad.com/reference#webhookcontacteventsubscriptionapi_getcontacteventsubscription 190 | """ 191 | return self.request(['contact', subscription_id], method='GET') 192 | 193 | def create_contact_event_subscription(self, webhook_id, contact_type, enabled=True, **kwargs): 194 | """Create a new contact event subscription. 195 | 196 | Args: 197 | webhook_id (str, required): The ID of the webhook which should be called when the 198 | subscription fires 199 | contact_type (str, required): The type of contact to subscribe to events for 200 | enabled (bool, optional): Whether or not the subscription should actually fire 201 | 202 | See Also: 203 | https://developers.dialpad.com/reference#webhookcontacteventsubscriptionapi_createcontacteventsubscription 204 | """ 205 | 206 | return self.request(['contact'], method='POST', 207 | data=dict(webhook_id=webhook_id, enabled=enabled, 208 | contact_type=contact_type, **kwargs)) 209 | 210 | def update_contact_event_subscription(self, subscription_id, **kwargs): 211 | """Update an existing contact event subscription. 212 | 213 | Args: 214 | subscription_id (str, required): The ID of the subscription 215 | webhook_id (str, optional): The ID of the webhook which should be called when the 216 | subscription fires 217 | contact_type (str, optional): The type of contact to subscribe to events for 218 | enabled (bool, optional): Whether or not the subscription should actually fire 219 | 220 | See Also: 221 | https://developers.dialpad.com/reference#webhookcontacteventsubscriptionapi_updatecontacteventsubscription 222 | """ 223 | return self.request(['contact', subscription_id], method='PATCH', data=kwargs) 224 | 225 | def delete_contact_event_subscription(self, subscription_id): 226 | """Deletes a specific contact event subscription. 227 | 228 | Args: 229 | subscription_id (str, required): The ID of the subscription 230 | 231 | See Also: 232 | https://developers.dialpad.com/reference#webhookcontacteventsubscriptionapi_deletecontacteventsubscription 233 | """ 234 | return self.request(['contact', subscription_id], method='DELETE') 235 | 236 | 237 | def list_sms_event_subscriptions(self, limit=25, **kwargs): 238 | """Lists SMS event subscriptions. 239 | 240 | Args: 241 | limit (int, optional): The number of subscriptions to fetch per request 242 | target_id (str, optional): The ID of a specific target to use as a filter 243 | target_type (str, optional): The type of the target (one of "department", "office", 244 | "callcenter", "user", "room", "staffgroup", "callrouter", 245 | "channel", "coachinggroup", or "unknown") 246 | 247 | See Also: 248 | https://developers.dialpad.com/reference#webhooksmseventsubscriptionapi_listsmseventsubscriptions 249 | """ 250 | return self.request(['sms'], method='GET', data=dict(limit=limit, **kwargs)) 251 | 252 | def get_sms_event_subscription(self, subscription_id): 253 | """Gets a specific sms event subscription. 254 | 255 | Args: 256 | subscription_id (str, required): The ID of the subscription 257 | 258 | See Also: 259 | https://developers.dialpad.com/reference#webhooksmseventsubscriptionapi_getsmseventsubscription 260 | """ 261 | return self.request(['sms', subscription_id], method='GET') 262 | 263 | def create_sms_event_subscription(self, webhook_id, direction, enabled=True, **kwargs): 264 | """Create a new SMS event subscription. 265 | 266 | Args: 267 | webhook_id (str, required): The ID of the webhook which should be called when the 268 | subscription fires 269 | direction (str, required): The SMS direction that should fire the subscripion ("inbound", 270 | "outbound", or "all") 271 | enabled (bool, optional): Whether or not the subscription should actually fire 272 | target_id (str, optional): The ID of a specific target to use as a filter 273 | target_type (str, optional): The type of the target (one of "department", "office", 274 | "callcenter", "user", "room", "staffgroup", "callrouter", 275 | "channel", "coachinggroup", or "unknown") 276 | 277 | See Also: 278 | https://developers.dialpad.com/reference#smseventsubscriptionapi_createorupdatesmseventsubscription 279 | """ 280 | 281 | return self.request(['sms'], method='POST', 282 | data=dict(webhook_id=webhook_id, enabled=enabled, direction=direction, 283 | **kwargs)) 284 | 285 | def update_sms_event_subscription(self, subscription_id, **kwargs): 286 | """Update an existing SMS event subscription. 287 | 288 | Args: 289 | subscription_id (str, required): The ID of the subscription 290 | webhook_id (str, optional): The ID of the webhook which should be called when the 291 | subscription fires 292 | direction (str, optional): The SMS direction that should fire the subscripion ("inbound", 293 | "outbound", or "all") 294 | enabled (bool, optional): Whether or not the subscription should actually fire 295 | target_id (str, optional): The ID of a specific target to use as a filter 296 | target_type (str, optional): The type of the target (one of "department", "office", 297 | "callcenter", "user", "room", "staffgroup", "callrouter", 298 | "channel", "coachinggroup", or "unknown") 299 | 300 | See Also: 301 | https://developers.dialpad.com/reference#smseventsubscriptionapi_createorupdatesmseventsubscription 302 | """ 303 | 304 | return self.request(['sms', subscription_id], method='PATCH', data=kwargs) 305 | 306 | def delete_sms_event_subscription(self, subscription_id): 307 | """Deletes a specific sms event subscription. 308 | 309 | Args: 310 | subscription_id (str, required): The ID of the subscription 311 | 312 | See Also: 313 | https://developers.dialpad.com/reference#webhooksmseventsubscriptionapi_deletesmseventsubscription 314 | """ 315 | return self.request(['sms', subscription_id], method='DELETE') 316 | 317 | -------------------------------------------------------------------------------- /dialpad/resources/transcript.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class TranscriptResource(DialpadResource): 4 | """TranscriptResource implements python bindings for the Dialpad API's transcript endpoints. 5 | See https://developers.dialpad.com/reference#transcripts for additional documentation. 6 | """ 7 | _resource_path = ['transcripts'] 8 | 9 | def get(self, call_id): 10 | """Get the transcript of a call. 11 | 12 | Args: 13 | call_id (int, required): The ID of the call. 14 | 15 | See Also: 16 | https://developers.dialpad.com/reference#transcriptapi_gettranscript 17 | """ 18 | return self.request([call_id]) 19 | -------------------------------------------------------------------------------- /dialpad/resources/user.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class UserResource(DialpadResource): 4 | """UserResource implements python bindings for the Dialpad API's user endpoints. 5 | See https://developers.dialpad.com/reference#users for additional documentation. 6 | """ 7 | _resource_path = ['users'] 8 | 9 | def list(self, limit=25, **kwargs): 10 | """Lists users in the company. 11 | 12 | Args: 13 | email (str, optional): Limits results to users with a matching email address. 14 | state (str, optional): Limits results to users in the specified state (one of "active", 15 | "canceled", "suspended", "pending", "deleted", "all) 16 | limit (int, optional): The number of users to fetch per request. 17 | 18 | See Also: 19 | https://developers.dialpad.com/reference#userapi_listusers 20 | """ 21 | return self.request(data=dict(limit=limit, **kwargs)) 22 | 23 | def create(self, email, office_id, **kwargs): 24 | """Creates a new user. 25 | 26 | Args: 27 | email (str, required): The user's email address. 28 | office_id (int, required): The ID of the office that the user should belong to. 29 | first_name (str, optional): The first name of the user. 30 | last_name (str, optional): The last name of the user. 31 | license (str, optional): The license that the user should be created with. (One of "talk", 32 | "agents", "lite_support_agents", "lite_lines") 33 | 34 | See Also: 35 | https://developers.dialpad.com/reference#userapi_createuser 36 | """ 37 | return self.request(method='POST', data=dict(email=email, office_id=office_id, **kwargs)) 38 | 39 | def delete(self, user_id): 40 | """Deletes a user. 41 | 42 | Args: 43 | user_id (int, required): The ID of the user to delete. 44 | 45 | See Also: 46 | https://developers.dialpad.com/reference#userapi_deleteuser 47 | """ 48 | return self.request([user_id], method='DELETE') 49 | 50 | def get(self, user_id): 51 | """Gets a user by ID. 52 | 53 | Args: 54 | user_id (int, required): The ID of the user. 55 | 56 | See Also: 57 | https://developers.dialpad.com/reference#userapi_getuser 58 | """ 59 | return self.request([user_id]) 60 | 61 | def update(self, user_id, **kwargs): 62 | """Updates a user by ID. 63 | 64 | Note: 65 | The "phone_numbers" argument can be used to re-order or unassign numbers, but it cannot be 66 | used to assign new numbers. To assign new numbers to a user, please use the number assignment 67 | API instead. 68 | 69 | Args: 70 | user_id (int, required): The ID of the user. 71 | admin_office_ids (list, optional): The office IDs that this user should be an admin of. 72 | emails (list, optional): The email addresses that should be assoiciated with this user. 73 | extension (str, optional): The extension that this user can be reached at. 74 | first_name (str, optional): The first name of the user. 75 | last_name (str, optional): The last name of the user. 76 | forwarding_numbers (list, optional): The e164-formatted numbers that should also ring 77 | when the user receives a Dialpad call. 78 | is_super_admin (bool, optional): Whether this user should be a company-level admin. 79 | job_title (str, optional): The user's job title. 80 | license (str, optional): The user's license type. Changing this affects billing for the user. 81 | office_id (int, optional): The ID of office to which this user should belong. 82 | phone_numbers (list, optional): The e164-formatted numbers that should be assigned to 83 | this user. 84 | state (str, optional): The state of the user (One of "suspended", "active") 85 | 86 | See Also: 87 | https://developers.dialpad.com/reference#userapi_updateuser 88 | """ 89 | return self.request([user_id], method='PATCH', data=kwargs) 90 | 91 | def toggle_call_recording(self, user_id, **kwargs): 92 | """Turn call recording on or off for a user's active call. 93 | 94 | Args: 95 | user_id (int, required): The ID of the user. 96 | is_recording (bool, optional): Whether recording should be turned on. 97 | play_message (bool, optional): Whether a message should be played to the user to notify them 98 | that they are now being (or no longer being) recorded. 99 | recording_type (str, optional): One of "user", "group", or "all". If set to "user", then only 100 | the user's individual calls will be recorded. If set to 101 | "group", then only calls in which the user is acting as an 102 | operator will be recorded. If set to "all", then all of the 103 | user's calls will be recorded. 104 | 105 | See Also: 106 | https://developers.dialpad.com/reference#callapi_updateactivecall 107 | """ 108 | return self.request([user_id, 'activecall'], method='PATCH', data=kwargs) 109 | 110 | def assign_number(self, user_id, **kwargs): 111 | """Assigns a new number to the user. 112 | 113 | Args: 114 | user_id (int, required): The ID of the user to which the number should be assigned. 115 | number (str, optional): An e164 number that has already been allocated to the company's 116 | reserved number pool that should be re-assigned to this user. 117 | area_code (str, optional): The area code to use to filter the set of available numbers to be 118 | assigned to this user. 119 | 120 | See Also: 121 | https://developers.dialpad.com/reference#numberapi_assignnumbertouser 122 | """ 123 | return self.request([user_id, 'assign_number'], method='POST', data=kwargs) 124 | 125 | def initiate_call(self, user_id, phone_number, **kwargs): 126 | """Causes a user's native Dialpad application to initiate an outbound call. 127 | 128 | Args: 129 | user_id (int, required): The ID of the user. 130 | phone_number (str, required): The e164-formatted number to call. 131 | custom_data (str, optional): free-form extra data to associate with the call. 132 | group_id (str, optional): The ID of a group that will be used to initiate the call. 133 | group_type (str, optional): The type of a group that will be used to initiate the call. 134 | outbound_caller_id (str, optional): The e164-formatted number shown to the call recipient 135 | (or "blocked"). If set to "blocked", the recipient will receive a 136 | call from "unknown caller". 137 | 138 | See Also: 139 | https://developers.dialpad.com/reference#callapi_initiatecall 140 | """ 141 | data = { 142 | 'phone_number': phone_number 143 | } 144 | for k in ['group_id', 'group_type', 'outbound_caller_id', 'custom_data']: 145 | if k in kwargs: 146 | data[k] = kwargs.pop(k) 147 | assert not kwargs 148 | return self.request([user_id, 'initiate_call'], method='POST', data=data) 149 | 150 | def unassign_number(self, user_id, number): 151 | """Unassigns the specified number from the specified user. 152 | 153 | Args: 154 | user_id (int, required): The ID of the user. 155 | number (str, required): The e164-formatted number that should be unassigned from the user. 156 | 157 | See Also: 158 | https://developers.dialpad.com/reference#numberapi_unassignnumberfromuser 159 | """ 160 | return self.request([user_id, 'unassign_number'], method='POST', data={'number': number}) 161 | 162 | def get_deskphones(self, user_id): 163 | """Lists the desk phones that are associated with a user. 164 | 165 | Args: 166 | user_id (int, required): The ID of the user. 167 | 168 | See Also: 169 | https://developers.dialpad.com/reference#deskphoneapi_listuserdeskphones 170 | """ 171 | return self.request([user_id, 'deskphones'], method='GET') 172 | 173 | def create_deskphone(self, user_id, mac_address, name, phone_type): 174 | """Creates a desk phone belonging to the specified user. 175 | 176 | Args: 177 | user_id (int, required): The ID of the user. 178 | mac_address (str, required): MAC address of the desk phone. 179 | name (str, required): A human-readable name for the desk phone. 180 | phone_type (str, required): Type (vendor) of the desk phone. One of "obi", "polycom", "sip", 181 | "mini", "audiocodes", "yealink". Use "sip" for generic types. 182 | 183 | See Also: 184 | https://developers.dialpad.com/reference#deskphoneapi_createuserdeskphone 185 | """ 186 | return self.request([user_id, 'deskphones'], method='POST', data={ 187 | 'mac_address': mac_address, 188 | 'name': name, 189 | 'type': phone_type, 190 | }) 191 | 192 | def delete_deskphone(self, user_id, deskphone_id): 193 | """Deletes the specified desk phone. 194 | 195 | Args: 196 | user_id (int, required): The ID of the user. 197 | deskphone_id (str, required): The ID of the desk phone. 198 | 199 | See Also: 200 | https://developers.dialpad.com/reference#deskphoneapi_deleteuserdeskphone 201 | """ 202 | return self.request([user_id, 'deskphones', deskphone_id], method='DELETE') 203 | 204 | def get_deskphone(self, user_id, deskphone_id): 205 | """Gets the specified desk phone. 206 | 207 | Args: 208 | user_id (int, required): The ID of the user. 209 | deskphone_id (str, required): The ID of the desk phone. 210 | 211 | See Also: 212 | https://developers.dialpad.com/reference#deskphoneapi_getuserdeskphone 213 | """ 214 | return self.request([user_id, 'deskphones', deskphone_id], method='GET') 215 | 216 | def get_personas(self, user_id): 217 | """Lists the calling personas that are associated with a user. 218 | 219 | Args: 220 | user_id (int, required): The ID of the user. 221 | 222 | See Also: 223 | https://developers.dialpad.com/reference#users 224 | """ 225 | return self.request([user_id, 'personas'], method='GET') 226 | 227 | def toggle_do_not_disturb(self, user_id, do_not_disturb): 228 | """Toggle DND status on or off for the given user. 229 | 230 | Args: 231 | user_id (int, required): The ID of the user. 232 | do_not_disturb (bool, required): A boolean indicating whether to enable or disable the 233 | "do not disturb" setting. 234 | 235 | See Also: 236 | https://developers.dialpad.com/reference/userapi_togglednd 237 | """ 238 | return self.request([user_id, 'togglednd'], method='PATCH', 239 | data={'do_not_disturb': do_not_disturb}) 240 | 241 | def search(self, query, **kwargs): 242 | """User -- Search 243 | 244 | Searches for users matching a specific criteria. It matches phone numbers, emails, or name. 245 | Optionally, it accepts filters to reduce the amount of final results. 246 | 247 | - The `cursor` value is provided in the API response, and can be passed as a parameter to 248 | retrieve subsequent pages of results. 249 | 250 | Args: 251 | query (str, required): A string that will be matched against user information. For phone 252 | numbers in e164 format, it is recommended to URL-encode the model 253 | term. 254 | cursor (str, optional): A token used to return the next page of a previous request. Use the 255 | cursor provided in the previous response. 256 | filter (str, optional): If provided, query will be performed against a smaller set of data. 257 | Format for providing filters is in the form of an array of key=value 258 | pairs. (i.e. filter=[key=value]) 259 | 260 | See Also: 261 | https://developers.dialpad.com/reference/searchusers 262 | """ 263 | return self.request(['search'], method='GET', data=dict(query=query, **kwargs)) 264 | -------------------------------------------------------------------------------- /dialpad/resources/userdevice.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class UserDeviceResource(DialpadResource): 4 | """UserDeviceResource implements python bindings for the Dialpad API's userdevice endpoints. 5 | See https://developers.dialpad.com/reference#userdevices for additional documentation. 6 | """ 7 | _resource_path = ['userdevices'] 8 | 9 | def get(self, device_id): 10 | """Gets a user device by ID. 11 | 12 | Args: 13 | device_id (str, required): The ID of the device. 14 | 15 | See Also: 16 | https://developers.dialpad.com/reference#userdeviceapi_getdevice 17 | """ 18 | return self.request([device_id]) 19 | 20 | def list(self, user_id, limit=25, **kwargs): 21 | """Lists the devices for a specific user. 22 | 23 | Args: 24 | user_id (int, required): The ID of the user. 25 | limit (int, optional): the number of devices to fetch per request. 26 | 27 | See Also: 28 | https://developers.dialpad.com/reference#userdeviceapi_listuserdevices 29 | """ 30 | return self.request(data=dict(user_id=user_id, limit=limit, **kwargs)) 31 | -------------------------------------------------------------------------------- /dialpad/resources/webhook.py: -------------------------------------------------------------------------------- 1 | from .resource import DialpadResource 2 | 3 | class WebhookResource(DialpadResource): 4 | """WebhookResource implements python bindings for the Dialpad API's webhook endpoints. 5 | 6 | See https://developers.dialpad.com/reference#webhooks for additional documentation. 7 | """ 8 | _resource_path = ['webhooks'] 9 | 10 | def list_webhooks(self, limit=25, **kwargs): 11 | """Lists all webhooks. 12 | 13 | Args: 14 | limit (int, optional): The number of subscriptions to fetch per request 15 | 16 | See Also: 17 | https://developers.dialpad.com/reference#webhookapi_listwebhooks 18 | """ 19 | return self.request(method='GET', data=dict(limit=limit, **kwargs)) 20 | 21 | def get_webhook(self, webhook_id): 22 | """Gets a specific webhook. 23 | 24 | Args: 25 | webhook_id (str, required): The ID of the webhook 26 | 27 | See Also: 28 | https://developers.dialpad.com/reference#webhookapi_getwebhook 29 | """ 30 | return self.request([webhook_id], method='GET') 31 | 32 | def create_webhook(self, hook_url, **kwargs): 33 | """Creates a new webhook. 34 | 35 | Args: 36 | hook_url (str, required): The URL which should be called when subscriptions fire 37 | secret (str, optional): A secret to use to encrypt subscription event payloads 38 | 39 | See Also: 40 | https://developers.dialpad.com/reference#webhookapi_createwebhook 41 | """ 42 | return self.request(method='POST', data=dict(hook_url=hook_url, **kwargs)) 43 | 44 | def update_webhook(self, webhook_id, **kwargs): 45 | """Updates a specific webhook 46 | 47 | Args: 48 | webhook_id (str, required): The ID of the webhook 49 | hook_url (str, optional): The URL which should be called when subscriptions fire 50 | secret (str, optional): A secret to use to encrypt subscription event payloads 51 | 52 | See Also: 53 | https://developers.dialpad.com/reference#webhookapi_updatewebhook 54 | """ 55 | return self.request([webhook_id], method='PATCH', data=kwargs) 56 | 57 | def delete_webhook(self, webhook_id): 58 | """Deletes a specific webhook. 59 | 60 | Args: 61 | webhook_id (str, required): The ID of the webhook 62 | 63 | See Also: 64 | https://developers.dialpad.com/reference#webhookapi_deletewebhook 65 | """ 66 | return self.request([webhook_id], method='DELETE') 67 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cached-property == 1.5.1 2 | certifi == 2020.6.20 3 | chardet == 3.0.4 4 | idna == 2.10 5 | requests == 2.24.0 6 | urllib3 == 1.25.10 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def readme(): 7 | with open('README.md') as f: 8 | return f.read() 9 | 10 | 11 | setup( 12 | name='python-dialpad', 13 | version='2.2.3', 14 | description='A python wrapper for the Dialpad REST API', 15 | long_description=readme(), 16 | long_description_content_type="text/markdown", 17 | author='Jake Nielsen', 18 | author_email='jnielsen@dialpad.com', 19 | license='MIT', 20 | url='https://github.com/dialpad/dialpad-python-sdk', 21 | install_requires=[ 22 | 'cached-property', 23 | 'certifi', 24 | 'chardet', 25 | 'idna', 26 | 'requests', 27 | 'urllib3' 28 | ], 29 | include_package_data=True, 30 | packages=find_packages() 31 | ) 32 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .utils import prepare_test_resources 3 | 4 | 5 | if __name__ == '__main__': 6 | prepare_test_resources() 7 | -------------------------------------------------------------------------------- /test/test_resource_sanity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Tests to automatically detect common issues with resource definitions. 4 | 5 | In particular these tests will look through the files in dialpad-python-sdk/dialpad/resources/ and 6 | ensure: 7 | 8 | - All subclasses of DialpadResource are exposed directly in resources/__init__.py 9 | - All resources are available as properties of DialpadClient 10 | - Public methods defined on the concrete subclasses only make web requests that agree with 11 | the Dialpad API's open-api spec 12 | """ 13 | 14 | import inspect 15 | import pkgutil 16 | import pytest 17 | 18 | from swagger_stub import swagger_stub 19 | 20 | from .utils import resource_filepath, prepare_test_resources 21 | 22 | from dialpad.client import DialpadClient 23 | from dialpad import resources 24 | from dialpad.resources.resource import DialpadResource 25 | 26 | 27 | # The "swagger_files_url" pytest fixture stubs out live requests with a schema validation check 28 | # against the Dialpad API swagger spec. 29 | 30 | # NOTE: Responses returned by the stub will not necessarily be a convincing dummy for the responses 31 | # returned by the live API, so some complex scenarios may not be possible to test using this 32 | # strategy. 33 | @pytest.fixture(scope='module') 34 | def swagger_files_url(): 35 | # Ensure that the spec is up-to-date first. 36 | prepare_test_resources() 37 | 38 | return [ 39 | (resource_filepath('swagger_spec.json'), 'https://dialpad.com'), 40 | ] 41 | 42 | 43 | class TestResourceSanity: 44 | """Sanity-tests for (largely) automatically validating new and existing client API methods. 45 | 46 | When new API resource methods are added to the library, examples of each method must be added to 47 | EX_METHOD_CALLS to allow the unit tests to call those methods and validate that the API requests 48 | they generate adhere to the swagger spec. 49 | 50 | The example calls should generally include as many keyword arguments as possible so that any 51 | potential mistakes in the parameter names, url path, and request body can be caught by the 52 | schema tests. 53 | 54 | Entries in the "EX_METHOD_CALLS" dictionary should be of the form: 55 | { 56 | '': { 57 | 'method_name': { 58 | 'arg_name': arg_value, 59 | 'other_arg_name': other_arg_value, 60 | }, 61 | 'other_method_name': etc... 62 | } 63 | } 64 | """ 65 | 66 | EX_METHOD_CALLS = { 67 | 'AppSettingsResource': { 68 | 'get': { 69 | 'target_id': '123', 70 | 'target_type': 'office', 71 | } 72 | }, 73 | 'BlockedNumberResource': { 74 | 'list': {}, 75 | 'block_numbers': { 76 | 'numbers': ['+12223334444'] 77 | }, 78 | 'unblock_numbers': { 79 | 'numbers': ['+12223334444'] 80 | }, 81 | 'get': { 82 | 'number': '+12223334444' 83 | }, 84 | }, 85 | 'CallResource': { 86 | 'get_info': { 87 | 'call_id': '123' 88 | }, 89 | 'initiate_call': { 90 | 'phone_number': '+12223334444', 91 | 'user_id': '123', 92 | 'group_id': '123', 93 | 'group_type': 'department', 94 | 'device_id': '123', 95 | 'custom_data': 'example custom data', 96 | } 97 | }, 98 | 'CallRouterResource': { 99 | 'list': { 100 | 'office_id': '123', 101 | }, 102 | 'get': { 103 | 'router_id': '123', 104 | }, 105 | 'create': { 106 | 'name': 'Test Router', 107 | 'routing_url': 'fakeurl.com/url', 108 | 'office_id': '123', 109 | 'default_target_id': '123', 110 | 'default_target_type': 'user', 111 | 'enabled': True, 112 | 'secret': '123', 113 | }, 114 | 'patch': { 115 | 'router_id': '123', 116 | 'name': 'Test Router', 117 | 'routing_url': 'fakeurl.com/url', 118 | 'office_id': '123', 119 | 'default_target_id': '123', 120 | 'default_target_type': 'user', 121 | 'enabled': True, 122 | 'secret': '123', 123 | }, 124 | 'delete': { 125 | 'router_id': '123', 126 | }, 127 | 'assign_number': { 128 | 'router_id': '123', 129 | 'area_code': '519', 130 | }, 131 | }, 132 | 'CallbackResource': { 133 | 'enqueue_callback': { 134 | 'call_center_id': '123', 135 | 'phone_number': '+12223334444', 136 | }, 137 | 'validate_callback': { 138 | 'call_center_id': '123', 139 | 'phone_number': '+12223334444', 140 | }, 141 | }, 142 | 'CallCenterResource': { 143 | 'get': { 144 | 'call_center_id': '123', 145 | }, 146 | 'get_operators': { 147 | 'call_center_id': '123', 148 | }, 149 | 'add_operator': { 150 | 'call_center_id': '123', 151 | 'user_id': '123', 152 | 'skill_level': '10', 153 | 'role': 'supervisor', 154 | 'license_type': 'lite_support_agents', 155 | 'keep_paid_numbers': False, 156 | }, 157 | 'remove_operator': { 158 | 'call_center_id': '123', 159 | 'user_id': '123', 160 | } 161 | }, 162 | 'CompanyResource': { 163 | 'get': {}, 164 | }, 165 | 'ContactResource': { 166 | 'list': { 167 | 'owner_id': '123', 168 | }, 169 | 'create': { 170 | 'first_name': 'Testiel', 171 | 'last_name': 'McTestersen', 172 | 'company_name': 'ABC', 173 | 'emails': ['tmtesten@test.com'], 174 | 'extension': '123', 175 | 'job_title': 'Eric the half-a-bee', 176 | 'owner_id': '123', 177 | 'phones': ['+12223334444'], 178 | 'trunk_group': '123', 179 | 'urls': ['test.com/about'], 180 | }, 181 | 'create_with_uid': { 182 | 'first_name': 'Testiel', 183 | 'last_name': 'McTestersen', 184 | 'uid': 'UUID-updownupdownleftrightab', 185 | 'company_name': 'ABC', 186 | 'emails': ['tmtesten@test.com'], 187 | 'extension': '123', 188 | 'job_title': 'Eric the half-a-bee', 189 | 'phones': ['+12223334444'], 190 | 'trunk_group': '123', 191 | 'urls': ['test.com/about'], 192 | }, 193 | 'delete': { 194 | 'contact_id': '123', 195 | }, 196 | 'get': { 197 | 'contact_id': '123', 198 | }, 199 | 'patch': { 200 | 'contact_id': '123', 201 | 'first_name': 'Testiel', 202 | 'last_name': 'McTestersen', 203 | 'company_name': 'ABC', 204 | 'emails': ['tmtesten@test.com'], 205 | 'extension': '123', 206 | 'job_title': 'Eric the half-a-bee', 207 | 'phones': ['+12223334444'], 208 | 'trunk_group': '123', 209 | 'urls': ['test.com/about'], 210 | }, 211 | }, 212 | 'DepartmentResource': { 213 | 'get': { 214 | 'department_id': '123', 215 | }, 216 | 'get_operators': { 217 | 'department_id': '123', 218 | }, 219 | 'add_operator': { 220 | 'department_id': '123', 221 | 'operator_id': '123', 222 | 'operator_type': 'room', 223 | 'role': 'operator', 224 | }, 225 | 'remove_operator': { 226 | 'department_id': '123', 227 | 'operator_id': '123', 228 | 'operator_type': 'room', 229 | }, 230 | }, 231 | 'EventSubscriptionResource': { 232 | 'list_call_event_subscriptions': { 233 | 'target_id': '123', 234 | 'target_type': 'room', 235 | }, 236 | 'get_call_event_subscription': { 237 | 'subscription_id': '123', 238 | }, 239 | 'put_call_event_subscription': { 240 | 'subscription_id': '123', 241 | 'url': 'test.com/subhook', 242 | 'secret': 'badsecret', 243 | 'enabled': True, 244 | 'group_calls_only': False, 245 | 'target_id': '123', 246 | 'target_type': 'office', 247 | 'call_states': ['connected', 'queued'], 248 | }, 249 | 'delete_call_event_subscription': { 250 | 'subscription_id': '123', 251 | }, 252 | 'list_sms_event_subscriptions': { 253 | 'target_id': '123', 254 | 'target_type': 'room', 255 | }, 256 | 'get_sms_event_subscription': { 257 | 'subscription_id': '123', 258 | }, 259 | 'put_sms_event_subscription': { 260 | 'subscription_id': '123', 261 | 'url': 'test.com/subhook', 262 | 'secret': 'badsecret', 263 | 'direction': 'outbound', 264 | 'enabled': True, 265 | 'target_id': '123', 266 | 'target_type': 'office', 267 | }, 268 | 'delete_sms_event_subscription': { 269 | 'subscription_id': '123', 270 | }, 271 | }, 272 | 'NumberResource': { 273 | 'list': { 274 | 'status': 'available', 275 | }, 276 | 'get': { 277 | 'number': '+12223334444', 278 | }, 279 | 'unassign': { 280 | 'number': '+12223334444', 281 | }, 282 | 'assign': { 283 | 'number': '+12223334444', 284 | 'target_id': '123', 285 | 'target_type': 'office', 286 | }, 287 | 'format': { 288 | 'country_code': 'gb', 289 | 'number': '020 3048 4377', 290 | }, 291 | }, 292 | 'OfficeResource': { 293 | 'list': {}, 294 | 'get': { 295 | 'office_id': '123', 296 | }, 297 | 'assign_number': { 298 | 'office_id': '123', 299 | 'number': '+12223334444', 300 | }, 301 | 'get_operators': { 302 | 'office_id': '123', 303 | }, 304 | 'unassign_number': { 305 | 'office_id': '123', 306 | 'number': '+12223334444', 307 | }, 308 | 'get_call_centers': { 309 | 'office_id': '123', 310 | }, 311 | 'get_departments': { 312 | 'office_id': '123', 313 | }, 314 | 'get_plan': { 315 | 'office_id': '123', 316 | }, 317 | 'update_licenses': { 318 | 'office_id': '123', 319 | 'fax_line_delta': '2', 320 | }, 321 | }, 322 | 'RoomResource': { 323 | 'list': { 324 | 'office_id': '123', 325 | }, 326 | 'create': { 327 | 'name': 'Where it happened', 328 | 'office_id': '123', 329 | }, 330 | 'generate_international_pin': { 331 | 'customer_ref': 'Burr, sir', 332 | }, 333 | 'delete': { 334 | 'room_id': '123', 335 | }, 336 | 'get': { 337 | 'room_id': '123', 338 | }, 339 | 'update': { 340 | 'room_id': '123', 341 | 'name': 'For the last tiiiime', 342 | 'phone_numbers': ['+12223334444'], 343 | }, 344 | 'assign_number': { 345 | 'room_id': '123', 346 | 'number': '+12223334444', 347 | }, 348 | 'unassign_number': { 349 | 'room_id': '123', 350 | 'number': '+12223334444', 351 | }, 352 | 'get_deskphones': { 353 | 'room_id': '123', 354 | }, 355 | 'create_deskphone': { 356 | 'room_id': '123', 357 | 'mac_address': 'Tim Cook', 358 | 'name': 'The red one.', 359 | 'phone_type': 'polycom', 360 | }, 361 | 'delete_deskphone': { 362 | 'room_id': '123', 363 | 'deskphone_id': '123', 364 | }, 365 | 'get_deskphone': { 366 | 'room_id': '123', 367 | 'deskphone_id': '123', 368 | }, 369 | }, 370 | 'SMSResource': { 371 | 'send_sms': { 372 | 'user_id': '123', 373 | 'to_numbers': ['+12223334444'], 374 | 'text': 'Itemized list to follow.', 375 | 'infer_country_code': False, 376 | 'sender_group_id': '123', 377 | 'sender_group_type': 'callcenter', 378 | }, 379 | }, 380 | 'StatsExportResource': { 381 | 'post': { 382 | 'coaching_group': False, 383 | 'days_ago_start': '1', 384 | 'days_ago_end': '2', 385 | 'is_today': False, 386 | 'export_type': 'records', 387 | 'stat_type': 'calls', 388 | 'office_id': '123', 389 | 'target_id': '123', 390 | 'target_type': 'callcenter', 391 | 'timezone': 'America/New_York', 392 | }, 393 | 'get': { 394 | 'export_id': '123', 395 | }, 396 | }, 397 | 'SubscriptionResource': { 398 | 'list_agent_status_event_subscriptions': {}, 399 | 'get_agent_status_event_subscription': { 400 | 'subscription_id': '123', 401 | }, 402 | 'create_agent_status_event_subscription': { 403 | 'agent_type': 'callcenter', 404 | 'enabled': True, 405 | 'webhook_id': '1000', 406 | }, 407 | 'update_agent_status_event_subscription': { 408 | 'subscription_id': '123', 409 | 'agent_type': 'callcenter', 410 | 'enabled': True, 411 | 'webhook_id': '1000', 412 | }, 413 | 'delete_agent_status_event_subscription': { 414 | 'subscription_id': '123', 415 | }, 416 | 'list_call_event_subscriptions': { 417 | 'target_id': '123', 418 | 'target_type': 'room', 419 | }, 420 | 'get_call_event_subscription': { 421 | 'subscription_id': '123', 422 | }, 423 | 'create_call_event_subscription': { 424 | 'enabled': True, 425 | 'group_calls_only': False, 426 | 'target_id': '123', 427 | 'target_type': 'office', 428 | 'call_states': ['connected', 'queued'], 429 | 'webhook_id': '1000', 430 | }, 431 | 'update_call_event_subscription': { 432 | 'subscription_id': '123', 433 | 'enabled': True, 434 | 'group_calls_only': False, 435 | 'target_id': '123', 436 | 'target_type': 'office', 437 | 'call_states': ['connected', 'queued'], 438 | 'webhook_id': '1000', 439 | }, 440 | 'delete_call_event_subscription': { 441 | 'subscription_id': '123', 442 | }, 443 | 'list_contact_event_subscriptions': {}, 444 | 'get_contact_event_subscription': { 445 | 'subscription_id': '123', 446 | }, 447 | 'create_contact_event_subscription': { 448 | 'contact_type': 'shared', 449 | 'enabled': True, 450 | 'webhook_id': '1000', 451 | }, 452 | 'update_contact_event_subscription': { 453 | 'subscription_id': '123', 454 | 'contact_type': 'shared', 455 | 'enabled': True, 456 | 'webhook_id': '1000', 457 | }, 458 | 'delete_contact_event_subscription': { 459 | 'subscription_id': '123', 460 | }, 461 | 'list_sms_event_subscriptions': { 462 | 'target_id': '123', 463 | 'target_type': 'room', 464 | }, 465 | 'get_sms_event_subscription': { 466 | 'subscription_id': '123', 467 | }, 468 | 'create_sms_event_subscription': { 469 | 'direction': 'outbound', 470 | 'enabled': True, 471 | 'target_id': '123', 472 | 'target_type': 'office', 473 | 'webhook_id': '1000', 474 | }, 475 | 'update_sms_event_subscription': { 476 | 'subscription_id': '123', 477 | 'direction': 'outbound', 478 | 'enabled': True, 479 | 'target_id': '123', 480 | 'target_type': 'office', 481 | 'webhook_id': '1000', 482 | }, 483 | 'delete_sms_event_subscription': { 484 | 'subscription_id': '123', 485 | }, 486 | }, 487 | 'TranscriptResource': { 488 | 'get': { 489 | 'call_id': '123', 490 | }, 491 | }, 492 | 'UserResource': { 493 | 'search': { 494 | 'query': 'test', 495 | 'cursor': 'iamacursor', 496 | }, 497 | 'list': { 498 | 'email': 'tmtesten@test.com', 499 | 'state': 'suspended', 500 | }, 501 | 'create': { 502 | 'email': 'tmtesten@test.com', 503 | 'office_id': '123', 504 | 'first_name': 'Testietta', 505 | 'last_name': 'McTestersen', 506 | 'license': 'lite_support_agents', 507 | }, 508 | 'delete': { 509 | 'user_id': '123', 510 | }, 511 | 'get': { 512 | 'user_id': '123', 513 | }, 514 | 'update': { 515 | 'user_id': '123', 516 | 'admin_office_ids': ['123'], 517 | 'emails': ['tmtesten@test.com'], 518 | 'extension': '123', 519 | 'first_name': 'Testietta', 520 | 'last_name': 'McTestersen', 521 | 'forwarding_numbers': ['+12223334444'], 522 | 'is_super_admin': True, 523 | 'job_title': 'Administraterar', 524 | 'license': 'lite_lines', 525 | 'office_id': '123', 526 | 'phone_numbers': ['+12223334444'], 527 | 'state': 'active', 528 | }, 529 | 'toggle_call_recording': { 530 | 'user_id': '123', 531 | 'is_recording': False, 532 | 'play_message': True, 533 | 'recording_type': 'group', 534 | }, 535 | 'assign_number': { 536 | 'user_id': '123', 537 | 'number': '+12223334444', 538 | }, 539 | 'initiate_call': { 540 | 'user_id': '123', 541 | 'phone_number': '+12223334444', 542 | 'custom_data': 'Y u call self?', 543 | 'group_id': '123', 544 | 'group_type': 'department', 545 | 'outbound_caller_id': 'O.0', 546 | }, 547 | 'unassign_number': { 548 | 'user_id': '123', 549 | 'number': '+12223334444', 550 | }, 551 | 'get_deskphones': { 552 | 'user_id': '123', 553 | }, 554 | 'create_deskphone': { 555 | 'user_id': '123', 556 | 'mac_address': 'Tim Cook', 557 | 'name': 'The red one.', 558 | 'phone_type': 'polycom', 559 | }, 560 | 'delete_deskphone': { 561 | 'user_id': '123', 562 | 'deskphone_id': '123', 563 | }, 564 | 'get_deskphone': { 565 | 'user_id': '123', 566 | 'deskphone_id': '123', 567 | }, 568 | 'get_personas': { 569 | 'user_id': '123', 570 | }, 571 | 'toggle_do_not_disturb': { 572 | 'user_id': '123', 573 | 'do_not_disturb': True, 574 | }, 575 | }, 576 | 'UserDeviceResource': { 577 | 'get': { 578 | 'device_id': '123', 579 | }, 580 | 'list': { 581 | 'user_id': '123', 582 | }, 583 | }, 584 | 'WebhookResource': { 585 | 'list_webhooks': {}, 586 | 'get_webhook': { 587 | 'webhook_id': '123', 588 | }, 589 | 'create_webhook': { 590 | 'hook_url': 'https://test.com/subhook', 591 | 'secret': 'badsecret', 592 | }, 593 | 'update_webhook': { 594 | 'webhook_id': '123', 595 | 'hook_url': 'https://test.com/subhook', 596 | 'secret': 'badsecret', 597 | }, 598 | 'delete_webhook': { 599 | 'webhook_id': '123', 600 | }, 601 | }, 602 | } 603 | 604 | def get_method_example_kwargs(self, resource_instance, resource_method): 605 | """Returns the appropriate kwargs to use when sanity-checking API resource methods.""" 606 | class_msg = 'DialpadResource subclass "%s" must have an entry in EX_METHOD_CALLS' 607 | 608 | class_name = resource_instance.__class__.__name__ 609 | assert class_name in self.EX_METHOD_CALLS, class_msg % class_name 610 | 611 | method_msg = 'Method "%s.%s" must have an entry in EX_METHOD_CALLS' 612 | method_name = resource_method.__name__ 613 | assert method_name in self.EX_METHOD_CALLS[class_name], method_msg % (class_name, method_name) 614 | 615 | return self.EX_METHOD_CALLS[class_name][method_name] 616 | 617 | def _get_resource_submodule_names(self): 618 | """Returns an iterator of python modules that exist in the dialpad/resources directory.""" 619 | for importer, modname, ispkg in pkgutil.iter_modules(resources.__path__): 620 | if modname == 'resource': 621 | continue 622 | 623 | if ispkg: 624 | continue 625 | 626 | yield modname 627 | 628 | def _get_resource_submodules(self): 629 | """Returns an iterator of python modules that are exposed via from dialpad.resources import *""" 630 | for modname in self._get_resource_submodule_names(): 631 | if hasattr(resources, modname): 632 | yield getattr(resources, modname) 633 | 634 | def _get_resource_classes(self): 635 | """Returns an iterator of DialpadResource subclasses that are exposed under dialpad.resources""" 636 | for mod in self._get_resource_submodules(): 637 | for k, v in mod.__dict__.items(): 638 | if not inspect.isclass(v): 639 | continue 640 | 641 | if not issubclass(v, DialpadResource): 642 | continue 643 | 644 | if v == DialpadResource: 645 | continue 646 | 647 | yield v 648 | 649 | def test_resources_properly_imported(self): 650 | """Verifies that all modules definied in the resources directory are properly exposed under 651 | dialpad.resources. 652 | """ 653 | exposed_resources = dir(resources) 654 | 655 | msg = '"%s" module is present in the resources directory, but is not imported in ' \ 656 | 'resources/__init__.py' 657 | 658 | for modname in self._get_resource_submodule_names(): 659 | assert modname in exposed_resources, msg % modname 660 | 661 | def test_resource_classes_properly_exposed(self): 662 | """Verifies that all subclasses of DialpadResource that are defined in the resources directory 663 | are also exposed as direct members of the resources module. 664 | """ 665 | exposed_resources = dir(resources) 666 | 667 | msg = '"%(name)s" resource class is present in the resources package, but is not exposed ' \ 668 | 'directly as resources.%(name)s via resources/__init__.py' 669 | 670 | for c in self._get_resource_classes(): 671 | assert c.__name__ in exposed_resources, msg % {'name': c.__name__} 672 | 673 | def test_request_conformance(self, swagger_stub): 674 | """Verifies that all API requests produced by this library conform to the swagger spec. 675 | 676 | Although this test cannot guarantee that the requests are semantically correct, it can at least 677 | determine whether they are schematically correct. 678 | 679 | This test will also fail if there are no test-kwargs defined in EX_METHOD_CALLS for any public 680 | method implemented by a subclass of DialpadResource. 681 | """ 682 | 683 | # Construct a DialpadClient with a fake API key. 684 | dp = DialpadClient('123') 685 | 686 | # Iterate through the attributes on the client object to find the API resource accessors. 687 | for a in dir(dp): 688 | resource_instance = getattr(dp, a) 689 | 690 | # Skip any attributes that are not DialpadResources 691 | if not isinstance(resource_instance, DialpadResource): 692 | continue 693 | 694 | print('\nVerifying request format of %s methods' % 695 | resource_instance.__class__.__name__) 696 | 697 | # Iterate through the attributes on the resource instance. 698 | for method_attr in dir(resource_instance): 699 | # Skip private attributes. 700 | if method_attr.startswith('_'): 701 | continue 702 | 703 | # Skip attributes that are not unique to this particular subclass of DialpadResource. 704 | if hasattr(DialpadResource, method_attr): 705 | continue 706 | 707 | # Skip attributes that are not functions. 708 | resource_method = getattr(resource_instance, method_attr) 709 | if not callable(resource_method): 710 | continue 711 | 712 | # Skip attributes that are not instance methods. 713 | arg_names = inspect.getargspec(resource_method).args 714 | if not arg_names or arg_names[0] != 'self': 715 | continue 716 | 717 | # Fetch example kwargs to test the method (and raise if they haven't been provided). 718 | method_kwargs = self.get_method_example_kwargs(resource_instance, resource_method) 719 | 720 | # Call the method, and allow the swagger mock to raise an exception if it encounters a 721 | # schema error. 722 | print('Testing %s with kwargs: %s' % (method_attr, method_kwargs)) 723 | resource_method(**method_kwargs) 724 | -------------------------------------------------------------------------------- /test/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import requests 4 | 5 | 6 | RESOURCE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), '.resources') 7 | 8 | 9 | def resource_filepath(filename): 10 | """Returns a path to the given file name in the test resources directory.""" 11 | return os.path.join(RESOURCE_PATH, filename) 12 | 13 | 14 | def prepare_test_resources(): 15 | """Prepares any resources that are expected to be available at test-time.""" 16 | 17 | if not os.path.exists(RESOURCE_PATH): 18 | os.mkdir(RESOURCE_PATH) 19 | 20 | # Generate the Dialpad API swagger spec, and write it to a file for easy access. 21 | with open(resource_filepath('swagger_spec.json'), 'w') as f: 22 | json.dump(_generate_swagger_spec(), f) 23 | 24 | 25 | def _generate_swagger_spec(): 26 | """Downloads current Dialpad API swagger spec and returns it as a dict.""" 27 | 28 | # Unfortunately, a little bit of massaging is needed to appease the swagger parser. 29 | def _hotpatch_spec_piece(piece): 30 | if 'type' in piece: 31 | if piece['type'] == 'string' and piece.get('format') == 'int64' and 'default' in piece: 32 | piece['default'] = str(piece['default']) 33 | 34 | if 'operationId' in piece and 'parameters' in piece: 35 | for sub_p in piece['parameters']: 36 | sub_p['required'] = sub_p.get('required', False) 37 | 38 | if 'basePath' in piece: 39 | del piece['basePath'] 40 | 41 | def _hotpatch_spec(spec): 42 | if isinstance(spec, dict): 43 | _hotpatch_spec_piece(spec) 44 | for k, v in spec.items(): 45 | _hotpatch_spec(v) 46 | 47 | elif isinstance(spec, list): 48 | for v in spec: 49 | _hotpatch_spec(v) 50 | 51 | return spec 52 | 53 | # Download the spec from dialpad.com. 54 | spec_json = requests.get('https://dialpad.com/static/openapi/apiv2openapi-en.json').json() 55 | 56 | # Return a patched version that will satisfy the swagger lib. 57 | return _hotpatch_spec(spec_json) 58 | -------------------------------------------------------------------------------- /tools/create_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOC="Build and release a new version of the python-dialpad package. 4 | 5 | Usage: 6 | create_version.sh [-h] [--patch|--minor|--major] [--bump-only|--no-upload] 7 | 8 | By default, this script will: 9 | 1 - Run the tests 10 | 2 - Check that we're on master 11 | 3 - Bump the patch-number in setup.py 12 | 4 - Check whether there are any remote changes that haven't been pulled 13 | 5 - Build the distribution package 14 | 6 - Verify the package integrity 15 | 7 - Commit the patch-number bump 16 | 8 - Tag the release 17 | 9 - Upload the package to PyPI 18 | 10 - Push the commit and tag to github 19 | 20 | If anything fails along the way, the script will bail out. 21 | 22 | Options: 23 | -h --help Show this message 24 | --patch Bump the patch version number (default) 25 | --minor Bump the minor version number 26 | --major Bump the major version number 27 | --bump-only Make the appropriate version bump, but don't do anything else 28 | (i.e. stop after performing step 3) 29 | --no-upload Do everything other than uploading the package to PyPI 30 | (i.e. skip step 9) 31 | " 32 | # docopt parser below, refresh this parser with `docopt.sh create_release.sh` 33 | # shellcheck disable=2016,1075 34 | docopt() { parse() { if ${DOCOPT_DOC_CHECK:-true}; then local doc_hash 35 | doc_hash=$(printf "%s" "$DOC" | shasum -a 256) 36 | if [[ ${doc_hash:0:5} != "$digest" ]]; then 37 | stderr "The current usage doc (${doc_hash:0:5}) does not match \ 38 | what the parser was generated with (${digest}) 39 | Run \`docopt.sh\` to refresh the parser."; _return 70; fi; fi; local root_idx=$1 40 | shift; argv=("$@"); parsed_params=(); parsed_values=(); left=(); testdepth=0 41 | local arg; while [[ ${#argv[@]} -gt 0 ]]; do if [[ ${argv[0]} = "--" ]]; then 42 | for arg in "${argv[@]}"; do parsed_params+=('a'); parsed_values+=("$arg"); done 43 | break; elif [[ ${argv[0]} = --* ]]; then parse_long 44 | elif [[ ${argv[0]} = -* && ${argv[0]} != "-" ]]; then parse_shorts 45 | elif ${DOCOPT_OPTIONS_FIRST:-false}; then for arg in "${argv[@]}"; do 46 | parsed_params+=('a'); parsed_values+=("$arg"); done; break; else 47 | parsed_params+=('a'); parsed_values+=("${argv[0]}"); argv=("${argv[@]:1}"); fi 48 | done; local idx; if ${DOCOPT_ADD_HELP:-true}; then 49 | for idx in "${parsed_params[@]}"; do [[ $idx = 'a' ]] && continue 50 | if [[ ${shorts[$idx]} = "-h" || ${longs[$idx]} = "--help" ]]; then 51 | stdout "$trimmed_doc"; _return 0; fi; done; fi 52 | if [[ ${DOCOPT_PROGRAM_VERSION:-false} != 'false' ]]; then 53 | for idx in "${parsed_params[@]}"; do [[ $idx = 'a' ]] && continue 54 | if [[ ${longs[$idx]} = "--version" ]]; then stdout "$DOCOPT_PROGRAM_VERSION" 55 | _return 0; fi; done; fi; local i=0; while [[ $i -lt ${#parsed_params[@]} ]]; do 56 | left+=("$i"); ((i++)) || true; done 57 | if ! required "$root_idx" || [ ${#left[@]} -gt 0 ]; then error; fi; return 0; } 58 | parse_shorts() { local token=${argv[0]}; local value; argv=("${argv[@]:1}") 59 | [[ $token = -* && $token != --* ]] || _return 88; local remaining=${token#-} 60 | while [[ -n $remaining ]]; do local short="-${remaining:0:1}" 61 | remaining="${remaining:1}"; local i=0; local similar=(); local match=false 62 | for o in "${shorts[@]}"; do if [[ $o = "$short" ]]; then similar+=("$short") 63 | [[ $match = false ]] && match=$i; fi; ((i++)) || true; done 64 | if [[ ${#similar[@]} -gt 1 ]]; then 65 | error "${short} is specified ambiguously ${#similar[@]} times" 66 | elif [[ ${#similar[@]} -lt 1 ]]; then match=${#shorts[@]}; value=true 67 | shorts+=("$short"); longs+=(''); argcounts+=(0); else value=false 68 | if [[ ${argcounts[$match]} -ne 0 ]]; then if [[ $remaining = '' ]]; then 69 | if [[ ${#argv[@]} -eq 0 || ${argv[0]} = '--' ]]; then 70 | error "${short} requires argument"; fi; value=${argv[0]}; argv=("${argv[@]:1}") 71 | else value=$remaining; remaining=''; fi; fi; if [[ $value = false ]]; then 72 | value=true; fi; fi; parsed_params+=("$match"); parsed_values+=("$value"); done 73 | }; parse_long() { local token=${argv[0]}; local long=${token%%=*} 74 | local value=${token#*=}; local argcount; argv=("${argv[@]:1}") 75 | [[ $token = --* ]] || _return 88; if [[ $token = *=* ]]; then eq='='; else eq='' 76 | value=false; fi; local i=0; local similar=(); local match=false 77 | for o in "${longs[@]}"; do if [[ $o = "$long" ]]; then similar+=("$long") 78 | [[ $match = false ]] && match=$i; fi; ((i++)) || true; done 79 | if [[ $match = false ]]; then i=0; for o in "${longs[@]}"; do 80 | if [[ $o = $long* ]]; then similar+=("$long"); [[ $match = false ]] && match=$i 81 | fi; ((i++)) || true; done; fi; if [[ ${#similar[@]} -gt 1 ]]; then 82 | error "${long} is not a unique prefix: ${similar[*]}?" 83 | elif [[ ${#similar[@]} -lt 1 ]]; then 84 | [[ $eq = '=' ]] && argcount=1 || argcount=0; match=${#shorts[@]} 85 | [[ $argcount -eq 0 ]] && value=true; shorts+=(''); longs+=("$long") 86 | argcounts+=("$argcount"); else if [[ ${argcounts[$match]} -eq 0 ]]; then 87 | if [[ $value != false ]]; then 88 | error "${longs[$match]} must not have an argument"; fi 89 | elif [[ $value = false ]]; then 90 | if [[ ${#argv[@]} -eq 0 || ${argv[0]} = '--' ]]; then 91 | error "${long} requires argument"; fi; value=${argv[0]}; argv=("${argv[@]:1}") 92 | fi; if [[ $value = false ]]; then value=true; fi; fi; parsed_params+=("$match") 93 | parsed_values+=("$value"); }; required() { local initial_left=("${left[@]}") 94 | local node_idx; ((testdepth++)) || true; for node_idx in "$@"; do 95 | if ! "node_$node_idx"; then left=("${initial_left[@]}"); ((testdepth--)) || true 96 | return 1; fi; done; if [[ $((--testdepth)) -eq 0 ]]; then 97 | left=("${initial_left[@]}"); for node_idx in "$@"; do "node_$node_idx"; done; fi 98 | return 0; }; either() { local initial_left=("${left[@]}"); local best_match_idx 99 | local match_count; local node_idx; ((testdepth++)) || true 100 | for node_idx in "$@"; do if "node_$node_idx"; then 101 | if [[ -z $match_count || ${#left[@]} -lt $match_count ]]; then 102 | best_match_idx=$node_idx; match_count=${#left[@]}; fi; fi 103 | left=("${initial_left[@]}"); done; ((testdepth--)) || true 104 | if [[ -n $best_match_idx ]]; then "node_$best_match_idx"; return 0; fi 105 | left=("${initial_left[@]}"); return 1; }; optional() { local node_idx 106 | for node_idx in "$@"; do "node_$node_idx"; done; return 0; }; switch() { local i 107 | for i in "${!left[@]}"; do local l=${left[$i]} 108 | if [[ ${parsed_params[$l]} = "$2" ]]; then 109 | left=("${left[@]:0:$i}" "${left[@]:((i+1))}") 110 | [[ $testdepth -gt 0 ]] && return 0; if [[ $3 = true ]]; then 111 | eval "((var_$1++))" || true; else eval "var_$1=true"; fi; return 0; fi; done 112 | return 1; }; stdout() { printf -- "cat <<'EOM'\n%s\nEOM\n" "$1"; }; stderr() { 113 | printf -- "cat <<'EOM' >&2\n%s\nEOM\n" "$1"; }; error() { 114 | [[ -n $1 ]] && stderr "$1"; stderr "$usage"; _return 1; }; _return() { 115 | printf -- "exit %d\n" "$1"; exit "$1"; }; set -e; trimmed_doc=${DOC:0:1027} 116 | usage=${DOC:64:83}; digest=b986d; shorts=(-h '' '' '' '' '') 117 | longs=(--help --patch --minor --major --bump-only --no-upload) 118 | argcounts=(0 0 0 0 0 0); node_0(){ switch __help 0; }; node_1(){ 119 | switch __patch 1; }; node_2(){ switch __minor 2; }; node_3(){ switch __major 3 120 | }; node_4(){ switch __bump_only 4; }; node_5(){ switch __no_upload 5; } 121 | node_6(){ optional 0; }; node_7(){ either 1 2 3; }; node_8(){ optional 7; } 122 | node_9(){ either 4 5; }; node_10(){ optional 9; }; node_11(){ required 6 8 10; } 123 | node_12(){ required 11; }; cat <<<' docopt_exit() { 124 | [[ -n $1 ]] && printf "%s\n" "$1" >&2; printf "%s\n" "${DOC:64:83}" >&2; exit 1 125 | }'; unset var___help var___patch var___minor var___major var___bump_only \ 126 | var___no_upload; parse 12 "$@"; local prefix=${DOCOPT_PREFIX:-''} 127 | local docopt_decl=1; [[ $BASH_VERSION =~ ^4.3 ]] && docopt_decl=2 128 | unset "${prefix}__help" "${prefix}__patch" "${prefix}__minor" \ 129 | "${prefix}__major" "${prefix}__bump_only" "${prefix}__no_upload" 130 | eval "${prefix}"'__help=${var___help:-false}' 131 | eval "${prefix}"'__patch=${var___patch:-false}' 132 | eval "${prefix}"'__minor=${var___minor:-false}' 133 | eval "${prefix}"'__major=${var___major:-false}' 134 | eval "${prefix}"'__bump_only=${var___bump_only:-false}' 135 | eval "${prefix}"'__no_upload=${var___no_upload:-false}'; local docopt_i=0 136 | for ((docopt_i=0;docopt_i /dev/null 159 | exit 1 160 | } 161 | 162 | confirm() { 163 | if [ -z "$*" ]; then 164 | read -p "Shall we proceed? (y/N)" -r confirmation 165 | else 166 | read -p "$*" -r confirmation 167 | fi 168 | if [[ ! $confirmation =~ ^[Yy]$ ]]; then 169 | bail_out 170 | fi 171 | echo 172 | } 173 | 174 | REPO_DIR=`dirname "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"` 175 | 176 | pushd $REPO_DIR &> /dev/null 177 | 178 | # Do a safety-confirmation if the user is about to do something that isn't trivial to undo. 179 | if [ $__bump_only == "false" ]; then 180 | if [ $__no_upload == "true" ]; then 181 | echo "You're about to build and push a new ($VERSION_PART) release to Github" 182 | echo "(Although we won't upload the package to PyPI)" 183 | else 184 | echo "You're about to build and push a new ($VERSION_PART) release to Github AND PyPI" 185 | fi 186 | confirm "Are you sure that's what you want to do? (y/N)" 187 | fi 188 | 189 | # Do some sanity checks to make sure we're in a sufficient state to actually do what the user wants. 190 | 191 | # If we're planning to do more than just bump the version, then make sure "twine" is installed. 192 | if [[ $__bump_only == "false" ]]; then 193 | if ! command -v twine &> /dev/null; then 194 | echo "You must install twine (pip install twine) if you want to upload to PyPI" 195 | bail_out 196 | fi 197 | fi 198 | 199 | # Make sure we're on master (but let the user proceed if they reeeeally want to). 200 | branch_name=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') 201 | if [ "$branch_name" != "master" ]; then 202 | echo "We probably shouldn't be bumping the version number if we're not on the master branch." 203 | confirm "Are you this is want you want? (y/N)" 204 | fi 205 | 206 | # Run the unit tests and make sure they're passing. 207 | test_failure_prompt="Are you *entirely* sure you want to release a build with failing tests? (y/N)" 208 | tox || confirm "There are failing tests. $test_failure_prompt" 209 | 210 | # If we're *only* bumping the version, then we're safe to proceed at this point. 211 | if [ $__bump_only == "true" ]; then 212 | tox -e bump $VERSION_PART 213 | exit 214 | fi 215 | 216 | # In any other scenario, we should make sure the working directory is clean, and that we're 217 | # up-to-date with origin/master 218 | if ! git pull origin master --dry-run -v 2>&1 | grep "origin/master" | grep "up to date" &> /dev/null; then 219 | echo "There are changes that you need to pull on master." 220 | bail_out 221 | fi 222 | 223 | # We'll let bump2version handle the dirty-working-directory scenario. 224 | tox -e bump $VERSION_PART || bail_out 225 | 226 | # Now we need to build the package, so let's clear away any junk that might be lying around. 227 | rm -rf ./dist &> /dev/null 228 | rm -rf ./build &> /dev/null 229 | 230 | # The build stdout is a bit noisy, but stderr will be helpful if there's an error. 231 | tox -e build > /dev/null || bail_out 232 | 233 | # Make sure there aren't any issues with the package. 234 | twine check dist/* || bail_out 235 | 236 | # Upload the package if that's desirable. 237 | if [ $__no_upload == "false" ]; then 238 | twine upload dist/* || bail_out 239 | fi 240 | 241 | # Finally, commit the changes, tag the commit, and push. 242 | git add . 243 | new_version=`cat .bumpversion.cfg | grep "current_version = " | sed "s/current_version = //g"` 244 | git commit -m "Release version $new_version" 245 | git tag -a "v$new_version" -m "Release version $new_version" 246 | 247 | git push origin master 248 | git push origin "v$new_version" 249 | 250 | echo "Congrats!" 251 | if [ $__no_upload == "true" ]; then 252 | echo "The $new_version release commit has been pushed to GitHub, and tagged as \"v$new_version\"" 253 | else 254 | echo "The $new_version release is now live on PyPI, and tagged as \"v$new_version\" on GitHub" 255 | fi 256 | 257 | popd &> /dev/null 258 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py2, py3 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir} 7 | 8 | deps = 9 | -rrequirements.txt 10 | -rdev_requirements.txt 11 | commands = 12 | python -m pytest --cov dialpad --disable-warnings 13 | 14 | [testenv:py3] 15 | basepython = python3 16 | 17 | [testenv:build] 18 | commands = 19 | python ./setup.py sdist bdist_wheel 20 | 21 | [testenv:bump] 22 | commands = 23 | python -m bumpversion --allow-dirty {posargs} 24 | 25 | --------------------------------------------------------------------------------