diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..0e3bc29
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[report]
+exclude_lines =
+ def __repr__
+ raise AssertionError
+ raise NotImplementedError
+ if __name__ == .__main__.:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9b4003e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.egg*
+*.pyc
+.coverage
+cover/
+dist/
+nosetests.xml
+build
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..a7c382e
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1 @@
+workspace.xml
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 0000000..2344e21
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+ast-ari-py
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..217af47
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..3572571
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..e206d70
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..be728ed
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..3b31283
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..db52d54
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..61fa462
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/.idea/other.xml b/.idea/other.xml
new file mode 100644
index 0000000..a7f0eb1
--- /dev/null
+++ b/.idea/other.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/.idea/runConfigurations/Unit_Tests.xml b/.idea/runConfigurations/Unit_Tests.xml
new file mode 100644
index 0000000..e5b5504
--- /dev/null
+++ b/.idea/runConfigurations/Unit_Tests.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/Unit_Tests__coverage_.xml b/.idea/runConfigurations/Unit_Tests__coverage_.xml
new file mode 100644
index 0000000..c51e83f
--- /dev/null
+++ b/.idea/runConfigurations/Unit_Tests__coverage_.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml
new file mode 100644
index 0000000..922003b
--- /dev/null
+++ b/.idea/scopes/scope_settings.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/testrunner.xml b/.idea/testrunner.xml
new file mode 100644
index 0000000..41ab8a6
--- /dev/null
+++ b/.idea/testrunner.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..275077f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..b6fc195
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,34 @@
+Copyright (c) 2013, Digium, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+ The name of Digium, Inc., or the name of any Contributor,
+ may not be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS NOT FAULT TOLERANT AND SHOULD NOT BE USED IN ANY
+SITUATION ENDANGERING HUMAN LIFE OR PROPERTY.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..926cc7b
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst
+include LICENSE.txt
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..76cba90
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,5 @@
+About
+-----
+
+This package contains the Python client library for the Asterisk REST
+Interface.
diff --git a/ari/__init__.py b/ari/__init__.py
new file mode 100644
index 0000000..56c6a87
--- /dev/null
+++ b/ari/__init__.py
@@ -0,0 +1,26 @@
+#
+# Copyright (c) 2013, Digium, Inc.
+#
+
+"""ARI client library
+"""
+
+import client
+import swaggerpy.http_client
+import urlparse
+
+Client = client.Client
+
+
+def connect(base_url, username, password):
+ """Helper method for easily connecting to ARI.
+
+ :param base_url: Base URL for Asterisk HTTP server (http://localhost:8088/)
+ :param username: ARI username
+ :param password: ARI password.
+ :return:
+ """
+ split = urlparse.urlsplit(base_url)
+ http_client = swaggerpy.http_client.SynchronousHttpClient()
+ http_client.set_basic_auth(split.hostname, username, password)
+ return Client(base_url, http_client)
diff --git a/ari/client.py b/ari/client.py
new file mode 100644
index 0000000..72a6fd1
--- /dev/null
+++ b/ari/client.py
@@ -0,0 +1,196 @@
+#
+# Copyright (c) 2013, Digium, Inc.
+#
+
+"""ARI client library.
+"""
+
+import json
+import logging
+import urlparse
+import swaggerpy.client
+
+from ari.model import *
+
+log = logging.getLogger(__name__)
+
+
+class Client(object):
+ """ARI Client object.
+
+ :param base_url: Base URL for accessing Asterisk.
+ :param http_client: HTTP client interface.
+ """
+
+ def __init__(self, base_url, http_client):
+ url = urlparse.urljoin(base_url, "ari/api-docs/resources.json")
+
+ self.swagger = swaggerpy.client.SwaggerClient(
+ url, http_client=http_client)
+ self.repositories = {
+ name: Repository(self, name, api)
+ for (name, api) in self.swagger.resources.items()}
+
+ # Extract models out of the events resource
+ events = [api['api_declaration']
+ for api in self.swagger.api_docs['apis']
+ if api['name'] == 'events']
+ if events:
+ self.event_models = events[0]['models']
+ else:
+ self.event_models = {}
+
+ self.event_listeners = {}
+ self.global_listeners = []
+
+ def __getattr__(self, item):
+ """Exposes repositories as fields of the client.
+
+ :param item: Field name
+ """
+ repo = self.get_repo(item)
+ if not repo:
+ raise AttributeError(
+ "'%r' object has no attribute '%s'" % (self, item))
+ return repo
+
+ def get_repo(self, name):
+ """Get a specific repo by name.
+
+ :param name: Name of the repo to get
+ :return: Repository, or None if not found.
+ :rtype: ari.model.Repository
+ """
+ return self.repositories.get(name)
+
+ def run(self, apps):
+ """Connect to the WebSocket and begin processing messages.
+
+ This method will block until all messages have been received from the
+ WebSocket.
+
+ :param apps: Application (or list of applications) to connect for
+ :type apps: str or list of str
+ """
+ if isinstance(apps, list):
+ apps = ','.join(apps)
+ ws = self.swagger.events.eventWebsocket(app=apps)
+ # TypeChecker false positive on iter(callable, sentinel) -> iterator
+ # Fixed in plugin v3.0.1
+ # noinspection PyTypeChecker
+ for msg_str in iter(lambda: ws.recv(), None):
+ msg_json = json.loads(msg_str)
+ if not isinstance(msg_json, dict) or 'type' not in msg_json:
+ log.error("Invalid event: %s" % msg_str)
+ continue
+
+ listeners = self.global_listeners + self.event_listeners.get(
+ msg_json['type'], [])
+ for listener in listeners:
+ # noinspection PyBroadException
+ try:
+ listener(msg_json)
+ except Exception:
+ log.exception("Event listener threw exception")
+
+ def on_event(self, event_type, event_cb):
+ """Register callback for events with given type.
+
+ :param event_type: String name of the event to register for.
+ :param event_cb: Callback function
+ :type event_cb: (dict) -> None
+ """
+ listeners = self.event_listeners.get(event_type)
+ if listeners is None:
+ listeners = []
+ self.event_listeners[event_type] = listeners
+ listeners.append(event_cb)
+
+ def on_object_event(self, event_type, event_cb, factory_fn, model_id):
+ """Register callback for events with the given type. Event fields of
+ the given model_id type are passed along to event_cb.
+
+ If multiple fields of the event have the type model_id, a dict is
+ passed mapping the field name to the model object.
+
+ :param event_type: String name of the event to register for.
+ :param event_cb: Callback function
+ :type event_cb: (Obj, dict) -> None or (dict[str, Obj], dict) ->
+ :param factory_fn: Function for creating Obj from JSON
+ :param model_id: String id for Obj from Swagger models.
+ """
+ # Find the associated model from the Swagger declaration
+ event_model = self.event_models.get(event_type)
+ if not event_model:
+ raise ValueError("Cannot find event model '%s'" % event_type)
+
+ # Extract the fields that are of the expected type
+ obj_fields = [k for (k, v) in event_model['properties'].items()
+ if v['type'] == model_id]
+ if not obj_fields:
+ raise ValueError("Event model '%s' has no fields of type %s"
+ % (event_type, model_id))
+
+ def extract_objects(event):
+ """Extract objects of a given type from an event.
+
+ :param event: Event
+ """
+ # Extract the fields which are of the expected type
+ obj = {obj_field: factory_fn(self, event[obj_field])
+ for obj_field in obj_fields
+ if event.get(obj_field)}
+ # If there's only one field in the schema, just pass that along
+ if len(obj_fields) == 1:
+ if obj:
+ obj = obj.values()[0]
+ else:
+ obj = None
+ event_cb(obj, event)
+
+ self.on_event(event_type, extract_objects)
+
+ def on_channel_event(self, event_type, fn):
+ """Register callback for Channel related events
+
+ :param event_type: String name of the event to register for.
+ :param fn: Callback function
+ :type fn: (Channel, dict) -> None or (list[Channel], dict) -> None
+ """
+ return self.on_object_event(event_type, fn, Channel, 'Channel')
+
+ def on_bridge_event(self, event_type, fn):
+ """Register callback for Bridge related events
+
+ :param event_type: String name of the event to register for.
+ :param fn: Callback function
+ :type fn: (Bridge, dict) -> None or (list[Bridge], dict) -> None
+ """
+ return self.on_object_event(event_type, fn, Bridge, 'Bridge')
+
+ def on_playback_event(self, event_type, fn):
+ """Register callback for Playback related events
+
+ :param event_type: String name of the event to register for.
+ :param fn: Callback function
+ :type fn: (Playback, dict) -> None or (list[Playback], dict) -> None
+ """
+ return self.on_object_event(event_type, fn, Playback, 'Playback')
+
+ def on_endpoint_event(self, event_type, fn):
+ """Register callback for Endpoint related events
+
+ :param event_type: String name of the event to register for.
+ :param fn: Callback function
+ :type fn: (Endpoint, dict) -> None or (list[Endpoint], dict) -> None
+ """
+ return self.on_object_event(event_type, fn, Endpoint, 'Endpoint')
+
+ def on_sound_event(self, event_type, fn):
+ """Register callback for Sound related events
+
+ :param event_type: String name of the event to register for.
+ :param fn: Sound function
+ :type fn: (Sound, dict) -> None or (list[Sound], dict) -> None
+ """
+ return self.on_object_event(event_type, fn, Sound, 'Sound')
diff --git a/ari/model.py b/ari/model.py
new file mode 100644
index 0000000..4717496
--- /dev/null
+++ b/ari/model.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python
+
+"""Model for mapping ARI Swagger resources and operations into objects.
+
+The API is modeled into the Repository pattern, as you would find in Domain
+Driven Design.
+
+Each Swagger Resource (a.k.a. API declaration) is mapped into a Repository
+object, which has the non-instance specific operations (just like what you
+would find in a repository object).
+
+Responses from operations are mapped into first-class objects, which themselves
+have methods which map to instance specific operations (just like what you
+would find in a domain object).
+
+The first-class objects also have 'on_event' methods, which can subscribe to
+Stasis events relating to that object.
+"""
+
+import re
+import requests
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class Repository(object):
+ """ARI repository.
+
+ This repository maps to an ARI Swagger resource. The operations on the
+ Swagger resource are mapped to methods on this object, using the
+ operation's nickname.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param name: Repository name. Maps to the basename of the resource's
+ .json file
+ :param resource: Associated Swagger resource.
+ :type resource: swaggerpy.client.Resource
+ """
+
+ def __init__(self, client, name, resource):
+ self.client = client
+ self.name = name
+ self.api = resource
+
+ def __repr__(self):
+ return "Repository(%s)" % self.name
+
+ def __getattr__(self, item):
+ """Maps resource operations to methods on this object.
+
+ :param item: Item name.
+ """
+ oper = getattr(self.api, item, None)
+ if not (hasattr(oper, '__call__') and hasattr(oper, 'json')):
+ raise AttributeError(
+ "'%r' object has no attribute '%s'" % (self, item))
+
+ # The returned function wraps the underlying operation, promoting the
+ # received HTTP response to a first class object.
+ return lambda **kwargs: promote(self.client, oper(**kwargs), oper.json)
+
+
+class ObjectIdGenerator(object):
+ """Interface for extracting identifying information from an object's JSON
+ representation.
+ """
+
+ def get_params(self, obj_json):
+ """Gets the paramater values for specifying this object in a query.
+
+ :param obj_json: Instance data.
+ :type obj_json: dict
+ :return: Dictionary with paramater names and values
+ :rtype: dict of str, str
+ """
+ raise NotImplementedError("Not implemented")
+
+ def id_as_str(self, obj_json):
+ """Gets a single string identifying an object.
+
+ :param obj_json: Instance data.
+ :type obj_json: dict
+ :return: Id string.
+ :rtype: str
+ """
+ raise NotImplementedError("Not implemented")
+
+
+# noinspection PyDocstring
+class DefaultObjectIdGenerator(ObjectIdGenerator):
+ """Id generator that works for most of our objects.
+
+ :param param_name: Name of the parameter to specify in queries.
+ :param id_field: Name of the field to specify in JSON.
+ """
+
+ def __init__(self, param_name, id_field='id'):
+ self.param_name = param_name
+ self.id_field = id_field
+
+ def get_params(self, obj_json):
+ return {self.param_name: obj_json[self.id_field]}
+
+ def id_as_str(self, obj_json):
+ return obj_json[self.id_field]
+
+
+class BaseObject(object):
+ """Base class for ARI domain objects.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param resource: Associated Swagger resource.
+ :type resource: swaggerpy.client.Resource
+ :param as_json: JSON representation of this object instance.
+ :type as_json: dict
+ :param event_reg:
+ """
+
+ id_generator = ObjectIdGenerator()
+
+ def __init__(self, client, resource, as_json, event_reg):
+ self.client = client
+ self.api = resource
+ self.json = as_json
+ self.id = self.id_generator.id_as_str(as_json)
+ self.event_reg = event_reg
+
+ def __repr__(self):
+ return "%s(%s)" % (self.__class__.__name__, self.id)
+
+ def __getattr__(self, item):
+ """Promote resource operations related to a single resource to methods
+ on this class.
+
+ :param item:
+ """
+ oper = getattr(self.api, item, None)
+ if not (hasattr(oper, '__call__') and hasattr(oper, 'json')):
+ raise AttributeError(
+ "'%r' object has no attribute '%r'" % (self, item))
+
+ def enrich_operation(**kwargs):
+ """Enriches an operation by specifying parameters specifying this
+ object's id (i.e., channelId=self.id), and promotes HTTP response
+ to a first-class object.
+
+ :param kwargs: Operation parameters
+ :return: First class object mapped from HTTP response.
+ """
+ # Add id to param list
+ kwargs.update(self.id_generator.get_params(self.json))
+ return promote(self.client, oper(**kwargs), oper.json)
+
+ return enrich_operation
+
+ def on_event(self, event_type, fn):
+ """Register event callbacks for this specific domain object.
+
+ :param event_type: Type of event to register for.
+ :type event_type: str
+ :param fn: Callback function for events.
+ :type fn: (object, dict) -> None
+ """
+
+ def fn_filter(objects, event):
+ """Filter recieved events for this object.
+
+ :param objects: Objects found in this event.
+ :param event: Event.
+ """
+ if isinstance(objects, dict):
+ if self.id in [c.id for c in objects.values()]:
+ fn(objects, event)
+ else:
+ if self.id == objects.id:
+ fn(objects, event)
+
+ self.event_reg(event_type, fn_filter)
+
+
+class Channel(BaseObject):
+ """First class object API.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param channel_json: Instance data
+ """
+
+ id_generator = DefaultObjectIdGenerator('channelId')
+
+ def __init__(self, client, channel_json):
+ super(Channel, self).__init__(
+ client, client.swagger.channels, channel_json,
+ client.on_channel_event)
+
+
+class Bridge(BaseObject):
+ """First class object API.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param bridge_json: Instance data
+ """
+
+ id_generator = DefaultObjectIdGenerator('bridgeId')
+
+ def __init__(self, client, bridge_json):
+ super(Bridge, self).__init__(
+ client, client.swagger.bridges, bridge_json,
+ client.on_bridge_event)
+
+
+class Playback(BaseObject):
+ """First class object API.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param playback_json: Instance data
+ """
+ id_generator = DefaultObjectIdGenerator('playbackId')
+
+ def __init__(self, client, playback_json):
+ super(Playback, self).__init__(
+ client, client.swagger.playback, playback_json,
+ client.on_playback_event)
+
+
+# noinspection PyDocstring
+class EndpointIdGenerator(ObjectIdGenerator):
+ """Id generator for endpoints, because they are weird.
+ """
+
+ def get_params(self, obj_json):
+ return {
+ 'tech': obj_json['technology'],
+ 'resource': obj_json['resource']
+ }
+
+ def id_as_str(self, obj_json):
+ return "%(tech)s/%(resource)s" % self.get_params(obj_json)
+
+
+class Endpoint(BaseObject):
+ """First class object API.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param endpoint_json: Instance data
+ """
+ id_generator = EndpointIdGenerator()
+
+ def __init__(self, client, endpoint_json):
+ super(Endpoint, self).__init__(
+ client, client.swagger.endpoints, endpoint_json,
+ client.on_endpoint_event)
+
+
+class Sound(BaseObject):
+ """First class object API.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param sound_json: Instance data
+ """
+
+ id_generator = DefaultObjectIdGenerator('soundId')
+
+ def __init__(self, client, sound_json):
+ super(Sound, self).__init__(
+ client, client.swagger.sounds, sound_json, client.on_sound_event)
+
+
+def promote(client, resp, operation_json):
+ """Promote a response from the request's HTTP response to a first class
+ object.
+
+ :param client: ARI client.
+ :type client: client.Client
+ :param resp: HTTP resonse.
+ :type resp: requests.Response
+ :param operation_json: JSON model from Swagger API.
+ :type operation_json: dict
+ :return:
+ """
+ resp.raise_for_status()
+
+ response_class = operation_json['responseClass']
+ is_list = False
+ m = re.match('''List\[(.*)\]''', response_class)
+ if m:
+ response_class = m.group(1)
+ is_list = True
+ factory = CLASS_MAP.get(response_class)
+ if factory:
+ resp_json = resp.json()
+ if is_list:
+ return [factory(client, obj) for obj in resp_json]
+ return factory(client, resp_json)
+ if resp.status_code == requests.codes.no_content:
+ return None
+ log.info("No mapping for %s; returning JSON" % response_class)
+ return resp.json()
+
+
+CLASS_MAP = {
+ 'Bridge': Bridge,
+ 'Channel': Channel,
+ 'Endpoint': Endpoint,
+ 'Playback': Playback
+}
diff --git a/ari_test/__init__.py b/ari_test/__init__.py
new file mode 100644
index 0000000..f9a5eee
--- /dev/null
+++ b/ari_test/__init__.py
@@ -0,0 +1,3 @@
+#
+# Copyright (c) 2013, Digium, Inc.
+#
diff --git a/ari_test/client_test.py b/ari_test/client_test.py
new file mode 100644
index 0000000..3444ddd
--- /dev/null
+++ b/ari_test/client_test.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+
+import ari
+import httpretty
+import json
+import requests
+import unittest
+import urllib
+
+from ari_test.utils import AriTestCase
+
+
+GET = httpretty.GET
+PUT = httpretty.PUT
+POST = httpretty.POST
+DELETE = httpretty.DELETE
+
+
+# noinspection PyDocstring
+class ClientTest(AriTestCase):
+ def test_docs(self):
+ fp = urllib.urlopen("http://ari.py/ari/api-docs/resources.json")
+ try:
+ actual = json.load(fp)
+ self.assertEqual(self.BASE_URL, actual['basePath'])
+ finally:
+ fp.close()
+
+ def test_empty_listing(self):
+ self.serve(GET, 'channels', body='[]')
+ actual = self.uut.channels.list()
+ self.assertEqual([], actual)
+
+ def test_one_listing(self):
+ self.serve(GET, 'channels', body='[{"id": "test-channel"}]')
+ self.serve(DELETE, 'channels', 'test-channel')
+
+ actual = self.uut.channels.list()
+ self.assertEqual(1, len(actual))
+ actual[0].hangup()
+
+ def test_play(self):
+ self.serve(GET, 'channels', 'test-channel',
+ body='{"id": "test-channel"}')
+ self.serve(POST, 'channels', 'test-channel', 'play',
+ body='{"id": "test-playback"}')
+ self.serve(DELETE, 'playback', 'test-playback')
+
+ channel = self.uut.channels.get(channelId='test-channel')
+ playback = channel.play(media='sound:test-sound')
+ playback.stop()
+
+ def test_bad_resource(self):
+ try:
+ self.uut.i_am_not_a_resource.list()
+ self.fail("How did it find that resource?")
+ except AttributeError:
+ pass
+
+ def test_bad_repo_method(self):
+ try:
+ self.uut.channels.i_am_not_a_method()
+ self.fail("How did it find that method?")
+ except AttributeError:
+ pass
+
+ def test_bad_object_method(self):
+ self.serve(GET, 'channels', 'test-channel',
+ body='{"id": "test-channel"}')
+
+ try:
+ channel = self.uut.channels.get(channelId='test-channel')
+ channel.i_am_not_a_method()
+ self.fail("How did it find that method?")
+ except AttributeError:
+ pass
+
+ def test_bad_param(self):
+ try:
+ self.uut.channels.list(i_am_not_a_param='asdf')
+ self.fail("How did it find that param?")
+ except TypeError:
+ pass
+
+ def test_bad_response(self):
+ self.serve(GET, 'channels', body='{"message": "This is just a test"}',
+ status=500)
+ try:
+ self.uut.channels.list()
+ self.fail("Should have thrown an exception")
+ except requests.HTTPError as e:
+ self.assertEqual(500, e.response.status_code)
+ self.assertEqual(
+ {"message": "This is just a test"}, e.response.json())
+
+ def test_endpoints(self):
+ self.serve(GET, 'endpoints',
+ body='[{"technology": "TEST", "resource": "1234"}]')
+ self.serve(GET, 'endpoints', 'TEST', '1234',
+ body='{"technology": "TEST", "resource": "1234"}')
+
+ endpoints = self.uut.endpoints.list()
+ self.assertEqual(1, len(endpoints))
+ endpoint = endpoints[0].get()
+ self.assertEqual('TEST', endpoint.json['technology'])
+ self.assertEqual('1234', endpoint.json['resource'])
+
+ def setUp(self):
+ super(ClientTest, self).setUp()
+ self.uut = ari.connect('http://ari.py/', 'test', 'test')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ari_test/utils.py b/ari_test/utils.py
new file mode 100644
index 0000000..3eb49e7
--- /dev/null
+++ b/ari_test/utils.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+import httpretty
+import os
+import unittest
+import urlparse
+import ari
+import requests
+
+
+class AriTestCase(unittest.TestCase):
+ """Base class for mock ARI server.
+ """
+
+ BASE_URL = "http://ari.py/ari"
+
+ def setUp(self):
+ """Setup httpretty; create ARI client.
+ """
+ super(AriTestCase, self).setUp()
+ httpretty.enable()
+ self.serve_api()
+ self.uut = ari.connect('http://ari.py/', 'test', 'test')
+
+ def tearDown(self):
+ """Cleanup.
+ """
+ super(AriTestCase, self).tearDown()
+ httpretty.disable()
+ httpretty.reset()
+
+ @classmethod
+ def build_url(cls, *args):
+ """Build a URL, based off of BASE_URL, with the given args.
+
+ >>> AriTestCase.build_url('foo', 'bar', 'bam', 'bang')
+ 'http://ari.py/ari/foo/bar/bam/bang'
+
+ :param args: URL components
+ :return: URL
+ """
+ url = cls.BASE_URL
+ for arg in args:
+ url = urlparse.urljoin(url + '/', arg)
+ return url
+
+ def serve_api(self):
+ """Register all api-docs with httpretty to serve them for unit tests.
+ """
+ for filename in os.listdir('sample-api'):
+ if filename.endswith('.json'):
+ with open(os.path.join('sample-api', filename)) as fp:
+ body = fp.read()
+ self.serve(httpretty.GET, 'api-docs', filename, body=body)
+
+ def serve(self, method, *args, **kwargs):
+ """Serve a single URL for current test.
+
+ :param method: HTTP method. httpretty.{GET,PUT,POST,DELETE}.
+ :param args: URL path segments.
+ :param kwargs: See httpretty.register_uri()
+ """
+ url = self.build_url(*args)
+ if kwargs.get('body') is None and 'status' not in kwargs:
+ kwargs['status'] = requests.codes.no_content
+ httpretty.register_uri(method, url,
+ content_type="application/json",
+ **kwargs)
diff --git a/ari_test/websocket_test.py b/ari_test/websocket_test.py
new file mode 100644
index 0000000..5f56579
--- /dev/null
+++ b/ari_test/websocket_test.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python
+
+import unittest
+import ari
+import httpretty
+
+from ari_test.utils import AriTestCase
+from swaggerpy.http_client import SynchronousHttpClient
+
+BASE_URL = "http://ari.py/ari"
+
+GET = httpretty.GET
+PUT = httpretty.PUT
+POST = httpretty.POST
+DELETE = httpretty.DELETE
+
+
+class WebSocketTest(AriTestCase):
+ def setUp(self):
+ super(WebSocketTest, self).setUp()
+ self.actual = []
+
+ def record_event(self, event):
+ self.actual.append(event)
+
+ def test_empty(self):
+ uut = connect(BASE_URL, 'test', [])
+ uut.on_event('ev', self.record_event)
+ uut.run('test')
+ self.assertEqual([], self.actual)
+
+ def test_series(self):
+ messages = [
+ '{"type": "ev", "data": 1}',
+ '{"type": "ev", "data": 2}',
+ '{"type": "not_ev", "data": 3}',
+ '{"type": "not_ev", "data": 5}',
+ '{"type": "ev", "data": 9}'
+ ]
+ uut = connect(BASE_URL, 'test', messages)
+ uut.on_event("ev", self.record_event)
+ uut.run('test')
+ expected = [
+ {"type": "ev", "data": 1},
+ {"type": "ev", "data": 2},
+ {"type": "ev", "data": 9}
+ ]
+ self.assertEqual(expected, self.actual)
+
+ def test_on_channel(self):
+ self.serve(DELETE, 'channel', 'test-channel')
+ messages = [
+ '{ "type": "StasisStart", "channel": { "id": "test-channel" } }'
+ ]
+ uut = connect(BASE_URL, 'test', messages)
+
+ def cb(channel, event):
+ self.record_event(event)
+ channel.hangup()
+
+ uut.on_channel_event('StasisStart', cb)
+ uut.run('test')
+
+ expected = [
+ {"type": "StasisStart", "channel": {"id": "test-channel"}}
+ ]
+ self.assertEqual(expected, self.actual)
+
+ def test_channel_on_event(self):
+ self.serve(GET, 'channels', 'test-channel',
+ body='{"id": "test-channel"}')
+ self.serve(DELETE, 'channels', 'test-channel')
+ messages = [
+ '{"type": "ChannelStateChange", "channel": {"id": "ignore-me"}}',
+ '{"type": "ChannelStateChange", "channel": {"id": "test-channel"}}'
+ ]
+
+ uut = connect(BASE_URL, 'test', messages)
+ channel = uut.channels.get(channelId='test-channel')
+
+ def cb(channel, event):
+ self.record_event(event)
+ channel.hangup()
+
+ channel.on_event('ChannelStateChange', cb)
+ uut.run('test')
+
+ expected = [
+ {"type": "ChannelStateChange", "channel": {"id": "test-channel"}}
+ ]
+ self.assertEqual(expected, self.actual)
+
+ def test_bad_event_type(self):
+ uut = connect(BASE_URL, 'test', [])
+ try:
+ uut.on_object_event(
+ 'BadEventType', self.noop, self.noop, 'Channel')
+ self.fail("Event does not exist")
+ except ValueError:
+ pass
+
+ def test_bad_object_type(self):
+ uut = connect(BASE_URL, 'test', [])
+ try:
+ uut.on_object_event('StasisStart', self.noop, self.noop, 'Bridge')
+ self.fail("Event has no bridge")
+ except ValueError:
+ pass
+
+ def noop(self, *args, **kwargs):
+ self.fail("Noop unexpectedly called")
+
+
+class WebSocketStubConnection(object):
+ def __init__(self, messages):
+ self.messages = list(messages)
+ self.messages.reverse()
+
+ def recv(self):
+ if self.messages:
+ return str(self.messages.pop())
+ return None
+
+
+class WebSocketStubClient(SynchronousHttpClient):
+ """Stub WebSocket connection.
+
+ :param messages: List of messages to return.
+ :type messages: list
+ """
+
+ def __init__(self, messages):
+ super(WebSocketStubClient, self).__init__()
+ self.messages = messages
+
+ def ws_connect(self, url, params=None):
+ return WebSocketStubConnection(self.messages)
+
+
+def connect(base_url, apps, messages):
+ http_client = WebSocketStubClient(messages)
+ return ari.Client(base_url, http_client)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/ast-ari-py.iml b/ast-ari-py.iml
new file mode 100644
index 0000000..85f6abe
--- /dev/null
+++ b/ast-ari-py.iml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/bridge_example.py b/examples/bridge_example.py
new file mode 100644
index 0000000..58cd1a9
--- /dev/null
+++ b/examples/bridge_example.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+"""Short example of how to use bridge objects.
+
+This example will create a holding bridge (if one doesn't already exist). Any
+channels that enter Stasis is placed into the bridge. Whenever a channel
+enters the bridge, a tone is played to the bridge.
+"""
+
+#
+# Copyright (c) 2013, Digium, Inc.
+#
+
+import ari
+
+client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo')
+
+#
+# Find (or create) a holding bridge.
+#
+bridges = [b for b in client.bridges.list() if
+ b.json['bridge_type'] == 'holding']
+if bridges:
+ bridge = bridges[0]
+ print "Using bridge %s" % bridge.id
+else:
+ bridge = client.bridges.create(type='holding')
+ print "Created bridge %s" % bridge.id
+
+
+def on_enter(bridge, ev):
+ """Callback for bridge enter events.
+
+ When channels enter the bridge, play tones to the whole bridge.
+
+ :param bridge: Bridge entering the channel.
+ :param ev: Event.
+ """
+ # ignore announcer channels - see ASTERISK-22744
+ if ev['channel']['name'].startswith('Announcer/'):
+ return
+ bridge.play(media="sound:ascending-2tone")
+
+
+bridge.on_event('ChannelEnteredBridge', on_enter)
+
+
+def stasis_start_cb(channel, ev):
+ """Callback for StasisStart events.
+
+ For new channels, answer and put them in the holding bridge.
+
+ :param channel: Channel that entered Stasis
+ :param ev: Event
+ """
+ channel.answer()
+ bridge.addChannel(channel=channel.id)
+
+
+client.on_channel_event('StasisStart', stasis_start_cb)
+
+# Run the WebSocket
+client.run(apps='hello')
diff --git a/examples/example.py b/examples/example.py
new file mode 100644
index 0000000..f2403cf
--- /dev/null
+++ b/examples/example.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+
+"""Brief example of using the channel API.
+
+This app will answer any channel sent to Stasis(hello), and play "Hello,
+world" to the channel. For any DTMF events received, the number is played back
+to the channel. Press # to hang up, and * for a special message.
+"""
+
+#
+# Copyright (c) 2013, Digium, Inc.
+#
+
+import ari
+
+client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo')
+
+
+def on_dtmf(channel, event):
+ """Callback for DTMF events.
+
+ When DTMF is received, play the digit back to the channel. # hangs up,
+ * plays a special message.
+
+ :param channel: Channel DTMF was received from.
+ :param event: Event.
+ """
+ digit = event['digit']
+ if digit == '#':
+ channel.play(media='sound:goodbye')
+ channel.continueInDialplan()
+ elif digit == '*':
+ channel.play(media='sound:asterisk-friend')
+ else:
+ channel.play(media='sound:digits/%s' % digit)
+
+
+def on_start(channel, event):
+ """Callback for StasisStart events.
+
+ On new channels, register the on_dtmf callback, answer the channel and
+ play "Hello, world"
+
+ :param channel: Channel DTMF was received from.
+ :param event: Event.
+ """
+ channel.on_event('ChannelDtmfReceived', on_dtmf)
+ channel.answer()
+ channel.play(media='sound:hello-world')
+
+
+client.on_channel_event('StasisStart', on_start)
+
+# Run the WebSocket
+client.run(apps="hello")
diff --git a/examples/originate_example.py b/examples/originate_example.py
new file mode 100644
index 0000000..77b7eba
--- /dev/null
+++ b/examples/originate_example.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+
+"""Example demonstrating ARI channel origination.
+
+"""
+
+#
+# Copyright (c) 2013, Digium, Inc.
+#
+import requests
+
+import ari
+
+from requests import HTTPError
+
+OUTGOING_ENDPOINT = "SIP/blink"
+
+client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo')
+
+#
+# Find (or create) a holding bridge.
+#
+bridges = [b for b in client.bridges.list()
+ if b.json['bridge_type'] == 'holding']
+if bridges:
+ holding_bridge = bridges[0]
+ print "Using bridge %s" % holding_bridge.id
+else:
+ holding_bridge = client.bridges.create(type='holding')
+ print "Created bridge %s" % holding_bridge.id
+
+
+def safe_hangup(channel):
+ """Hangup a channel, ignoring 404 errors.
+
+ :param channel: Channel to hangup.
+ """
+ try:
+ channel.hangup()
+ except HTTPError as e:
+ # Ignore 404's, since channels can go away before we get to them
+ if e.response.status_code != requests.codes.not_found:
+ raise
+
+
+def on_start(incoming, event):
+ """Callback for StasisStart events.
+
+ When an incoming channel starts, put it in the holding bridge and
+ originate a channel to connect to it. When that channel answers, create a
+ bridge and put both of them into it.
+
+ :param incoming:
+ :param event:
+ """
+ # Only process channels with the 'incoming' argument
+ if event['args'] != ['incoming']:
+ return
+
+ # Answer and put in the holding bridge
+ incoming.answer()
+ incoming.play(media="sound:pls-wait-connect-call")
+ holding_bridge.addChannel(channel=incoming.id)
+
+ # Originate the outgoing channel
+ outgoing = client.channels.originate(
+ endpoint=OUTGOING_ENDPOINT, app="hello", appArgs="dialed")
+
+ # If the incoming channel ends, hangup the outgoing channel
+ incoming.on_event('StasisEnd', lambda *args: safe_hangup(outgoing))
+ # and vice versa. If the endpoint rejects the call, it is destroyed
+ # without entering Stasis()
+ outgoing.on_event('ChannelDestroyed',
+ lambda *args: safe_hangup(incoming))
+
+ def outgoing_on_start(channel, event):
+ """Callback for StasisStart events on the outgoing channel
+
+ :param channel: Outgoing channel.
+ :param event: Event.
+ """
+ # Create a bridge, putting both channels into it.
+ bridge = client.bridges.create(type='mixing')
+ outgoing.answer()
+ bridge.addChannel(channel=[incoming.id, outgoing.id])
+ # Clean up the bridge when done
+ outgoing.on_event('StasisEnd', lambda *args: bridge.destroy())
+
+ outgoing.on_event('StasisStart', outgoing_on_start)
+
+
+client.on_channel_event('StasisStart', on_start)
+
+# Run the WebSocket
+client.run(apps="hello")
diff --git a/examples/playback_example.py b/examples/playback_example.py
new file mode 100644
index 0000000..0371945
--- /dev/null
+++ b/examples/playback_example.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+"""Example demonstrating using the returned object from an API call.
+
+This app plays demo-contrats on any channel sent to Stasis(hello). DTMF keys
+are used to control the playback.
+"""
+
+#
+# Copyright (c) 2013, Digium, Inc.
+#
+
+import ari
+import sys
+
+client = ari.connect('http://localhost:8088/', 'hey', 'peekaboo')
+
+
+def on_start(channel, event):
+ """Callback for StasisStart events.
+
+ On new channels, answer, play demo-congrats, and register a DTMF listener.
+
+ :param channel: Channel DTMF was received from.
+ :param event: Event.
+ """
+ channel.answer()
+ playback = channel.play(media='sound:demo-congrats')
+
+ def on_dtmf(channel, event):
+ """Callback for DTMF events.
+
+ DTMF events control the playback operation.
+
+ :param channel: Channel DTMF was received on.
+ :param event: Event.
+ """
+ # Since the callback was registered to a specific channel, we can
+ # control the playback object we already have in scope.
+ digit = event['digit']
+ if digit == '5':
+ playback.control(operation='pause')
+ elif digit == '8':
+ playback.control(operation='unpause')
+ elif digit == '4':
+ playback.control(operation='reverse')
+ elif digit == '6':
+ playback.control(operation='forward')
+ elif digit == '2':
+ playback.control(operation='restart')
+ elif digit == '#':
+ playback.stop()
+ channel.continueInDialplan()
+ else:
+ print >> sys.stderr, "Unknown DTMF %s" % digit
+
+ channel.on_event('ChannelDtmfReceived', on_dtmf)
+
+
+client.on_channel_event('StasisStart', on_start)
+
+# Run the WebSocket
+client.run(apps='hello')
diff --git a/nose.cfg b/nose.cfg
new file mode 100644
index 0000000..1f6f0ed
--- /dev/null
+++ b/nose.cfg
@@ -0,0 +1,13 @@
+[nosetests]
+cover-erase = True
+cover-html = True
+cover-inclusive = True
+cover-package = ari
+with-doctest = True
+doctest-tests = True
+with-xunit = True
+with-tissue = True
+tissue-package = ari_test,ari
+logging-level = DEBUG
+nocapture = True
+no-byte-compile = True
diff --git a/sample-api/README.md b/sample-api/README.md
new file mode 100644
index 0000000..92bdd73
--- /dev/null
+++ b/sample-api/README.md
@@ -0,0 +1,5 @@
+
+
+This directory contains a slimmed down example of the Asterisk REST Interface
+Swagger definitions. These are only used for unit testing, so shouldn't be taken
+too seriously.
diff --git a/sample-api/applications.json b/sample-api/applications.json
new file mode 100644
index 0000000..4b719c7
--- /dev/null
+++ b/sample-api/applications.json
@@ -0,0 +1,167 @@
+{
+ "_copyright": "Copyright (C) 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401312 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/applications.{format}",
+ "apis": [
+ {
+ "path": "/applications",
+ "description": "Stasis applications",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List all applications.",
+ "nickname": "list",
+ "responseClass": "List[Application]"
+ }
+ ]
+ },
+ {
+ "path": "/applications/{applicationName}",
+ "description": "Stasis application",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Get details of an application.",
+ "nickname": "get",
+ "responseClass": "Application",
+ "parameters": [
+ {
+ "name": "applicationName",
+ "description": "Application's name",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Application does not exist."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/applications/{applicationName}/subscription",
+ "description": "Stasis application",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Subscribe an application to a event source.",
+ "notes": "Returns the state of the application after the subscriptions have changed",
+ "nickname": "subscribe",
+ "responseClass": "Application",
+ "parameters": [
+ {
+ "name": "applicationName",
+ "description": "Application's name",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "eventSource",
+ "description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": true,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Missing parameter."
+ },
+ {
+ "code": 404,
+ "reason": "Application does not exist."
+ },
+ {
+ "code": 422,
+ "reason": "Event source does not exist."
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Unsubscribe an application from an event source.",
+ "notes": "Returns the state of the application after the subscriptions have changed",
+ "nickname": "unsubscribe",
+ "responseClass": "Application",
+ "parameters": [
+ {
+ "name": "applicationName",
+ "description": "Application's name",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "eventSource",
+ "description": "URI for event source (channel:{channelId}, bridge:{bridgeId}, endpoint:{tech}/{resource}",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": true,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Missing parameter; event source scheme not recognized."
+ },
+ {
+ "code": 404,
+ "reason": "Application does not exist."
+ },
+ {
+ "code": 409,
+ "reason": "Application not subscribed to event source."
+ },
+ {
+ "code": 422,
+ "reason": "Event source does not exist."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "Application": {
+ "id": "Application",
+ "description": "Details of a Stasis application",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of this application",
+ "required": true
+ },
+ "channel_ids": {
+ "type": "List[string]",
+ "description": "Id's for channels subscribed to.",
+ "required": true
+ },
+ "bridge_ids": {
+ "type": "List[string]",
+ "description": "Id's for bridges subscribed to.",
+ "required": true
+ },
+ "endpoint_ids": {
+ "type": "List[string]",
+ "description": "{tech}/{resource} for endpoints subscribed to.",
+ "required": true
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/asterisk.json b/sample-api/asterisk.json
new file mode 100644
index 0000000..6b5221e
--- /dev/null
+++ b/sample-api/asterisk.json
@@ -0,0 +1,259 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401259 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/asterisk.{format}",
+ "apis": [
+ {
+ "path": "/asterisk/info",
+ "description": "Asterisk system information (similar to core show settings)",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Gets Asterisk system information.",
+ "nickname": "getInfo",
+ "responseClass": "AsteriskInfo",
+ "parameters": [
+ {
+ "name": "only",
+ "description": "Filter information returned",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": true,
+ "dataType": "string",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "build",
+ "system",
+ "config",
+ "status"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/asterisk/variable",
+ "description": "Global variables",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Get the value of a global variable.",
+ "nickname": "getGlobalVar",
+ "responseClass": "Variable",
+ "parameters": [
+ {
+ "name": "variable",
+ "description": "The variable to get",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Missing variable parameter."
+ }
+ ]
+ },
+ {
+ "httpMethod": "POST",
+ "summary": "Set the value of a global variable.",
+ "nickname": "setGlobalVar",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "variable",
+ "description": "The variable to set",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value to set the variable to",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Missing variable parameter."
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "BuildInfo": {
+ "id": "BuildInfo",
+ "description": "Info about how Asterisk was built",
+ "properties": {
+ "os": {
+ "required": true,
+ "type": "string",
+ "description": "OS Asterisk was built on."
+ },
+ "kernel": {
+ "required": true,
+ "type": "string",
+ "description": "Kernel version Asterisk was built on."
+ },
+ "options": {
+ "required": true,
+ "type": "string",
+ "description": "Compile time options, or empty string if default."
+ },
+ "machine": {
+ "required": true,
+ "type": "string",
+ "description": "Machine architecture (x86_64, i686, ppc, etc.)"
+ },
+ "date": {
+ "required": true,
+ "type": "string",
+ "description": "Date and time when Asterisk was built."
+ },
+ "user": {
+ "required": true,
+ "type": "string",
+ "description": "Username that build Asterisk"
+ }
+ }
+ },
+ "SystemInfo": {
+ "id": "SystemInfo",
+ "description": "Info about Asterisk",
+ "properties": {
+ "version": {
+ "required": true,
+ "type": "string",
+ "description": "Asterisk version."
+ },
+ "entity_id": {
+ "required": true,
+ "type": "string",
+ "description": ""
+ }
+ }
+ },
+ "SetId": {
+ "id": "SetId",
+ "description": "Effective user/group id",
+ "properties": {
+ "user": {
+ "required": true,
+ "type": "string",
+ "description": "Effective user id."
+ },
+ "group": {
+ "required": true,
+ "type": "string",
+ "description": "Effective group id."
+ }
+ }
+ },
+ "ConfigInfo": {
+ "id": "ConfigInfo",
+ "description": "Info about Asterisk configuration",
+ "properties": {
+ "name": {
+ "required": true,
+ "type": "string",
+ "description": "Asterisk system name."
+ },
+ "default_language": {
+ "required": true,
+ "type": "string",
+ "description": "Default language for media playback."
+ },
+ "max_channels": {
+ "required": false,
+ "type": "int",
+ "description": "Maximum number of simultaneous channels."
+ },
+ "max_open_files": {
+ "required": false,
+ "type": "int",
+ "description": "Maximum number of open file handles (files, sockets)."
+ },
+ "max_load": {
+ "required": false,
+ "type": "double",
+ "description": "Maximum load avg on system."
+ },
+ "setid": {
+ "required": true,
+ "type": "SetId",
+ "description": "Effective user/group id for running Asterisk."
+ }
+ }
+ },
+ "StatusInfo": {
+ "id": "StatusInfo",
+ "description": "Info about Asterisk status",
+ "properties": {
+ "startup_time": {
+ "required": true,
+ "type": "Date",
+ "description": "Time when Asterisk was started."
+ },
+ "last_reload_time": {
+ "required": true,
+ "type": "Date",
+ "description": "Time when Asterisk was last reloaded."
+ }
+ }
+ },
+ "AsteriskInfo": {
+ "id": "AsteriskInfo",
+ "description": "Asterisk system information",
+ "properties": {
+ "build": {
+ "required": false,
+ "type": "BuildInfo",
+ "description": "Info about how Asterisk was built"
+ },
+ "system": {
+ "required": false,
+ "type": "SystemInfo",
+ "description": "Info about the system running Asterisk"
+ },
+ "config": {
+ "required": false,
+ "type": "ConfigInfo",
+ "description": "Info about Asterisk configuration"
+ },
+ "status": {
+ "required": false,
+ "type": "StatusInfo",
+ "description": "Info about Asterisk status"
+ }
+ }
+ },
+ "Variable": {
+ "id": "Variable",
+ "description": "The value of a channel variable",
+ "properties": {
+ "value": {
+ "required": true,
+ "type": "string",
+ "description": "The value of the variable requested"
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/bridges.json b/sample-api/bridges.json
new file mode 100644
index 0000000..4d844c1
--- /dev/null
+++ b/sample-api/bridges.json
@@ -0,0 +1,509 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401312 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/bridges.{format}",
+ "apis": [
+ {
+ "path": "/bridges",
+ "description": "Active bridges",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List all active bridges in Asterisk.",
+ "nickname": "list",
+ "responseClass": "List[Bridge]"
+ },
+ {
+ "httpMethod": "POST",
+ "summary": "Create a new bridge.",
+ "notes": "This bridge persists until it has been shut down, or Asterisk has been shut down.",
+ "nickname": "create",
+ "responseClass": "Bridge",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of bridge to create.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "mixing",
+ "holding"
+ ]
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/bridges/{bridgeId}",
+ "description": "Individual bridge",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Get bridge details.",
+ "nickname": "get",
+ "responseClass": "Bridge",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Shut down a bridge.",
+ "notes": "If any channels are in this bridge, they will be removed and resume whatever they were doing beforehand.",
+ "nickname": "destroy",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/bridges/{bridgeId}/addChannel",
+ "description": "Add a channel to a bridge",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Add a channel to a bridge.",
+ "nickname": "addChannel",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "channel",
+ "description": "Ids of channels to add to bridge",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": true,
+ "dataType": "string"
+ },
+ {
+ "name": "role",
+ "description": "Channel's role in the bridge",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ },
+ {
+ "code": 409,
+ "reason": "Bridge not in Stasis application"
+ },
+ {
+ "code": 422,
+ "reason": "Channel not in Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/bridges/{bridgeId}/removeChannel",
+ "description": "Remove a channel from a bridge",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Remove a channel from a bridge.",
+ "nickname": "removeChannel",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "channel",
+ "description": "Ids of channels to remove from bridge",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": true,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ },
+ {
+ "code": 409,
+ "reason": "Bridge not in Stasis application"
+ },
+ {
+ "code": 422,
+ "reason": "Channel not in this bridge"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/bridges/{bridgeId}/moh",
+ "description": "Play music on hold to a bridge",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Play music on hold to a bridge or change the MOH class that is playing.",
+ "nickname": "startMoh",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "mohClass",
+ "description": "Channel's id",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ },
+ {
+ "code": 409,
+ "reason": "Bridge not in Stasis application"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Stop playing music on hold to a bridge.",
+ "notes": "This will only stop music on hold being played via POST bridges/{bridgeId}/moh.",
+ "nickname": "stopMoh",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ },
+ {
+ "code": 409,
+ "reason": "Bridge not in Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/bridges/{bridgeId}/play",
+ "description": "Play media to the participants of a bridge",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Start playback of media on a bridge.",
+ "notes": "The media URI may be any of a number of URI's. Currently sound: and recording: URI's are supported. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)",
+ "nickname": "play",
+ "responseClass": "Playback",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "media",
+ "description": "Media's URI to play.",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "lang",
+ "description": "For sounds, selects language for sound.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "offsetms",
+ "description": "Number of media to skip before playing.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+
+ },
+ {
+ "name": "skipms",
+ "description": "Number of milliseconds to skip for forward/reverse operations.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 3000,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ },
+ {
+ "code": 409,
+ "reason": "Bridge not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/bridges/{bridgeId}/record",
+ "description": "Record audio on a bridge",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Start a recording.",
+ "notes": "This records the mixed audio from all channels participating in this bridge.",
+ "nickname": "record",
+ "responseClass": "LiveRecording",
+ "parameters": [
+ {
+ "name": "bridgeId",
+ "description": "Bridge's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "name",
+ "description": "Recording's filename",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "format",
+ "description": "Format to encode audio in",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "maxDurationSeconds",
+ "description": "Maximum duration of the recording, in seconds. 0 for no limit.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+ },
+ {
+ "name": "maxSilenceSeconds",
+ "description": "Maximum duration of silence, in seconds. 0 for no limit.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+ },
+ {
+ "name": "ifExists",
+ "description": "Action to take if a recording with the same name already exists.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string",
+ "defaultValue": "fail",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "fail",
+ "overwrite",
+ "append"
+ ]
+ }
+ },
+ {
+ "name": "beep",
+ "description": "Play beep when recording begins",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "boolean",
+ "defaultValue": false
+ },
+ {
+ "name": "terminateOn",
+ "description": "DTMF input to terminate recording.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string",
+ "defaultValue": "none",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "none",
+ "any",
+ "*",
+ "#"
+ ]
+ }
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Recording name invalid"
+ },
+ {
+ "code": 404,
+ "reason": "Bridge not found"
+ },
+ {
+ "code": 409,
+ "reason": "Bridge not in Stasis application; Recording already in progress"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "Bridge": {
+ "id": "Bridge",
+ "description": "The merging of media from one or more channels.\n\nEveryone on the bridge receives the same audio.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique identifier for this bridge",
+ "required": true
+ },
+ "technology": {
+ "type": "string",
+ "description": "Name of the current bridging technology",
+ "required": true
+ },
+ "bridge_type": {
+ "type": "string",
+ "description": "Type of bridge technology",
+ "required": true,
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "mixing",
+ "holding"
+ ]
+ }
+ },
+ "bridge_class": {
+ "type": "string",
+ "description": "Bridging class",
+ "required": true
+ },
+ "channels": {
+ "type": "List[string]",
+ "description": "Ids of channels participating in this bridge",
+ "required": true
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/channels.json b/sample-api/channels.json
new file mode 100644
index 0000000..bdbfc60
--- /dev/null
+++ b/sample-api/channels.json
@@ -0,0 +1,902 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401436 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/channels.{format}",
+ "apis": [
+ {
+ "path": "/channels",
+ "description": "Active channels",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List all active channels in Asterisk.",
+ "nickname": "list",
+ "responseClass": "List[Channel]"
+ },
+ {
+ "httpMethod": "POST",
+ "summary": "Create a new channel (originate).",
+ "notes": "The new channel is created immediately and a snapshot of it returned. If a Stasis application is provided it will be automatically subscribed to the originated channel for further events and updates.",
+ "nickname": "originate",
+ "responseClass": "Channel",
+ "parameters": [
+ {
+ "name": "endpoint",
+ "description": "Endpoint to call.",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "extension",
+ "description": "The extension to dial after the endpoint answers",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "context",
+ "description": "The context to dial after the endpoint answers. If omitted, uses 'default'",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "priority",
+ "description": "The priority to dial after the endpoint answers. If omitted, uses 1",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "long"
+ },
+ {
+ "name": "app",
+ "description": "The application that is subscribed to the originated channel, and passed to the Stasis application.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "appArgs",
+ "description": "The application arguments to pass to the Stasis application.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "callerId",
+ "description": "CallerID to use when dialing the endpoint or extension.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "timeout",
+ "description": "Timeout (in seconds) before giving up dialing, or -1 for no timeout.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 30
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Invalid parameters for originating a channel."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}",
+ "description": "Active channel",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Channel details.",
+ "nickname": "get",
+ "responseClass": "Channel",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Delete (i.e. hangup) a channel.",
+ "nickname": "hangup",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/dial",
+ "description": "Create a new channel (originate) and bridge to this channel",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Create a new channel (originate) and bridge to this channel.",
+ "nickname": "dial",
+ "responseClass": "Dialed",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "endpoint",
+ "description": "Endpoint to call. If not specified, dial is routed via dialplan",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "extension",
+ "description": "Extension to dial",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "context",
+ "description": "When routing via dialplan, the context use. If omitted, uses 'default'",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "timeout",
+ "description": "Timeout (in seconds) before giving up dialing, or -1 for no timeout.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 30
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/continue",
+ "description": "Exit application; continue execution in the dialplan",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Exit application; continue execution in the dialplan.",
+ "nickname": "continueInDialplan",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "context",
+ "description": "The context to continue to.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "extension",
+ "description": "The extension to continue to.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "priority",
+ "description": "The priority to continue to.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/answer",
+ "description": "Answer a channel",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Answer a channel.",
+ "nickname": "answer",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/mute",
+ "description": "Mute a channel",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Mute a channel.",
+ "nickname": "mute",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "direction",
+ "description": "Direction in which to mute audio",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string",
+ "defaultValue": "both",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "both",
+ "in",
+ "out"
+ ]
+ }
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Unmute a channel.",
+ "nickname": "unmute",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "direction",
+ "description": "Direction in which to unmute audio",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string",
+ "defaultValue": "both",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "both",
+ "in",
+ "out"
+ ]
+ }
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/hold",
+ "description": "Put a channel on hold",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Hold a channel.",
+ "nickname": "hold",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Remove a channel from hold.",
+ "nickname": "unhold",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/moh",
+ "description": "Play music on hold to a channel",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Play music on hold to a channel.",
+ "notes": "Using media operations such as playOnChannel on a channel playing MOH in this manner will suspend MOH without resuming automatically. If continuing music on hold is desired, the stasis application must reinitiate music on hold.",
+ "nickname": "startMoh",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "mohClass",
+ "description": "Music on hold class to use",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Stop playing music on hold to a channel.",
+ "nickname": "stopMoh",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/play",
+ "description": "Play media to a channel",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Start playback of media.",
+ "notes": "The media URI may be any of a number of URI's. Currently sound: and recording: URI's are supported. This operation creates a playback resource that can be used to control the playback of media (pause, rewind, fast forward, etc.)",
+ "nickname": "play",
+ "responseClass": "Playback",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "media",
+ "description": "Media's URI to play.",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "lang",
+ "description": "For sounds, selects language for sound.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "offsetms",
+ "description": "Number of media to skip before playing.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int"
+ },
+ {
+ "name": "skipms",
+ "description": "Number of milliseconds to skip for forward/reverse operations.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 3000
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/record",
+ "description": "Record audio from a channel",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Start a recording.",
+ "notes": "Record audio from a channel. Note that this will not capture audio sent to the channel. The bridge itself has a record feature if that's what you want.",
+ "nickname": "record",
+ "responseClass": "LiveRecording",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "name",
+ "description": "Recording's filename",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "format",
+ "description": "Format to encode audio in",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "maxDurationSeconds",
+ "description": "Maximum duration of the recording, in seconds. 0 for no limit",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+ },
+ {
+ "name": "maxSilenceSeconds",
+ "description": "Maximum duration of silence, in seconds. 0 for no limit",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "int",
+ "defaultValue": 0,
+ "allowableValues": {
+ "valueType": "RANGE",
+ "min": 0
+ }
+ },
+ {
+ "name": "ifExists",
+ "description": "Action to take if a recording with the same name already exists.",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string",
+ "defaultValue": "fail",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "fail",
+ "overwrite",
+ "append"
+ ]
+ }
+ },
+ {
+ "name": "beep",
+ "description": "Play beep when recording begins",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "boolean",
+ "defaultValue": false
+ },
+ {
+ "name": "terminateOn",
+ "description": "DTMF input to terminate recording",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string",
+ "defaultValue": "none",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "none",
+ "any",
+ "*",
+ "#"
+ ]
+ }
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Invalid parameters"
+ },
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel is not in a Stasis application; the channel is currently bridged with other channels; A recording with the same name is currently in progress."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/channels/{channelId}/variable",
+ "description": "Variables on a channel",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Get the value of a channel variable or function.",
+ "nickname": "getChannelVar",
+ "responseClass": "Variable",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "variable",
+ "description": "The channel variable or function to get",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Missing variable parameter."
+ },
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ },
+ {
+ "httpMethod": "POST",
+ "summary": "Set the value of a channel variable or function.",
+ "nickname": "setChannelVar",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "channelId",
+ "description": "Channel's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "variable",
+ "description": "The channel variable or function to set",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value to set the variable to",
+ "paramType": "query",
+ "required": false,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "Missing variable parameter."
+ },
+ {
+ "code": 404,
+ "reason": "Channel not found"
+ },
+ {
+ "code": 409,
+ "reason": "Channel not in a Stasis application"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "Dialed": {
+ "id": "Dialed",
+ "description": "Dialed channel information.",
+ "properties": {}
+ },
+ "DialplanCEP": {
+ "id": "DialplanCEP",
+ "description": "Dialplan location (context/extension/priority)",
+ "properties": {
+ "context": {
+ "required": true,
+ "type": "string",
+ "description": "Context in the dialplan"
+ },
+ "exten": {
+ "required": true,
+ "type": "string",
+ "description": "Extension in the dialplan"
+ },
+ "priority": {
+ "required": true,
+ "type": "long",
+ "description": "Priority in the dialplan"
+ }
+ }
+ },
+ "CallerID": {
+ "id": "CallerID",
+ "description": "Caller identification",
+ "properties": {
+ "name": {
+ "required": true,
+ "type": "string"
+ },
+ "number": {
+ "required": true,
+ "type": "string"
+ }
+ }
+ },
+ "Channel": {
+ "id": "Channel",
+ "description": "A specific communication connection between Asterisk and an Endpoint.",
+ "properties": {
+ "id": {
+ "required": true,
+ "type": "string",
+ "description": "Unique identifier of the channel.\n\nThis is the same as the Uniqueid field in AMI."
+ },
+ "name": {
+ "required": true,
+ "type": "string",
+ "description": "Name of the channel (i.e. SIP/foo-0000a7e3)"
+ },
+ "state": {
+ "required": true,
+ "type": "string",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "Down",
+ "Rsrved",
+ "OffHook",
+ "Dialing",
+ "Ring",
+ "Ringing",
+ "Up",
+ "Busy",
+ "Dialing Offhook",
+ "Pre-ring",
+ "Unknown"
+ ]
+ }
+ },
+ "caller": {
+ "required": true,
+ "type": "CallerID"
+ },
+ "connected": {
+ "required": true,
+ "type": "CallerID"
+ },
+ "accountcode": {
+ "required": true,
+ "type": "string"
+ },
+ "dialplan": {
+ "required": true,
+ "type": "DialplanCEP",
+ "description": "Current location in the dialplan"
+ },
+ "creationtime": {
+ "required": true,
+ "type": "Date",
+ "description": "Timestamp when channel was created"
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/endpoints.json b/sample-api/endpoints.json
new file mode 100644
index 0000000..6084986
--- /dev/null
+++ b/sample-api/endpoints.json
@@ -0,0 +1,105 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401312 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/endpoints.{format}",
+ "apis": [
+ {
+ "path": "/endpoints",
+ "description": "Asterisk endpoints",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List all endpoints.",
+ "nickname": "list",
+ "responseClass": "List[Endpoint]"
+ }
+ ]
+ },
+ {
+ "path": "/endpoints/{tech}",
+ "description": "Asterisk endpoints",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List available endoints for a given endpoint technology.",
+ "nickname": "listByTech",
+ "responseClass": "List[Endpoint]",
+ "parameters": [
+ {
+ "name": "tech",
+ "description": "Technology of the endpoints (sip,iax2,...)",
+ "paramType": "path",
+ "dataType": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/endpoints/{tech}/{resource}",
+ "description": "Single endpoint",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Details for an endpoint.",
+ "nickname": "get",
+ "responseClass": "Endpoint",
+ "parameters": [
+ {
+ "name": "tech",
+ "description": "Technology of the endpoint",
+ "paramType": "path",
+ "dataType": "string"
+ },
+ {
+ "name": "resource",
+ "description": "ID of the endpoint",
+ "paramType": "path",
+ "dataType": "string"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "Endpoint": {
+ "id": "Endpoint",
+ "description": "An external device that may offer/accept calls to/from Asterisk.\n\nUnlike most resources, which have a single unique identifier, an endpoint is uniquely identified by the technology/resource pair.",
+ "properties": {
+ "technology": {
+ "type": "string",
+ "description": "Technology of the endpoint",
+ "required": true
+ },
+ "resource": {
+ "type": "string",
+ "description": "Identifier of the endpoint, specific to the given technology.",
+ "required": true
+ },
+ "state": {
+ "type": "string",
+ "description": "Endpoint's state",
+ "required": false,
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "unknown",
+ "offline",
+ "online"
+ ]
+ }
+ },
+ "channel_ids": {
+ "type": "List[string]",
+ "description": "Id's of channels associated with this endpoint",
+ "required": true
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/events.json b/sample-api/events.json
new file mode 100644
index 0000000..3f62cae
--- /dev/null
+++ b/sample-api/events.json
@@ -0,0 +1,385 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 400522 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.3",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/events.{format}",
+ "apis": [
+ {
+ "path": "/events",
+ "description": "Events from Asterisk to applications",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "upgrade": "websocket",
+ "websocketProtocol": "ari",
+ "summary": "WebSocket connection for events.",
+ "nickname": "eventWebsocket",
+ "responseClass": "Message",
+ "parameters": [
+ {
+ "name": "app",
+ "description": "Applications to subscribe to.",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": true,
+ "dataType": "string"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "Message": {
+ "id": "Message",
+ "description": "Base type for errors and events",
+ "discriminator": "type",
+ "properties": {
+ "type": {
+ "type": "string",
+ "required": true,
+ "description": "Indicates the type of this message."
+ }
+ }
+ },
+ "MissingParams": {
+ "id": "MissingParams",
+ "extends": "Message",
+ "description": "Error event sent when required params are missing.",
+ "properties": {
+ "params": {
+ "required": true,
+ "type": "List[string]",
+ "description": "A list of the missing parameters"
+ }
+ }
+ },
+ "Event": {
+ "id": "Event",
+ "extends": "Message",
+ "description": "Base type for asynchronous events from Asterisk.",
+ "properties": {
+ "application": {
+ "type": "string",
+ "description": "Name of the application receiving the event.",
+ "required": true
+ },
+ "timestamp": {
+ "type": "Date",
+ "description": "Time at which this event was created.",
+ "required": false
+ }
+ }
+ },
+ "PlaybackStarted": {
+ "id": "PlaybackStarted",
+ "extends": "Event",
+ "description": "Event showing the start of a media playback operation.",
+ "properties": {
+ "playback": {
+ "type": "Playback",
+ "description": "Playback control object",
+ "required": true
+ }
+ }
+ },
+ "PlaybackFinished": {
+ "id": "PlaybackFinished",
+ "extends": "Event",
+ "description": "Event showing the completion of a media playback operation.",
+ "properties": {
+ "playback": {
+ "type": "Playback",
+ "description": "Playback control object",
+ "required": true
+ }
+ }
+ },
+ "ApplicationReplaced": {
+ "id": "ApplicationReplaced",
+ "extends": "Event",
+ "description": "Notification that another WebSocket has taken over for an application.\n\nAn application may only be subscribed to by a single WebSocket at a time. If multiple WebSockets attempt to subscribe to the same application, the newer WebSocket wins, and the older one receives this event.",
+ "properties": {}
+ },
+ "BridgeCreated": {
+ "id": "BridgeCreated",
+ "extends": "Event",
+ "description": "Notification that a bridge has been created.",
+ "properties": {
+ "bridge": {
+ "required": true,
+ "type": "Bridge"
+ }
+ }
+ },
+ "BridgeDestroyed": {
+ "id": "BridgeDestroyed",
+ "extends": "Event",
+ "description": "Notification that a bridge has been destroyed.",
+ "properties": {
+ "bridge": {
+ "required": true,
+ "type": "Bridge"
+ }
+ }
+ },
+ "BridgeMerged": {
+ "id": "BridgeMerged",
+ "extends": "Event",
+ "description": "Notification that one bridge has merged into another.",
+ "properties": {
+ "bridge": {
+ "required": true,
+ "type": "Bridge"
+ },
+ "bridge_from": {
+ "required": true,
+ "type": "Bridge"
+ }
+ }
+ },
+ "ChannelCreated": {
+ "id": "ChannelCreated",
+ "extends": "Event",
+ "description": "Notification that a channel has been created.",
+ "properties": {
+ "channel": {
+ "required": true,
+ "type": "Channel"
+ }
+ }
+ },
+ "ChannelDestroyed": {
+ "id": "ChannelDestroyed",
+ "extends": "Event",
+ "description": "Notification that a channel has been destroyed.",
+ "properties": {
+ "cause": {
+ "required": true,
+ "description": "Integer representation of the cause of the hangup",
+ "type": "int"
+ },
+ "cause_txt": {
+ "required": true,
+ "description": "Text representation of the cause of the hangup",
+ "type": "string"
+ },
+ "channel": {
+ "required": true,
+ "type": "Channel"
+ }
+ }
+ },
+ "ChannelEnteredBridge": {
+ "id": "ChannelEnteredBridge",
+ "extends": "Event",
+ "description": "Notification that a channel has entered a bridge.",
+ "properties": {
+ "bridge": {
+ "required": true,
+ "type": "Bridge"
+ },
+ "channel": {
+ "type": "Channel"
+ }
+ }
+ },
+ "ChannelLeftBridge": {
+ "id": "ChannelLeftBridge",
+ "extends": "Event",
+ "description": "Notification that a channel has left a bridge.",
+ "properties": {
+ "bridge": {
+ "required": true,
+ "type": "Bridge"
+ },
+ "channel": {
+ "required": true,
+ "type": "Channel"
+ }
+ }
+ },
+ "ChannelStateChange": {
+ "id": "ChannelStateChange",
+ "extends": "Event",
+ "description": "Notification of a channel's state change.",
+ "properties": {
+ "channel": {
+ "required": true,
+ "type": "Channel"
+ }
+ }
+ },
+ "ChannelDtmfReceived": {
+ "id": "ChannelDtmfReceived",
+ "extends": "Event",
+ "description": "DTMF received on a channel.\n\nThis event is sent when the DTMF ends. There is no notification about the start of DTMF",
+ "properties": {
+ "digit": {
+ "required": true,
+ "type": "string",
+ "description": "DTMF digit received (0-9, A-E, # or *)"
+ },
+ "duration_ms": {
+ "required": true,
+ "type": "int",
+ "description": "Number of milliseconds DTMF was received"
+ },
+ "channel": {
+ "required": true,
+ "type": "Channel",
+ "description": "The channel on which DTMF was received"
+ }
+ }
+ },
+ "ChannelDialplan": {
+ "id": "ChannelDialplan",
+ "extends": "Event",
+ "description": "Channel changed location in the dialplan.",
+ "properties": {
+ "channel": {
+ "required": true,
+ "type": "Channel",
+ "description": "The channel that changed dialplan location."
+ },
+ "dialplan_app": {
+ "required": true,
+ "type": "string",
+ "description": "The application about to be executed."
+ },
+ "dialplan_app_data": {
+ "required": true,
+ "type": "string",
+ "description": "The data to be passed to the application."
+ }
+ }
+ },
+ "ChannelCallerId": {
+ "id": "ChannelCallerId",
+ "extends": "Event",
+ "description": "Channel changed Caller ID.",
+ "properties": {
+ "caller_presentation": {
+ "required": true,
+ "type": "int",
+ "description": "The integer representation of the Caller Presentation value."
+ },
+ "caller_presentation_txt": {
+ "required": true,
+ "type": "string",
+ "description": "The text representation of the Caller Presentation value."
+ },
+ "channel": {
+ "required": true,
+ "type": "Channel",
+ "description": "The channel that changed Caller ID."
+ }
+ }
+ },
+ "ChannelUserevent": {
+ "id": "ChannelUserevent",
+ "extends": "Event",
+ "description": "User-generated event with additional user-defined fields in the object.",
+ "properties": {
+ "eventname": {
+ "required": true,
+ "type": "string",
+ "description": "The name of the user event."
+ },
+ "channel": {
+ "required": true,
+ "type": "Channel",
+ "description": "The channel that signaled the user event."
+ },
+ "userevent": {
+ "required": true,
+ "type": "object",
+ "description": "Custom Userevent data"
+ }
+ }
+ },
+ "ChannelHangupRequest": {
+ "id": "ChannelHangupRequest",
+ "extends": "Event",
+ "description": "A hangup was requested on the channel.",
+ "properties": {
+ "cause": {
+ "type": "int",
+ "description": "Integer representation of the cause of the hangup."
+ },
+ "soft": {
+ "type": "boolean",
+ "description": "Whether the hangup request was a soft hangup request."
+ },
+ "channel": {
+ "required": true,
+ "type": "Channel",
+ "description": "The channel on which the hangup was requested."
+ }
+ }
+ },
+ "ChannelVarset": {
+ "id": "ChannelVarset",
+ "extends": "Event",
+ "description": "Channel variable changed.",
+ "properties": {
+ "variable": {
+ "required": true,
+ "type": "string",
+ "description": "The variable that changed."
+ },
+ "value": {
+ "required": true,
+ "type": "string",
+ "description": "The new value of the variable."
+ },
+ "channel": {
+ "required": false,
+ "type": "Channel",
+ "description": "The channel on which the variable was set.\n\nIf missing, the variable is a global variable."
+ }
+ }
+ },
+ "EndpointStateChange": {
+ "id": "EndpointStateChange",
+ "extends": "Event",
+ "description": "Endpoint state changed.",
+ "properties": {
+ "endpoint": {
+ "required": true,
+ "type": "Endpoint"
+ }
+ }
+ },
+ "StasisEnd": {
+ "id": "StasisEnd",
+ "extends": "Event",
+ "description": "Notification that a channel has left a Stasis appliction.",
+ "properties": {
+ "channel": {
+ "required": true,
+ "type": "Channel"
+ }
+ }
+ },
+ "StasisStart": {
+ "id": "StasisStart",
+ "extends": "Event",
+ "description": "Notification that a channel has entered a Stasis appliction.",
+ "properties": {
+ "args": {
+ "required": true,
+ "type": "List[string]",
+ "description": "Arguments to the application"
+ },
+ "channel": {
+ "required": true,
+ "type": "Channel"
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/playback.json b/sample-api/playback.json
new file mode 100644
index 0000000..6cad428
--- /dev/null
+++ b/sample-api/playback.json
@@ -0,0 +1,143 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401259 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/playback.{format}",
+ "apis": [
+ {
+ "path": "/playback/{playbackId}",
+ "description": "Control object for a playback operation.",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Get a playback's details.",
+ "nickname": "get",
+ "responseClass": "Playback",
+ "parameters": [
+ {
+ "name": "playbackId",
+ "description": "Playback's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Stop a playback.",
+ "nickname": "stop",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "playbackId",
+ "description": "Playback's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/playback/{playbackId}/control",
+ "description": "Control object for a playback operation.",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Control a playback.",
+ "nickname": "control",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "playbackId",
+ "description": "Playback's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ },
+ {
+ "name": "operation",
+ "description": "Operation to perform on the playback.",
+ "paramType": "query",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "restart",
+ "pause",
+ "unpause",
+ "reverse",
+ "forward"
+ ]
+ }
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 400,
+ "reason": "The provided operation parameter was invalid"
+ },
+ {
+ "code": 404,
+ "reason": "The playback cannot be found"
+ },
+ {
+ "code": 409,
+ "reason": "The operation cannot be performed in the playback's current state"
+ }
+]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "Playback": {
+ "id": "Playback",
+ "description": "Object representing the playback of media to a channel",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "ID for this playback operation",
+ "required": true
+ },
+ "media_uri": {
+ "type": "string",
+ "description": "URI for the media to play back.",
+ "required": true
+ },
+ "target_uri": {
+ "type": "string",
+ "description": "URI for the channel or bridge to play the media on",
+ "required": true
+ },
+ "language": {
+ "type": "string",
+ "description": "For media types that support multiple languages, the language requested for playback."
+ },
+ "state": {
+ "type": "string",
+ "description": "Current state of the playback operation.",
+ "required": true,
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "queued",
+ "playing",
+ "complete"
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/recordings.json b/sample-api/recordings.json
new file mode 100644
index 0000000..5fd224e
--- /dev/null
+++ b/sample-api/recordings.json
@@ -0,0 +1,329 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401312 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/recordings.{format}",
+ "apis": [
+ {
+ "path": "/recordings/stored",
+ "description": "Recordings",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List recordings that are complete.",
+ "nickname": "listStored",
+ "responseClass": "List[StoredRecording]"
+ }
+ ]
+ },
+ {
+ "path": "/recordings/stored/{recordingName}",
+ "description": "Individual recording",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Get a stored recording's details.",
+ "nickname": "getStored",
+ "responseClass": "StoredRecording",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Delete a stored recording.",
+ "nickname": "deleteStored",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/recordings/live/{recordingName}",
+ "description": "A recording that is in progress",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List live recordings.",
+ "nickname": "getLive",
+ "responseClass": "LiveRecording",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ }
+ ]
+ },
+ {
+ "httpMethod": "DELETE",
+ "summary": "Stop a live recording and discard it.",
+ "nickname": "cancel",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/recordings/live/{recordingName}/stop",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Stop a live recording and store it.",
+ "nickname": "stop",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/recordings/live/{recordingName}/pause",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Pause a live recording.",
+ "notes": "Pausing a recording suspends silence detection, which will be restarted when the recording is unpaused. Paused time is not included in the accounting for maxDurationSeconds.",
+ "nickname": "pause",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ },
+ {
+ "code": 409,
+ "reason": "Recording not in session"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/recordings/live/{recordingName}/unpause",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Unpause a live recording.",
+ "nickname": "unpause",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ },
+ {
+ "code": 409,
+ "reason": "Recording not in session"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/recordings/live/{recordingName}/mute",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Mute a live recording.",
+ "notes": "Muting a recording suspends silence detection, which will be restarted when the recording is unmuted.",
+ "nickname": "mute",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ },
+ {
+ "code": 409,
+ "reason": "Recording not in session"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/recordings/live/{recordingName}/unmute",
+ "operations": [
+ {
+ "httpMethod": "POST",
+ "summary": "Unmute a live recording.",
+ "nickname": "unmute",
+ "responseClass": "void",
+ "parameters": [
+ {
+ "name": "recordingName",
+ "description": "The name of the recording",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ],
+ "errorResponses": [
+ {
+ "code": 404,
+ "reason": "Recording not found"
+ },
+ {
+ "code": 409,
+ "reason": "Recording not in session"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "StoredRecording": {
+ "id": "StoredRecording",
+ "description": "A past recording that may be played back.",
+ "properties": {
+ "name": {
+ "required": true,
+ "type": "string"
+ },
+ "format": {
+ "required": true,
+ "type": "string"
+ }
+ }
+ },
+ "LiveRecording": {
+ "id": "LiveRecording",
+ "description": "A recording that is in progress",
+ "properties": {
+ "name": {
+ "required": true,
+ "type": "string",
+ "description": "Base name for the recording"
+ },
+ "format": {
+ "required": true,
+ "type": "string",
+ "description": "Recording format (wav, gsm, etc.)"
+ },
+ "state": {
+ "required": false,
+ "type": "string",
+ "allowableValues": {
+ "valueType": "LIST",
+ "values": [
+ "queued",
+ "playing",
+ "paused",
+ "done"
+ ]
+ }
+ },
+ "state": {
+ "required": true,
+ "type": "string"
+ },
+ "format": {
+ "required": true,
+ "type": "string"
+ }
+ }
+ }
+ }
+}
diff --git a/sample-api/resources.json b/sample-api/resources.json
new file mode 100644
index 0000000..23fc4b8
--- /dev/null
+++ b/sample-api/resources.json
@@ -0,0 +1,46 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401096 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "apis": [
+ {
+ "path": "/api-docs/asterisk.{format}",
+ "description": "Asterisk resources"
+ },
+ {
+ "path": "/api-docs/endpoints.{format}",
+ "description": "Endpoint resources"
+ },
+ {
+ "path": "/api-docs/channels.{format}",
+ "description": "Channel resources"
+ },
+ {
+ "path": "/api-docs/bridges.{format}",
+ "description": "Bridge resources"
+ },
+ {
+ "path": "/api-docs/recordings.{format}",
+ "description": "Recording resources"
+ },
+ {
+ "path": "/api-docs/sounds.{format}",
+ "description": "Sound resources"
+ },
+ {
+ "path": "/api-docs/playback.{format}",
+ "description": "Playback control resources"
+ },
+ {
+ "path": "/api-docs/events.{format}",
+ "description": "WebSocket resource"
+ },
+ {
+ "path": "/api-docs/applications.{format}",
+ "description": "Stasis application resources"
+ }
+ ]
+}
diff --git a/sample-api/sounds.json b/sample-api/sounds.json
new file mode 100644
index 0000000..ad9c866
--- /dev/null
+++ b/sample-api/sounds.json
@@ -0,0 +1,99 @@
+{
+ "_copyright": "Copyright (C) 2012 - 2013, Digium, Inc.",
+ "_author": "David M. Lee, II ",
+ "_svn_revision": "$Revision: 401312 $",
+ "apiVersion": "0.0.0-test",
+ "swaggerVersion": "1.1",
+ "basePath": "http://ari.py/ari",
+ "resourcePath": "/api-docs/sounds.{format}",
+ "apis": [
+ {
+ "path": "/sounds",
+ "description": "Sounds",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "List all sounds.",
+ "nickname": "list",
+ "responseClass": "List[Sound]",
+ "parameters": [
+ {
+ "name": "lang",
+ "description": "Lookup sound for a specific language.",
+ "paramType": "query",
+ "dataType": "string",
+ "required": false
+ },
+ {
+ "name": "format",
+ "description": "Lookup sound in a specific format.",
+ "paramType": "query",
+ "dataType": "string",
+ "required": false,
+ "__note": "core show translation can show translation paths between formats, along with relative costs. so this could be just installed format, or we could follow that for transcoded formats."
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "path": "/sounds/{soundId}",
+ "description": "Individual sound",
+ "operations": [
+ {
+ "httpMethod": "GET",
+ "summary": "Get a sound's details.",
+ "nickname": "get",
+ "responseClass": "Sound",
+ "parameters": [
+ {
+ "name": "soundId",
+ "description": "Sound's id",
+ "paramType": "path",
+ "required": true,
+ "allowMultiple": false,
+ "dataType": "string"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "models": {
+ "FormatLangPair": {
+ "id": "FormatLangPair",
+ "description": "Identifies the format and language of a sound file",
+ "properties": {
+ "language": {
+ "required": true,
+ "type": "string"
+ },
+ "format": {
+ "required": true,
+ "type": "string"
+ }
+ }
+ },
+ "Sound": {
+ "id": "Sound",
+ "description": "A media file that may be played back.",
+ "properties": {
+ "id": {
+ "required": true,
+ "description": "Sound's identifier.",
+ "type": "string"
+ },
+ "text": {
+ "required": false,
+ "description": "Text description of the sound, usually the words spoken.",
+ "type": "string"
+ },
+ "formats": {
+ "required": true,
+ "description": "The formats and languages in which this sound is available.",
+ "type": "List[FormatLangPair]"
+ }
+ }
+ }
+ }
+}
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..ed40864
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[metadata]
+description-file = README.md
+
+[nosetests]
+config = nose.cfg
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..6be6cd1
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+
+#
+# Copyright (c) 2013, Digium, Inc.
+#
+
+import os
+
+from setuptools import setup
+
+setup(
+ name="ari",
+ version="0.1.0",
+ license="BSD 3-Clause License",
+ description="Library for accessing the Asterisk REST Interface",
+ long_description=open(os.path.join(os.path.dirname(__file__),
+ "README.rst")).read(),
+ author="Digium, Inc.",
+ url="https://github.com/asterisk/asterisk_rest_libraries",
+ packages=["ari"],
+ classifiers=[
+ "Development Status :: 1 - Planning",
+ "Intended Audience :: Developers",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ ],
+ tests_require=["coverage", "httpretty", "nose", "tissue"],
+ install_requires=["swaggerpy"],
+)