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"], +)