Skip to content

Commit 6d7ee32

Browse files
author
Samuel Riolo
committedJun 30, 2015
Initial commit for GitHub
1 parent 7f9e547 commit 6d7ee32

29 files changed

+1924
-0
lines changed
 

‎CHANGES.rst

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Changelog
2+
=========
3+
4+
1.0-dev (unreleased)
5+
--------------------
6+
7+
- Initial
8+
[Samuel Riolo]

‎LICENSE.txt

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
GNU LESSER GENERAL PUBLIC LICENSE
2+
Version 3, 29 June 2007
3+
4+
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5+
Everyone is permitted to copy and distribute verbatim copies
6+
of this license document, but changing it is not allowed.
7+
8+
9+
This version of the GNU Lesser General Public License incorporates
10+
the terms and conditions of version 3 of the GNU General Public
11+
License, supplemented by the additional permissions listed below.
12+
13+
0. Additional Definitions.
14+
15+
As used herein, "this License" refers to version 3 of the GNU Lesser
16+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
17+
General Public License.
18+
19+
"The Library" refers to a covered work governed by this License,
20+
other than an Application or a Combined Work as defined below.
21+
22+
An "Application" is any work that makes use of an interface provided
23+
by the Library, but which is not otherwise based on the Library.
24+
Defining a subclass of a class defined by the Library is deemed a mode
25+
of using an interface provided by the Library.
26+
27+
A "Combined Work" is a work produced by combining or linking an
28+
Application with the Library. The particular version of the Library
29+
with which the Combined Work was made is also called the "Linked
30+
Version".
31+
32+
The "Minimal Corresponding Source" for a Combined Work means the
33+
Corresponding Source for the Combined Work, excluding any source code
34+
for portions of the Combined Work that, considered in isolation, are
35+
based on the Application, and not on the Linked Version.
36+
37+
The "Corresponding Application Code" for a Combined Work means the
38+
object code and/or source code for the Application, including any data
39+
and utility programs needed for reproducing the Combined Work from the
40+
Application, but excluding the System Libraries of the Combined Work.
41+
42+
1. Exception to Section 3 of the GNU GPL.
43+
44+
You may convey a covered work under sections 3 and 4 of this License
45+
without being bound by section 3 of the GNU GPL.
46+
47+
2. Conveying Modified Versions.
48+
49+
If you modify a copy of the Library, and, in your modifications, a
50+
facility refers to a function or data to be supplied by an Application
51+
that uses the facility (other than as an argument passed when the
52+
facility is invoked), then you may convey a copy of the modified
53+
version:
54+
55+
a) under this License, provided that you make a good faith effort to
56+
ensure that, in the event an Application does not supply the
57+
function or data, the facility still operates, and performs
58+
whatever part of its purpose remains meaningful, or
59+
60+
b) under the GNU GPL, with none of the additional permissions of
61+
this License applicable to that copy.
62+
63+
3. Object Code Incorporating Material from Library Header Files.
64+
65+
The object code form of an Application may incorporate material from
66+
a header file that is part of the Library. You may convey such object
67+
code under terms of your choice, provided that, if the incorporated
68+
material is not limited to numerical parameters, data structure
69+
layouts and accessors, or small macros, inline functions and templates
70+
(ten or fewer lines in length), you do both of the following:
71+
72+
a) Give prominent notice with each copy of the object code that the
73+
Library is used in it and that the Library and its use are
74+
covered by this License.
75+
76+
b) Accompany the object code with a copy of the GNU GPL and this license
77+
document.
78+
79+
4. Combined Works.
80+
81+
You may convey a Combined Work under terms of your choice that,
82+
taken together, effectively do not restrict modification of the
83+
portions of the Library contained in the Combined Work and reverse
84+
engineering for debugging such modifications, if you also do each of
85+
the following:
86+
87+
a) Give prominent notice with each copy of the Combined Work that
88+
the Library is used in it and that the Library and its use are
89+
covered by this License.
90+
91+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
92+
document.
93+
94+
c) For a Combined Work that displays copyright notices during
95+
execution, include the copyright notice for the Library among
96+
these notices, as well as a reference directing the user to the
97+
copies of the GNU GPL and this license document.
98+
99+
d) Do one of the following:
100+
101+
0) Convey the Minimal Corresponding Source under the terms of this
102+
License, and the Corresponding Application Code in a form
103+
suitable for, and under terms that permit, the user to
104+
recombine or relink the Application with a modified version of
105+
the Linked Version to produce a modified Combined Work, in the
106+
manner specified by section 6 of the GNU GPL for conveying
107+
Corresponding Source.
108+
109+
1) Use a suitable shared library mechanism for linking with the
110+
Library. A suitable mechanism is one that (a) uses at run time
111+
a copy of the Library already present on the user's computer
112+
system, and (b) will operate properly with a modified version
113+
of the Library that is interface-compatible with the Linked
114+
Version.
115+
116+
e) Provide Installation Information, but only if you would otherwise
117+
be required to provide such information under section 6 of the
118+
GNU GPL, and only to the extent that such information is
119+
necessary to install and execute a modified version of the
120+
Combined Work produced by recombining or relinking the
121+
Application with a modified version of the Linked Version. (If
122+
you use option 4d0, the Installation Information must accompany
123+
the Minimal Corresponding Source and Corresponding Application
124+
Code. If you use option 4d1, you must provide the Installation
125+
Information in the manner specified by section 6 of the GNU GPL
126+
for conveying Corresponding Source.)
127+
128+
5. Combined Libraries.
129+
130+
You may place library facilities that are a work based on the
131+
Library side by side in a single library together with other library
132+
facilities that are not Applications and are not covered by this
133+
License, and convey such a combined library under terms of your
134+
choice, if you do both of the following:
135+
136+
a) Accompany the combined library with a copy of the same work based
137+
on the Library, uncombined with any other library facilities,
138+
conveyed under the terms of this License.
139+
140+
b) Give prominent notice with the combined library that part of it
141+
is a work based on the Library, and explaining where to find the
142+
accompanying uncombined form of the same work.
143+
144+
6. Revised Versions of the GNU Lesser General Public License.
145+
146+
The Free Software Foundation may publish revised and/or new versions
147+
of the GNU Lesser General Public License from time to time. Such new
148+
versions will be similar in spirit to the present version, but may
149+
differ in detail to address new problems or concerns.
150+
151+
Each version is given a distinguishing version number. If the
152+
Library as you received it specifies that a certain numbered version
153+
of the GNU Lesser General Public License "or any later version"
154+
applies to it, you have the option of following the terms and
155+
conditions either of that published version or of any later version
156+
published by the Free Software Foundation. If the Library as you
157+
received it does not specify a version number of the GNU Lesser
158+
General Public License, you may choose any version of the GNU Lesser
159+
General Public License ever published by the Free Software Foundation.
160+
161+
If the Library as you received it specifies that a proxy can decide
162+
whether future versions of the GNU Lesser General Public License shall
163+
apply, that proxy's public statement of acceptance of any version is
164+
permanent authorization for you to choose that version for the
165+
Library.

‎MANIFEST.in

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include *.txt *.rst
2+
include tox.ini
3+
include .travis.yml
4+
recursive-include src/pyaxl *.xml *.txt *.py

‎README.rst

+283
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
2+
.. contents::
3+
4+
What is pyaxl
5+
-------------
6+
7+
pyaxl is a python library accessing the Cisco Callmanger over the AXL interface. This library was build
8+
inspired by `the work of Sebastian Kratzert <http://kra-se.de/pyAXL/>`_ and works as a
9+
second layer over SUDS, which contains further improvements by `jurko <https://bitbucket.org/jurko/suds>`_.
10+
We use `SoupUI <http://www.soapui.org/>`_ and recommend it if you want to work with this library, as it helps
11+
analizing and understanding how the WSDL from Cisco Callmanager is composed.
12+
13+
pyaxl is licensed under the LGPL 3, see COPYING.txt for details.
14+
15+
16+
Import WSDL
17+
-----------
18+
The WSDL files are not included with this library due to licenses terms. pyaxl provides
19+
a script to import it and then build a cache directly into the library.
20+
21+
First of all you need to download the WSDL files. The AXL WSDL is included in the AXL SQL Toolkit download,
22+
which is available in Cisco Unified CM. Follow these steps to download the AXL SQL Toolkit from your Cisco
23+
Unified CM server:
24+
25+
1. Log into the Cisco Unified CM Administration application.
26+
2. Go to Application | Plugins
27+
3. Click on the Download link by the Cisco CallManager AXL SQL Toolkit Plugin.
28+
29+
The axlsqltoolkit.zip file contains the complete schema definition for different versions of Cisco Unified CM.
30+
The important files for each version are:
31+
* AXLAPI.wsdl
32+
* AXLEnums.xsd
33+
* axlmessage.xsd
34+
* axlsoap.xsd
35+
* axl.xsd
36+
37+
Note: all files must be in the same directory and have the same name as the version you want use.
38+
39+
.. code-block:: bash
40+
41+
$ ./pyaxl_import_wsdl -p path_to_wsdl/10.5/AXLAPI.wsdl
42+
43+
Hint: We put all these file in the buildout directory. While buildout is running, the WSDL files are imported automatically.
44+
45+
.. code-block:: ini
46+
47+
[buildout]
48+
parts =
49+
pyaxl_import
50+
pyaxl_import_exec
51+
52+
[pyaxl_import]
53+
recipe = zc.recipe.egg:scripts
54+
eggs = pyaxl
55+
scripts=pyaxl_import_wsdl=import_wsdl
56+
57+
[pyaxl_import_exec]
58+
recipe = collective.recipe.cmd
59+
on_install=true
60+
on_update=true
61+
cmds = ${buildout:directory}/bin/import_wsdl -p ${buildout:directory}/wsdl/10.5/AXLAPI.wsdl
62+
63+
64+
Configuration
65+
-------------
66+
67+
>>> import pyaxl
68+
>>> from pyaxl import ccm
69+
>>> from pyaxl.testing import validate
70+
>>> from pyaxl.testing.transport import TestingTransport
71+
72+
For these tests we use a fake transport layer. For this we must tell which xml
73+
the transporter should use for the response.
74+
75+
>>> transport = TestingTransport()
76+
>>> transport.define('10.5_user_riols')
77+
>>> transport_testing = TestingTransport()
78+
>>> transport_testing.define('8.0_user_riols')
79+
80+
>>> settings = pyaxl.AXLClientSettings(host='https://callmanger.fake:8443',
81+
... user='super-admin',
82+
... passwd='nobody knows',
83+
... path='/axl/',
84+
... version='10.5',
85+
... suds_config=dict(transport=transport))
86+
>>> pyaxl.registry.register(settings)
87+
88+
pyaxl supports multiple settings. To use that, pass the configuration name as
89+
second attribute in the register method.
90+
91+
>>> settings_testing = pyaxl.AXLClientSettings(host='https://callmanger-testing.fake:8443',
92+
... user='super-admin',
93+
... passwd='nobody knows',
94+
... path='/axl/',
95+
... version='8.0',
96+
... suds_config=dict(transport=transport_testing))
97+
>>> pyaxl.registry.register(settings_testing, 'testing')
98+
99+
if you want to use a custom configuration, you also need to pass
100+
it when you are getting the object:
101+
102+
>>> user = ccm.User('riols', configname='testing')
103+
104+
if you don't need multiple settings, you can just use the default.
105+
106+
>>> user = ccm.User('riols')
107+
108+
Don't forget to build the cache for the defined configuration name:
109+
110+
.. code-block:: bash
111+
112+
$ ./pyaxl_import_wsdl -p -c testing path_to_wsdl/10.5/AXLAPI.wsdl
113+
114+
115+
Working with pyaxl
116+
------------------
117+
118+
Get all information for a specific user.
119+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
120+
121+
>>> transport.define('10.5_user_riols')
122+
>>> user1 = ccm.User('riols')
123+
124+
>>> validate.printSOAPRequest(transport.lastrequest())
125+
getUser:
126+
userid=riols
127+
128+
>>> user1.firstName
129+
Samuel
130+
>>> user1.lastName
131+
Riolo
132+
133+
134+
Get the same user with his UUID.
135+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
136+
137+
>>> transport.define('10.5_user_riols')
138+
>>> user2 = ccm.User(uuid='{5B5C014F-63A8-412F-B793-782BDA987371}')
139+
>>> user1._uuid == user2._uuid
140+
True
141+
142+
143+
Search and list information
144+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
145+
146+
>>> transport.define('10.5_user_armstrong')
147+
>>> users = ccm.User.list(dict(lastName='Armstrong'), ('firstName', 'lastName'))
148+
>>> validate.printSOAPRequest(transport.lastrequest())
149+
listUser:
150+
searchCriteria:
151+
lastName=Armstrong
152+
returnedTags:
153+
firstName=True
154+
lastName=True
155+
156+
>>> list(users)
157+
[(Lance, Armstrong), (Neil, Armstrong)]
158+
159+
160+
Search and fetch information as objects
161+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
162+
163+
>>> transport.define('10.5_user_riols')
164+
>>> users = ccm.User.list_obj(dict(lastName='Riolo', firstName='Samuel'))
165+
>>> for user in users:
166+
... print(user.firstName, user.lastName)
167+
Samuel Riolo
168+
169+
170+
Reload an object
171+
~~~~~~~~~~~~~~~~
172+
173+
>>> transport.define('10.5_user_riols')
174+
>>> user = ccm.User('riols')
175+
>>> user.firstName = 'Yuri'
176+
>>> user.lastName = 'Gagarin'
177+
>>> print(user.firstName, user.lastName)
178+
Yuri Gagarin
179+
>>> user.reload()
180+
Traceback (most recent call last):
181+
...
182+
pyaxl.exceptions.ReloadException: Error because some field are already changed by the client. Use force or update it first.
183+
>>> user.reload(force=True)
184+
>>> print(user.firstName, user.lastName)
185+
Samuel Riolo
186+
187+
188+
Update an object
189+
~~~~~~~~~~~~~~~~
190+
191+
>>> transport.define('10.5_user_riols')
192+
>>> user = ccm.User('riols')
193+
>>> user.firstName = 'Claude'
194+
>>> user.lastName = 'Nicollier'
195+
>>> user.update()
196+
>>> validate.printSOAPRequest(transport.lastrequest())
197+
updateUser:
198+
uuid={5B5C014F-63A8-412F-B793-782BDA987371}
199+
firstName=Claude
200+
lastName=Nicollier
201+
202+
203+
Remove an object
204+
~~~~~~~~~~~~~~~~
205+
206+
>>> transport.define('10.5_user_riols')
207+
>>> user = ccm.User('riols')
208+
>>> user.remove()
209+
>>> validate.printSOAPRequest(transport.lastrequest())
210+
removeUser:
211+
uuid={5B5C014F-63A8-412F-B793-782BDA987371}
212+
213+
214+
Create a new object
215+
~~~~~~~~~~~~~~~~~~~
216+
217+
>>> transport.define('10.5_user_riols')
218+
>>> user = ccm.User()
219+
>>> user.lastName = 'Edison'
220+
>>> user.firstName = 'Thomas'
221+
>>> user.userid = 'tedison'
222+
>>> user.presenceGroupName = 'SC Presence Group'
223+
>>> user.ipccExtension = None
224+
>>> user.ldapDirectoryName = None
225+
>>> user.userProfile = None
226+
>>> user.serviceProfile = None
227+
>>> user.primaryDevice = None
228+
>>> user.pinCredentials = None
229+
>>> user.passwordCredentials = None
230+
>>> user.subscribeCallingSearchSpaceName = None
231+
>>> user.defaultProfile = None
232+
>>> user.convertUserAccount = None
233+
234+
>>> user.update()
235+
Traceback (most recent call last):
236+
...
237+
pyaxl.exceptions.UpdateException: you must create a object with "create" before update
238+
239+
>>> user.create()
240+
{12345678-1234-1234-1234-123123456789}
241+
>>> validate.printSOAPRequest(transport.lastrequest())
242+
addUser:
243+
user:
244+
firstName=Thomas
245+
lastName=Edison
246+
userid=tedison
247+
presenceGroupName=SC Presence Group
248+
249+
250+
If you try to create a user twice, an Exception of the type CreationException is thrown:
251+
252+
>>> user.create()
253+
Traceback (most recent call last):
254+
...
255+
pyaxl.exceptions.CreationException: this object are already attached
256+
257+
258+
Clone an object
259+
~~~~~~~~~~~~~~~
260+
261+
>>> transport.define('10.5_user_riols')
262+
>>> user = ccm.User('riols')
263+
>>> clone = user.clone()
264+
>>> clone.userid = 'riols2'
265+
>>> clone.update()
266+
Traceback (most recent call last):
267+
...
268+
pyaxl.exceptions.UpdateException: you must create a object with "create" before update
269+
>>> clone.create()
270+
{12345678-1234-1234-1234-123123456789}
271+
272+
273+
Running the doc tests
274+
---------------------
275+
276+
.. code-block:: bash
277+
278+
$ tox -- <path to axlsqltoolkit directory>
279+
280+
281+
:Author: Samuel Riolo
282+
:Organization:Biel/Bienne
283+
:Contact: samuel.riolo@biel-bienne.ch

‎setup.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from setuptools import setup, find_packages
2+
import os
3+
4+
version = '1.0'
5+
6+
long_description = (
7+
open('README.rst').read()
8+
+ '\n' +
9+
open('CHANGES.rst').read()
10+
+ '\n')
11+
12+
setup(name='pyaxl',
13+
version=version,
14+
description="pyaxl is a python library accessing the Cisco Callmanger over the AXL interface",
15+
long_description=long_description,
16+
# Get more strings from
17+
# http://pypi.python.org/pypi?%3Aaction=list_classifiers
18+
classifiers=[
19+
'Programming Language :: Python :: 3 :: Only',
20+
'Natural Language :: English',
21+
'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)',
22+
'Operating System :: OS Independent',
23+
'Development Status :: 4 - Beta'
24+
25+
],
26+
keywords='bielbienne cisco callmanger axl soap',
27+
author='Samuel Riolo',
28+
author_email='samuel.riolo@biel-bienne.ch',
29+
url='https://github.com/bielbienne/pyaxl',
30+
license='lgpl',
31+
packages=find_packages('src'),
32+
package_dir={'': 'src'},
33+
include_package_data=True,
34+
zip_safe=False,
35+
install_requires=[
36+
'setuptools',
37+
'suds-jurko'
38+
],
39+
tests_require=[
40+
'suds-jurko',
41+
],
42+
test_suite='pyaxl.testing.test_suite',
43+
entry_points={
44+
'console_scripts': ['pyaxl_import_wsdl = pyaxl.axlhandler:import_wsdl'],
45+
})

‎src/pyaxl/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cache

‎src/pyaxl/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from pyaxl.configuration import registry
2+
from pyaxl.configuration import AXLClientSettings

‎src/pyaxl/axlhandler.py

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import os
2+
import sys
3+
import pyaxl
4+
import shutil
5+
import logging
6+
import fnmatch
7+
import argparse
8+
9+
from suds.xsd import doctor
10+
from suds.client import Client
11+
from suds.reader import Reader
12+
from suds.cache import ObjectCache
13+
from suds.plugin import MessagePlugin
14+
from suds.transport.http import HttpAuthenticated
15+
16+
17+
FILE_PREFIX = 'file://%s'
18+
AXLAPI = 'AXLAPI.wsdl'
19+
20+
Logger = logging.Logger('pyaxl')
21+
22+
23+
def mangle_patch(self, url, x):
24+
"""
25+
Mangle the name by hashing the I{name} and appending I{x}.
26+
@return: the mangled name.
27+
"""
28+
version, filename = url.rsplit('/')[-2:]
29+
return '%s-%s-%s' % (version, filename, x)
30+
Reader.mangle = mangle_patch
31+
Logger.warning('"suds.reader.Reader.mangle" patched')
32+
33+
34+
class AXLImportDoctor(doctor.ImportDoctor):
35+
""" Reads automatically XSD Schema from a given path.
36+
"""
37+
38+
def __init__(self, xsd_location):
39+
self.xsd_location = xsd_location
40+
super(AXLImportDoctor, self).__init__(*self.imports())
41+
42+
def imports(self):
43+
imports = list()
44+
for i in os.listdir(self.xsd_location):
45+
xsd = os.path.join(self.xsd_location, i)
46+
if fnmatch.fnmatch(xsd, '*.xsd'):
47+
imports.append(doctor.Import(FILE_PREFIX % xsd))
48+
return imports
49+
50+
51+
class DebugTransportPlugin(MessagePlugin):
52+
""" special http transport that will ask for sending each
53+
request.
54+
"""
55+
56+
def sending(self, context):
57+
print('\n\n')
58+
print('=' * 100)
59+
print(context.envelope)
60+
print('=' * 100)
61+
msg = '\nAre you sure you want to send this request to callmanager? [yes|no]'
62+
if input(msg).lower() in ('yes', 'y'):
63+
return super(DebugHttpAuthenicated, self).send(request)
64+
print('request aborted!')
65+
sys.exit(1)
66+
67+
68+
class AXLClient(Client):
69+
""" AXL client to handle all soap request to callmanager.
70+
"""
71+
72+
clients = dict()
73+
74+
def __init__(self, configname='default'):
75+
76+
wsdl = None
77+
importdoctor = None
78+
config = pyaxl.configuration.registry.get(configname)
79+
if config.schema_path is None:
80+
modpath = os.path.dirname(pyaxl.__file__)
81+
cachefiles = os.path.join(get_cache_path(configname), 'files')
82+
if not os.path.exists(cachefiles):
83+
print('Cache for configuration "%s" doesn\'t exist. Use pyaxl_import_wsdl_to create it first!' % configname,
84+
file=sys.stderr)
85+
raise Exception('Path for cache doesn\'t exist')
86+
with open(cachefiles) as f:
87+
wsdl = f.readline().strip()
88+
importdoctor = doctor.ImportDoctor(*[doctor.Import(l.strip()) for l in f.readlines()])
89+
else:
90+
schema_path = config.schema_path
91+
wsdl = os.path.join(schema_path, AXLAPI)
92+
if not os.path.exists(wsdl):
93+
raise ValueError('The version %s is not supported. WSDL was not found.' % config.version)
94+
wsdl = FILE_PREFIX % wsdl
95+
importdoctor = AXLImportDoctor(schema_path)
96+
httpconfig = dict(username=config.user, password=config.passwd, proxy=config.proxy)
97+
transport = HttpAuthenticated(**httpconfig)
98+
plugins = list()
99+
if config.transport_debugger:
100+
plugins.append(DebugTransportPlugin())
101+
102+
kwargs = dict(cache=get_cache(configname),
103+
location='%s/%s' % (config.host, config.path),
104+
doctor=importdoctor,
105+
plugins=plugins,
106+
transport=transport)
107+
kwargs.update(config.suds_config)
108+
super(AXLClient, self).__init__(wsdl, **kwargs)
109+
110+
@classmethod
111+
def get_client(cls, configname='default', recreate=False):
112+
""" return a single instance of client for each configuration.
113+
"""
114+
client = None
115+
if configname not in cls.clients or recreate:
116+
client = AXLClient(configname)
117+
return cls.clients.setdefault(configname, client)
118+
119+
120+
def get_cache_path(configname):
121+
name = '%s.cache' % configname
122+
return os.path.join(os.path.dirname(pyaxl.__file__), 'cache', name)
123+
124+
125+
def get_cache(configname):
126+
return ObjectCache(get_cache_path(configname))
127+
128+
129+
def import_wsdl():
130+
parser = argparse.ArgumentParser(description='''Import Cisco's WSDL. The WSDL must be in a
131+
directory with the version as his name. All
132+
additional XSD must be in the same directory.''')
133+
parser.add_argument('-c', '--configname', type=str, default='default', dest='configname',
134+
help='''Name of the configuration because pyaxl support multiple
135+
configuration. Empty for the default configuration name.''')
136+
parser.add_argument('-p', '--purge', default=False, dest='purge', action='store_true',
137+
help='''Purge old cache files if already exists''')
138+
parser.add_argument('source', metavar='AXLAPI.wsdl', type=str,
139+
help='Path to AXLAPI.wsdl')
140+
args = parser.parse_args()
141+
142+
if not os.path.exists(args.source):
143+
print('AXL doesn\'t exist', file=sys.stderr)
144+
sys.exit(1)
145+
146+
source = os.path.abspath(args.source)
147+
doctor = AXLImportDoctor(os.path.dirname(source))
148+
cache = get_cache(args.configname)
149+
150+
if os.path.exists(os.path.join(cache.location, 'files')):
151+
if args.purge:
152+
shutil.rmtree(cache.location)
153+
print('WSDL cache at %s cleared' % cache.location)
154+
else:
155+
print('Fail: cache of WSDL already exist. Use -p to purge it.', file=sys.stderr)
156+
sys.exit(1)
157+
158+
client = Client(FILE_PREFIX % source, cache=cache, doctor=doctor)
159+
with open(os.path.join(client.options.cache.location, 'files'), 'w') as f:
160+
f.write('%s\n' % client.wsdl.url)
161+
for imp in doctor.imports:
162+
f.write('%s\n' % imp.ns)
163+
f.close()

‎src/pyaxl/axlsql.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import logging
2+
from pyaxl import utils
3+
from pyaxl.axlhandler import AXLClient
4+
5+
log = logging.getLogger('pyaxl')
6+
7+
8+
class AXLSQL(object):
9+
10+
def __init__(self, configname):
11+
self.client = AXLClient.get_client(configname)
12+
13+
def _exec(self, sql):
14+
log.info('Execute SqlQuery "%s"' % sql)
15+
return self.client.service.executeSQLQuery(sql)
16+
17+
def _execupdate(self, sql):
18+
log.info('Execute SqlUpdate "%s"' % sql)
19+
return self.client.service.executeSQLUpdate(sql)
20+
21+
def _genresult(self, dom_or_part, ispart=False):
22+
if not ispart:
23+
if 'row' not in dom_or_part['return']:
24+
return None
25+
li = dom_or_part['return']['row']
26+
if len(li) < 1:
27+
return None
28+
if len(li) > 1:
29+
raise ValueError('too many results.')
30+
dom_or_part = li[0]
31+
return dict(dom_or_part)
32+
33+
def _genresultlist(self, dom):
34+
if 'row' not in dom['return']:
35+
return
36+
for part in dom['return']['row']:
37+
yield self._genresult(part, True)
38+
39+
def _tobool(self, value):
40+
return 't' if bool(value) else 'f'
41+
42+
43+
class AXLSQLUtils(AXLSQL):
44+
45+
def user_phone_association(self, fkenduser):
46+
sql = 'SELECT * FROM extensionmobilitydynamic WHERE fkenduser="%(fkenduser)s"'
47+
return self._genresultlist(self._exec(sql % dict(fkenduser=utils.uuid(fkenduser))))
48+
49+
def has_cups_cupc(self, fkenduser):
50+
sql = 'SELECT * FROM enduserlicense WHERE fkenduser="%(fkenduser)s"'
51+
return self._genresult(self._exec(sql % dict(fkenduser=utils.uuid(fkenduser))))
52+
53+
def insert_cups(self, fkenduser, cupc):
54+
sql = 'INSERT INTO enduserlicense (fkenduser, enablecups, enablecupc) VALUES ("%(fkenduser)s", "t", "%(cupc)s")'
55+
self._execupdate(sql % dict(fkenduser=utils.uuid(fkenduser), cupc=self._tobool(cupc)))
56+
57+
def remove_cups(self, fkenduser):
58+
sql = 'DELETE FROM enduserlicense WHERE fkenduser = "%(fkenduser)s"'
59+
self._execupdate(sql % dict(fkenduser=utils.uuid(fkenduser)))
60+
61+
def update_cups(self, fkenduser, cupc):
62+
sql = 'UPDATE enduserlicense SET enablecupc = "%(cupc)s" WHERE fkenduser = "%(fkenduser)s"'
63+
self._execupdate(sql % dict(fkenduser=utils.uuid(fkenduser), cupc=self._tobool(cupc)))
64+
65+
def update_bfcp(self, fkenduser, bfcp):
66+
sql = 'UPDATE device SET enablebfcp = "%(bfcp)s" WHERE pkid = "%(fkenduser)s"'
67+
self._execupdate(sql % dict(fkenduser=utils.uuid(fkenduser), bfcp=self._tobool(bfcp)))
68+
69+
def set_single_number_reach(self, fkremotedestination, value):
70+
sql = 'UPDATE remotedestinationdynamic SET enablesinglenumberreach = "%(value)s" WHERE fkremotedestination = "%(fkremotedestination)s"'
71+
self._execupdate(sql % dict(fkremotedestination=utils.uuid(fkremotedestination), value=self._tobool(value)))
72+
73+
def get_single_number_reach(self, fkremotedestination):
74+
sql = 'SELECT enablesinglenumberreach FROM remotedestinationdynamic WHERE fkremotedestination = "%(fkremotedestination)s"'
75+
return self._genresult(self._exec(sql % dict(fkremotedestination=utils.uuid(fkremotedestination))))

‎src/pyaxl/ccm/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from pyaxl.ccm.common import *
2+
from pyaxl.ccm.xtypes import *

‎src/pyaxl/ccm/abstracts.py

+293
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
import pyaxl
2+
import logging
3+
from copy import copy
4+
from suds.sax.text import Text
5+
from suds.sudsobject import Object
6+
7+
from pyaxl import exceptions
8+
from pyaxl.axlhandler import AXLClient
9+
10+
11+
PF_LIST = 'list'
12+
PF_GET = 'get'
13+
PF_UPDATE = 'update'
14+
PF_ADD = 'add'
15+
PF_REMOVE = 'remove'
16+
XSD_NS = 'ns0'
17+
ESCAPES = dict(cls='class')
18+
19+
log = logging.getLogger('pyaxl')
20+
21+
22+
class BaseCCModel(Object):
23+
""" Provide base functionality for Abstract
24+
or XTypes Objects.
25+
"""
26+
27+
__name__ = ''
28+
__configname__ = ''
29+
__config__ = None
30+
__client__ = None
31+
__attached__ = False
32+
__updateable__ = list()
33+
34+
def __init__(self, *args, **kwargs):
35+
""" if no arguments are given this object will be created as
36+
empty object. Else it will find and fetch the data from
37+
callmanager and fill this object up.
38+
"""
39+
configname = 'default'
40+
if 'configname' in kwargs:
41+
configname = kwargs['configname']
42+
del kwargs['configname']
43+
self._configure(configname)
44+
self._initalize(args, kwargs)
45+
46+
def __setattr__(self, name, value):
47+
""" remember which attributes was changed. Generally used for
48+
the update method.
49+
"""
50+
if hasattr(self, '__keylist__') and name in self.__keylist__:
51+
self.__updateable__.append(name)
52+
super(BaseCCModel, self).__setattr__(name, value)
53+
54+
@classmethod
55+
def _axl_method(cls, prefix, name, client):
56+
""" return a function to call the callmanager.
57+
"""
58+
return getattr(client.service, '%s%s' % (prefix, name,))
59+
60+
@classmethod
61+
def _prepare_result(cls, result, returns):
62+
""" unwrap suds object as tuple and return a generator.
63+
"""
64+
unwrapped = result['return']
65+
if isinstance(unwrapped, str):
66+
return
67+
unwrapped = unwrapped[0]
68+
for obj in unwrapped:
69+
yield tuple([getattr(obj, r) for r in returns])
70+
71+
def _initalize(self, args, kwargs):
72+
""" a part of init method. If some search criteria was found it
73+
will automatically load this object.
74+
"""
75+
if not args and not kwargs:
76+
self._create_empty()
77+
return
78+
self._load(args, kwargs)
79+
80+
def _load(self, args, kwargs):
81+
""" call the callmanager and load the required object.
82+
"""
83+
first_lower = lambda s: s[:1].lower() + s[1:] if s else ''
84+
method = self._axl_method(PF_GET, self.__name__, self.__client__)
85+
result = method(*args, **kwargs)
86+
result = getattr(getattr(result, 'return'), first_lower(self.__name__))
87+
self._loadattr(result)
88+
self.__attached__ = True
89+
90+
def _convert_ecaped(self, kw):
91+
""" convert tags like "cls" to "class". This is normally
92+
done by suds, but by creating or update an object
93+
the attribute are not converted. This function will fix it.
94+
"""
95+
for key, value in kw.items():
96+
if key in ESCAPES:
97+
del kw[key]
98+
kw[ESCAPES[key]] = value
99+
return kw
100+
101+
def _skip_empty_tags(self, obj):
102+
""" callmanager can't handle attributes that are empty.
103+
This will recursive create a copy of object and remove
104+
all empty tags.
105+
"""
106+
copyobj = copy(obj)
107+
keylist = list()
108+
for key in obj.__keylist__:
109+
value = getattr(obj, key)
110+
if isinstance(value, list):
111+
copyobj[key] = [i if isinstance(i, Text) else self._skip_empty_tags(i) for i in value]
112+
keylist.append(key)
113+
elif isinstance(value, Object):
114+
copyobj[key] = self._skip_empty_tags(value)
115+
keylist.append(key)
116+
else:
117+
if isinstance(value, Text) and value != '' and value is not None:
118+
keylist.append(key)
119+
else:
120+
del copyobj.__dict__[key]
121+
copyobj.__keylist__ = keylist
122+
return copyobj
123+
124+
def _configure(self, configname):
125+
""" a part of init method. If no name is given it will
126+
take automatically the name of the class.
127+
"""
128+
self.__client__ = AXLClient.get_client(configname)
129+
self.__config__ = pyaxl.configuration.registry.get(configname)
130+
self.__configname__ = configname
131+
if self.__name__ is '':
132+
self.__name__ = self.__class__.__name__
133+
134+
def _create_empty(self):
135+
""" create an empty object. All attributes are set
136+
from a xsd type.
137+
"""
138+
obj = self.__client__.factory.create('%s:X%s' % (XSD_NS, self.__name__,))
139+
self._loadattr(obj)
140+
141+
def _loadattr(self, sudsinst):
142+
""" merge a suds object in this object... yes, python
143+
is so powerful :-O
144+
145+
first: update object attributes with suds attributes.
146+
second: copy all attributes of class instance to object.
147+
148+
The result will be a object that has all attributes as theses in XDS.
149+
"""
150+
151+
self.__dict__.update(sudsinst.__dict__)
152+
for k in sudsinst.__dict__.keys():
153+
if hasattr(self.__class__, k):
154+
self.__dict__[k] = self.__class__.__dict__[k]
155+
156+
157+
class AbstractCCMModel(BaseCCModel):
158+
""" Base class for all CiscoCallmanager objects.
159+
This will make the bridge between SUDS and CCM
160+
objects. In addition all standard method are implement here.
161+
"""
162+
163+
def create(self):
164+
""" add this object to callmanager.
165+
"""
166+
if self.__attached__:
167+
raise exceptions.CreationException('this object are already attached')
168+
method = self._axl_method(PF_ADD, self.__name__, self.__client__)
169+
xtype = self.__client__.factory.create('%s:%s' % (XSD_NS, method.method.name))
170+
xtype = xtype[1] # take attributes from wrapper
171+
tags = xtype.__keylist__ + list(ESCAPES.keys())
172+
unwrapped = dict()
173+
for key in self.__keylist__:
174+
value = getattr(self, key)
175+
if key in tags and value != '' and value is not None:
176+
if isinstance(value, Object):
177+
unwrapped[key] = self._skip_empty_tags(value)
178+
else:
179+
unwrapped[key] = value
180+
unwrapped = self._convert_ecaped(unwrapped)
181+
result = method(unwrapped)
182+
uuid = result['return']
183+
self.__attached__ = True
184+
self._uuid = uuid
185+
self.__updateable__ = list()
186+
log.info('new %s was created, uuid=%s' % (self.__name__, uuid,))
187+
return uuid
188+
189+
def update(self):
190+
""" all attributes that was changed will be committed to the callmanager.
191+
"""
192+
if not self.__attached__:
193+
raise exceptions.UpdateException('you must create a object with "create" before update')
194+
method = self._axl_method(PF_UPDATE, self.__name__, self.__client__)
195+
xtype = self.__client__.factory.create('%s:%s' % (XSD_NS, method.method.name))
196+
tags = xtype.__keylist__ + list(ESCAPES.keys())
197+
unwrapped = dict([(i, getattr(self, i),) for i in self.__updateable__ if i in tags])
198+
unwrapped.update(dict(uuid=self._uuid))
199+
unwrapped = self._convert_ecaped(unwrapped)
200+
method(**unwrapped)
201+
self.__updateable__ = list()
202+
log.info('%s was updated, uuid=%s' % (self.__name__, self._uuid,))
203+
204+
def remove(self):
205+
""" delete this object.
206+
"""
207+
if not self.__attached__:
208+
msg = 'This object is not attached and can not removed from callmanager'
209+
raise exceptions.RemoveException(msg)
210+
method = self._axl_method(PF_REMOVE, self.__name__, self.__client__)
211+
method(uuid=self._uuid)
212+
self._uuid = None
213+
self.__attached__ = False
214+
log.info('%s was removed, uuid=%s' % (self.__name__, self._uuid,))
215+
216+
def reload(self, force=False):
217+
""" Reload an object.
218+
"""
219+
if not self.__attached__:
220+
msg = 'This object is not attached and can not reloaded from callmanager'
221+
raise exceptions.ReloadException(msg)
222+
if not force and len(self.__updateable__):
223+
msg = 'Error because some field are already changed by the client. Use force or update it first.'
224+
raise exceptions.ReloadException(msg)
225+
self._load(list(), dict(uuid=self._uuid))
226+
227+
def clone(self):
228+
""" Clone a existing object. After cloning the new object will
229+
be detached. This means it can directly added to the callmanager
230+
with the create method.
231+
"""
232+
obj = self.__class__()
233+
#obj.__dict__.update(self.__dict__)
234+
for i in ['__updateable__', '__keylist__', ] + self.__keylist__:
235+
obj.__dict__[i] = copy(getattr(self, i))
236+
obj._uuid = None
237+
obj.__attached__ = False
238+
log.debug('%s was cloned' % self.__name__)
239+
return obj
240+
241+
@classmethod
242+
def list(cls, criteria, returns, skip=None, first=None, configname='default'):
243+
""" find all object with the given search criteria. It also
244+
required a list with return values. The return value is a
245+
generator and the next call will return a tuple with the returnsValues.
246+
"""
247+
client = AXLClient.get_client(configname)
248+
method = cls._axl_method(PF_LIST, cls.__name__, client)
249+
tags = dict([(i, True) for i in returns])
250+
log.debug('fetch list of %ss, search criteria=%s' % (cls.__name__, str(criteria)))
251+
args = criteria, tags
252+
if skip is not None or first is not None:
253+
if skip is None:
254+
skip = 0
255+
if first is None:
256+
args = criteria, tags, skip
257+
else:
258+
args = criteria, tags, skip, first
259+
return cls._prepare_result(method(*args), returns)
260+
261+
@classmethod
262+
def list_obj(cls, criteria, skip=None, first=None, configname='default'):
263+
""" find all object with the given search criteria.
264+
The return value is generator. Each next call will
265+
fetch a new instance and return it as object.
266+
"""
267+
for uuid, in cls.list(criteria, ('_uuid',), skip, first, configname):
268+
yield cls(uuid=uuid)
269+
270+
271+
class AbstractXType(BaseCCModel):
272+
273+
def _initalize(self, args, kwargs):
274+
""" Xtype is part of soap structure. XType will never be load directly so it's
275+
need to be created as empty object.
276+
"""
277+
self._create_empty()
278+
279+
def _create_empty(self):
280+
""" create an empty object. All attributes are set
281+
from a xsd type.
282+
"""
283+
obj = self.__client__.factory.create('%s:%s' % (XSD_NS, self.__name__,))
284+
self._loadattr(obj)
285+
286+
287+
class AbstractXTypeListItem(dict):
288+
""" A special XType that can be used to fill into a list.
289+
"""
290+
def __init__(self, *args, **kwargs):
291+
name = self.__class__.__name__
292+
xtype = type(name, (AbstractXType, self.__class__), dict())(*args, **kwargs)
293+
self[name[1:]] = xtype

‎src/pyaxl/ccm/common.py

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
from copy import copy
2+
from pyaxl import exceptions
3+
from pyaxl.axlsql import AXLSQLUtils
4+
from pyaxl.ccm.abstracts import AbstractCCMModel
5+
from pyaxl.ccm.mixings import MixingAbstractLines
6+
from pyaxl.ccm.mixings import MixingAbstractTemplate
7+
8+
9+
class DeviceProfile(AbstractCCMModel,
10+
MixingAbstractTemplate,
11+
MixingAbstractLines):
12+
pass
13+
14+
15+
class User(AbstractCCMModel, MixingAbstractTemplate):
16+
17+
def set_associated_devices(self, phones):
18+
if not isinstance(phones, list):
19+
phones = [phones]
20+
self.associatedDevices = [dict(device=i.name) for i in phones]
21+
22+
def set_cti_controlled_device_profiles(self, deviceprofiles):
23+
if not isinstance(deviceprofiles, list):
24+
deviceprofiles = [deviceprofiles]
25+
self.ctiControlledDeviceProfiles = [dict(profileName=dict(_uuid=i._uuid)) for i in deviceprofiles]
26+
27+
def set_phone_profiles(self, deviceprofiles):
28+
if not isinstance(deviceprofiles, list):
29+
deviceprofiles = [deviceprofiles]
30+
self.phoneProfiles = [dict(profileName=dict(_uuid=i._uuid)) for i in deviceprofiles]
31+
32+
def get_mobility_association(self):
33+
""" return phones that are associated with this user.
34+
"""
35+
sqlutils = AXLSQLUtils(self.__configname__)
36+
if not self.__attached__:
37+
raise exceptions.NotAttachedException('User is not attached')
38+
for i in sqlutils.user_phone_association(self._uuid):
39+
yield Phone(uuid=i['fkdevice'])
40+
41+
def get_cups_cupc(self):
42+
cups, cupc, pkid = self._get_cups_cupc()
43+
return cups, cupc
44+
45+
def set_cups_cupc(self, cups, cupc):
46+
sqlutils = AXLSQLUtils(self.__configname__)
47+
if cupc and not cups:
48+
raise exceptions.PyAXLException('If cupc is true, cups must also be true')
49+
rcups, rcupc, pkid = self._get_cups_cupc()
50+
if rcups is None and cups:
51+
sqlutils.insert_cups(self._uuid, cupc)
52+
elif rcups and not cups:
53+
sqlutils.remove_cups(self._uuid)
54+
elif rcups and cups:
55+
sqlutils.update_cups(self._uuid, cupc)
56+
57+
def _get_cups_cupc(self):
58+
sqlutils = AXLSQLUtils(self.__configname__)
59+
if not self.__attached__:
60+
raise exceptions.NotAttachedException('User is not attached')
61+
re = sqlutils.has_cups_cupc(self._uuid)
62+
if re is None:
63+
return None, None, None
64+
return re['enablecups'] == 't', re['enablecupc'] == 't', re['pkid']
65+
66+
67+
class UserGroup(AbstractCCMModel):
68+
pass
69+
70+
71+
class Line(AbstractCCMModel):
72+
pass
73+
74+
75+
class TransPattern(AbstractCCMModel):
76+
pass
77+
78+
79+
class Phone(AbstractCCMModel,
80+
MixingAbstractTemplate,
81+
MixingAbstractLines):
82+
83+
def logout(self):
84+
if not self.__attached__:
85+
raise exceptions.LogoutException('Phone is not attached')
86+
self.__client__.service.doDeviceLogout(dict(_uuid=self._uuid))
87+
88+
def login(self, user, deviceProfile, duration=1):
89+
if not self.__attached__:
90+
raise exceptions.LogoutException('Phone is not attached')
91+
self.__client__.service.doDeviceLogin(dict(_uuid=self._uuid),
92+
duration,
93+
dict(_uuid=deviceProfile._uuid),
94+
user.userid)
95+
96+
def update_bfcp(self, value):
97+
if not self.__attached__:
98+
raise exceptions.LogoutException('Phone is not attached')
99+
if not self.protocol == 'SIP':
100+
raise exceptions.PyAXLException('To change BFCP the phone must support SIP protocol')
101+
102+
# only available for newer version, is this flag is not present we need to do it with sql
103+
if hasattr(self, 'AllowPresentationSharingUsingBfcp'):
104+
clone = copy(self)
105+
clone.AllowPresentationSharingUsingBfcp = value
106+
clone.__updateable__ = ['AllowPresentationSharingUsingBfcp']
107+
clone.update()
108+
self.AllowPresentationSharingUsingBfcp = value
109+
else:
110+
sqlutils = AXLSQLUtils(self.__configname__)
111+
sqlutils.update_bfcp(self._uuid, value)
112+
113+
114+
class AppUser(AbstractCCMModel):
115+
pass
116+
117+
118+
class CallPickupGroup(AbstractCCMModel):
119+
pass
120+
121+
122+
class Css(AbstractCCMModel):
123+
pass
124+
125+
126+
class CtiRoutingPoint(AbstractCCMModel):
127+
pass
128+
129+
130+
class DevicePool(AbstractCCMModel):
131+
pass
132+
133+
134+
class HuntList(AbstractCCMModel):
135+
pass
136+
137+
138+
class HuntPilot(AbstractCCMModel):
139+
pass
140+
141+
142+
class LineGroup(AbstractCCMModel):
143+
pass
144+
145+
146+
class PhoneButtonTemplate(AbstractCCMModel):
147+
pass
148+
149+
150+
class RoutePartition(AbstractCCMModel):
151+
pass
152+
153+
154+
class VoiceMailPilot(AbstractCCMModel):
155+
pass
156+
157+
158+
class VoiceMailProfile(AbstractCCMModel):
159+
pass
160+
161+
162+
class RemoteDestination(AbstractCCMModel):
163+
164+
def set_single_number_reach(self, value):
165+
""" Set single number reach flag is not possible in version 10.5
166+
see ticket: https://supportforums.cisco.com/discussion/12438721/single-number-reach-axl
167+
The workaround is again to set some value with SQL! Yupiiiii!
168+
"""
169+
if not self.__attached__:
170+
raise exceptions.NotAttachedException('User is not attached')
171+
sqlutils = AXLSQLUtils(self.__configname__)
172+
sqlutils.set_single_number_reach(self._uuid, value)
173+
174+
def get_single_number_reach(self):
175+
if not self.__attached__:
176+
raise exceptions.NotAttachedException('User is not attached')
177+
sqlutils = AXLSQLUtils(self.__configname__)
178+
value = sqlutils.get_single_number_reach(self._uuid)
179+
return value['enablesinglenumberreach'] == 't'
180+
181+
182+
class RemoteDestinationProfile(AbstractCCMModel,
183+
MixingAbstractLines,
184+
MixingAbstractTemplate):
185+
@classmethod
186+
def template(cls, *args, **kwargs):
187+
return super(RemoteDestinationProfile, cls).template(*args, typeclass='Remote Destination Profile', **kwargs)
188+
189+
190+
class TodAccess(AbstractCCMModel):
191+
pass
192+
193+
194+
class TimeSchedule(AbstractCCMModel):
195+
196+
def removeMembers(self, members):
197+
if not isinstance(members, list):
198+
members = [members]
199+
removeMembers = [dict(member=dict(timePeriodName=dict(_uuid=uuid))) for uuid in members]
200+
self.__client__.service.updateTimeSchedule(removeMembers=removeMembers, uuid=self._uuid)
201+
202+
def addMembers(self, members):
203+
if not isinstance(members, list):
204+
members = [members]
205+
addMembers = [dict(member=dict(timePeriodName=dict(_uuid=uuid))) for uuid in members]
206+
self.__client__.service.updateTimeSchedule(addMembers=addMembers, uuid=self._uuid)
207+
208+
209+
class TimePeriod(AbstractCCMModel):
210+
pass

‎src/pyaxl/ccm/mixings.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import logging
2+
import types
3+
4+
5+
log = logging.getLogger('pyaxl')
6+
7+
8+
class MixingAbstractTemplate(object):
9+
""" Mixing class for all Types with template support.
10+
"""
11+
12+
@classmethod
13+
def template(cls, *args, typeclass=None, **kwargs):
14+
""" return with the given search criteria an complete object.
15+
On this object all attributes are presetted with the
16+
values from template.
17+
"""
18+
template = cls(*args, **kwargs)
19+
log.debug('%s created from template, criteria: %s, %s' % (cls.__name__, str(args), str(kwargs),))
20+
obj = template.clone()
21+
if typeclass is not None:
22+
obj.cls = typeclass
23+
return obj
24+
25+
26+
class MixingAbstractLines(object):
27+
28+
def set_lines(self, lines):
29+
""" associate a list or a single ccm.Line object
30+
to deviceprofile.
31+
"""
32+
if not isinstance(lines, (list, types.GeneratorType)):
33+
lines = [lines]
34+
self.lines = list()
35+
for index, line in enumerate(lines):
36+
self.lines.append(dict(line=dict(index=index + 1, dirn=dict(_uuid=line._uuid))))
37+
38+
def set_phonelines(self, xphonelines):
39+
""" associate a list or a single ccm.XPhoneLine object
40+
to deviceprofile.
41+
"""
42+
if not isinstance(xphonelines, list):
43+
xphonelines = [xphonelines]
44+
self.lines = list()
45+
for index, xpl in enumerate(xphonelines):
46+
xpl.index = index + 1
47+
self.lines.append(dict(line=xpl))

‎src/pyaxl/ccm/xtypes.py

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from pyaxl.ccm.abstracts import AbstractXType
2+
from pyaxl.ccm.abstracts import AbstractXTypeListItem
3+
4+
5+
class XPhoneLine(AbstractXType):
6+
pass
7+
8+
9+
class XCallForwardAll(AbstractXType):
10+
pass
11+
12+
13+
class XCallForwardAlternateParty(AbstractXType):
14+
pass
15+
16+
17+
class XCallForwardBusy(AbstractXType):
18+
pass
19+
20+
21+
class XCallForwardBusyInt(AbstractXType):
22+
pass
23+
24+
25+
class XCallForwardNoAnswer(AbstractXType):
26+
pass
27+
28+
29+
class XCallForwardNoAnswerInt(AbstractXType):
30+
pass
31+
32+
33+
class XCallForwardNoCoverage(AbstractXType):
34+
pass
35+
36+
37+
class XCallForwardNoCoverageInt(AbstractXType):
38+
pass
39+
40+
41+
class XCallForwardNotRegistered(AbstractXType):
42+
pass
43+
44+
45+
class XCallForwardNotRegisteredInt(AbstractXType):
46+
pass
47+
48+
49+
class XCallForwardOnFailure(AbstractXType):
50+
pass
51+
52+
53+
class XLineAppearanceAssociationForPresence(AbstractXType):
54+
pass
55+
56+
57+
class XLineAssociation(AbstractXType):
58+
pass

‎src/pyaxl/configuration.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
class AXLClientSettings(object):
2+
3+
def __init__(self, host, user, passwd, path, version,
4+
schema_path=None, suds_config=None, proxy=dict(),
5+
transport_debugger=False):
6+
7+
self.host = host
8+
self.user = user
9+
self.passwd = passwd
10+
self.path = path
11+
self.schema_path = schema_path
12+
self.suds_config = dict()
13+
self.proxy = proxy
14+
self.transport_debugger = transport_debugger
15+
if suds_config is not None:
16+
self.suds_config = suds_config
17+
self.version = '.'.join((str(version).split('.') + ['0'])[:2])
18+
19+
20+
class ConfigurationRegistry(object):
21+
22+
configurations = dict()
23+
24+
def register(self, configuration, name='default'):
25+
self.configurations[name] = configuration
26+
27+
def get(self, name='default'):
28+
return self.configurations[name]
29+
30+
registry = ConfigurationRegistry()

‎src/pyaxl/exceptions.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
class PyAXLException(Exception):
2+
pass
3+
4+
5+
class UpdateException(PyAXLException):
6+
pass
7+
8+
9+
class CreationException(PyAXLException):
10+
pass
11+
12+
13+
class RemoveException(PyAXLException):
14+
pass
15+
16+
17+
class ReloadException(PyAXLException):
18+
pass
19+
20+
21+
class LogoutException(PyAXLException):
22+
pass
23+
24+
25+
class NotAttachedException(PyAXLException):
26+
pass

‎src/pyaxl/testing/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from pyaxl.testing.testing import test_suite
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
2+
<soapenv:Body>
3+
<ns:listUserResponse xmlns:ns="http://www.cisco.com/AXL/API/10.5">
4+
<return>
5+
<user uuid="{5B5C014F-63A8-412F-B793-782BDA987371}">
6+
<firstName>Lance</firstName>
7+
<lastName>Armstrong</lastName>
8+
</user>
9+
<user uuid="{5B5C014F-63A8-412F-B793-782BDA987372}">
10+
<firstName>Neil</firstName>
11+
<lastName>Armstrong</lastName>
12+
</user>
13+
</return>
14+
</ns:listUserResponse>
15+
</soapenv:Body>
16+
</soapenv:Envelope>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
2+
<soapenv:Body>
3+
<ns:addUserResponse xmlns:ns="http://www.cisco.com/AXL/API/10.5">
4+
<return>{12345678-1234-1234-1234-123123456789}</return>
5+
</ns:addUserResponse>
6+
</soapenv:Body>
7+
</soapenv:Envelope>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
2+
<soapenv:Body>
3+
<ns:getUserResponse xmlns:ns="http://www.cisco.com/AXL/API/10.5">
4+
<return>
5+
<user uuid="{5B5C014F-63A8-412F-B793-782BDA987371}">
6+
<firstName>Samuel</firstName>
7+
<middleName/>
8+
<lastName>Riolo</lastName>
9+
<userid>RiolS</userid>
10+
<password/>
11+
<pin/>
12+
<mailid>Samuel.Riolo@biel-bienne.ch</mailid>
13+
<department>IUL IT (FID)</department>
14+
<manager/>
15+
<userLocale>German Germany</userLocale>
16+
<associatedDevices>
17+
<device>CSFRiolS</device>
18+
</associatedDevices>
19+
<primaryExtension>
20+
<pattern>\+41123456789</pattern>
21+
<routePartitionName>internal</routePartitionName>
22+
</primaryExtension>
23+
<associatedPc/>
24+
<associatedGroups>
25+
<userGroup>
26+
<name>Standard CTI Enabled</name>
27+
<userRoles>
28+
<userRole>Standard CTI Enabled</userRole>
29+
</userRoles>
30+
</userGroup>
31+
<userGroup>
32+
<name>Standard CTI Allow Control of Phones supporting Connected Xfer and conf</name>
33+
<userRoles>
34+
<userRole>Standard CTI Allow Control of Phones supporting Connected Xfer and conf</userRole>
35+
</userRoles>
36+
</userGroup>
37+
<userGroup>
38+
<name>Standard CCM Super Users</name>
39+
<userRoles>
40+
<userRole>Standard CCMADMIN Administration</userRole>
41+
<userRole>Standard SERVICEABILITY Administration</userRole>
42+
<userRole>Standard CCM Admin Users</userRole>
43+
<userRole>Standard Admin Rep Tool Admin</userRole>
44+
<userRole>Standard AXL API Access</userRole>
45+
<userRole>Standard SSO Config Admin</userRole>
46+
<userRole>Standard EM Authentication Proxy Rights</userRole>
47+
<userRole>Standard CUReporting</userRole>
48+
</userRoles>
49+
</userGroup>
50+
<userGroup>
51+
<name>Standard CTI Allow Control of Phones supporting Rollover Mode</name>
52+
<userRoles>
53+
<userRole>Standard CTI Allow Control of Phones supporting Rollover Mode</userRole>
54+
</userRoles>
55+
</userGroup>
56+
<userGroup>
57+
<name>Standard CCM End Users</name>
58+
<userRoles>
59+
<userRole>Standard CCM End Users</userRole>
60+
<userRole>Standard CCMUSER Administration</userRole>
61+
</userRoles>
62+
</userGroup>
63+
<userGroup>
64+
<name>Standard RealtimeAndTraceCollection</name>
65+
<userRoles>
66+
<userRole>Standard RealtimeAndTraceCollection</userRole>
67+
</userRoles>
68+
</userGroup>
69+
</associatedGroups>
70+
<enableCti>true</enableCti>
71+
<digestCredentials/>
72+
<phoneProfiles>
73+
<profileName uuid="a44f421a-9749-1942-6d5a-43d23d5fe186">RiolS-dp</profileName>
74+
</phoneProfiles>
75+
<defaultProfile/>
76+
<presenceGroupName uuid="{FDD1D95B-F10E-BF5C-4093-33A3D374E145}">SC Presence Group</presenceGroupName>
77+
<subscribeCallingSearchSpaceName/>
78+
<enableMobility>true</enableMobility>
79+
<enableMobileVoiceAccess>false</enableMobileVoiceAccess>
80+
<maxDeskPickupWaitTime>10000</maxDeskPickupWaitTime>
81+
<remoteDestinationLimit>4</remoteDestinationLimit>
82+
<associatedRemoteDestinationProfiles>
83+
<remoteDestinationProfile>RiolS-rdp</remoteDestinationProfile>
84+
</associatedRemoteDestinationProfiles>
85+
<passwordCredentials>
86+
<pwdCredPolicyName>Default Credential Policy</pwdCredPolicyName>
87+
<pwdCredUserCantChange>false</pwdCredUserCantChange>
88+
<pwdCredUserMustChange>true</pwdCredUserMustChange>
89+
<pwdCredDoesNotExpire>false</pwdCredDoesNotExpire>
90+
<pwdCredTimeChanged>November 15, 2013 00:03:31 CET</pwdCredTimeChanged>
91+
<pwdCredTimeAdminLockout/>
92+
<pwdCredLockedByAdministrator>false</pwdCredLockedByAdministrator>
93+
</passwordCredentials>
94+
<pinCredentials>
95+
<pinCredPolicyName>Default Credential Policy</pinCredPolicyName>
96+
<pinCredUserCantChange>true</pinCredUserCantChange>
97+
<pinCredUserMustChange>false</pinCredUserMustChange>
98+
<pinCredDoesNotExpire>false</pinCredDoesNotExpire>
99+
<pinCredTimeChanged>May 26, 2015 16:56:56 CEST</pinCredTimeChanged>
100+
<pinCredTimeAdminLockout/>
101+
<pinCredLockedByAdministrator>false</pinCredLockedByAdministrator>
102+
</pinCredentials>
103+
<associatedTodAccess>
104+
<todAccess>TOD-RD-e9f8cfa3-5958-ea98-e7b0-70a488710934</todAccess>
105+
</associatedTodAccess>
106+
<status>1</status>
107+
<enableEmcc>false</enableEmcc>
108+
<associatedCapfProfiles/>
109+
<ctiControlledDeviceProfiles>
110+
<profileName uuid="a44f421a-9749-1942-6d5a-43d23d5fe186">RiolS-dp</profileName>
111+
</ctiControlledDeviceProfiles>
112+
<patternPrecedence/>
113+
<numericUserId/>
114+
<mlppPassword/>
115+
<customUserFields/>
116+
<homeCluster>true</homeCluster>
117+
<imAndPresenceEnable>true</imAndPresenceEnable>
118+
<serviceProfile uuid="{C298DC01-040C-4C25-A389-C4C73A9DD93B}">UCServiceProfile_Default</serviceProfile>
119+
<lineAppearanceAssociationForPresences>
120+
<lineAppearanceAssociationForPresence uuid="{9810E19B-58E1-3ABB-B477-CB1F7654C568}">
121+
<laapAssociate>t</laapAssociate>
122+
<laapProductType>Cisco Unified Client Services Framework</laapProductType>
123+
<laapDeviceName>CSFRiolS</laapDeviceName>
124+
<laapDirectory>\+41123456789</laapDirectory>
125+
<laapPartition>internal</laapPartition>
126+
<laapDescription>+41123456789/Riolo Samuel/biel001</laapDescription>
127+
</lineAppearanceAssociationForPresence>
128+
<lineAppearanceAssociationForPresence uuid="{CEBC8BA3-A334-82FA-AFD7-5F86D4967AEA}">
129+
<laapAssociate>t</laapAssociate>
130+
<laapProductType>Cisco 7940</laapProductType>
131+
<laapDeviceName>RiolS-dp</laapDeviceName>
132+
<laapDirectory>\+41123456789</laapDirectory>
133+
<laapPartition>internal</laapPartition>
134+
<laapDescription>+41123456789/Riolo Samuel/biel001</laapDescription>
135+
</lineAppearanceAssociationForPresence>
136+
<lineAppearanceAssociationForPresence uuid="{CC356CDA-5E51-6E5E-BB46-E446ECE98D44}">
137+
<laapAssociate>t</laapAssociate>
138+
<laapProductType>Cisco 7940</laapProductType>
139+
<laapDeviceName>RiolS-dp</laapDeviceName>
140+
<laapDirectory>\+41123456789</laapDirectory>
141+
<laapPartition>internal</laapPartition>
142+
<laapDescription>+41123456789/Riolo Samuel/biel001</laapDescription>
143+
</lineAppearanceAssociationForPresence>
144+
<lineAppearanceAssociationForPresence uuid="{9810E19B-58E1-3ABB-B477-CB1F7654C568}">
145+
<laapAssociate>t</laapAssociate>
146+
<laapProductType>Cisco Unified Client Services Framework</laapProductType>
147+
<laapDeviceName>CSFRiolS</laapDeviceName>
148+
<laapDirectory>\+41123456789</laapDirectory>
149+
<laapPartition>internal</laapPartition>
150+
<laapDescription>+41123456789/Riolo Samuel/biel001</laapDescription>
151+
</lineAppearanceAssociationForPresence>
152+
<lineAppearanceAssociationForPresence uuid="{CEBC8BA3-A334-82FA-AFD7-5F86D4967AEA}">
153+
<laapAssociate>t</laapAssociate>
154+
<laapProductType>Cisco 7940</laapProductType>
155+
<laapDeviceName>RiolS-dp</laapDeviceName>
156+
<laapDirectory>\+41123456789</laapDirectory>
157+
<laapPartition>internal</laapPartition>
158+
<laapDescription>+41123456789/Riolo Samuel/biel001</laapDescription>
159+
</lineAppearanceAssociationForPresence>
160+
<lineAppearanceAssociationForPresence uuid="{CC356CDA-5E51-6E5E-BB46-E446ECE98D44}">
161+
<laapAssociate>t</laapAssociate>
162+
<laapProductType>Cisco 7940</laapProductType>
163+
<laapDeviceName>RiolS-dp</laapDeviceName>
164+
<laapDirectory>\+41123456789</laapDirectory>
165+
<laapPartition>internal</laapPartition>
166+
<laapDescription>+41123456789/Riolo Samuel/biel001</laapDescription>
167+
</lineAppearanceAssociationForPresence>
168+
</lineAppearanceAssociationForPresences>
169+
<directoryUri/>
170+
<telephoneNumber>+41123456789</telephoneNumber>
171+
<title/>
172+
<mobileNumber/>
173+
<homeNumber/>
174+
<pagerNumber/>
175+
<extensionsInfo>
176+
<extension uuid="{5906FE9F-1E70-4956-B4E1-A40299F2F7D4}">
177+
<sortOrder/>
178+
<pattern uuid="{3A72C5BC-F9F5-607D-C560-58CBCF550890}">\+41123456789</pattern>
179+
<routePartition>internal</routePartition>
180+
</extension>
181+
</extensionsInfo>
182+
<selfService>41123456789</selfService>
183+
<userProfile/>
184+
<calendarPresence>false</calendarPresence>
185+
<ldapDirectoryName uuid="{88F8740B-5998-99CB-673D-CB6D82056993}">LDAP_Biel</ldapDirectoryName>
186+
<userIdentity>RiolS@bielbienne.local</userIdentity>
187+
<nameDialing/>
188+
<ipccExtension uuid="{3A72C5BC-F9F5-607D-C560-58CBCF550890}">\+41123456789</ipccExtension>
189+
<convertUserAccount uuid="{88F8740B-5998-99CB-673D-CB6D82056993}">LDAP_Biel</convertUserAccount>
190+
</user>
191+
</return>
192+
</ns:getUserResponse>
193+
</soapenv:Body>
194+
</soapenv:Envelope>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
2+
<soapenv:Body>
3+
<ns:listUserResponse xmlns:ns="http://www.cisco.com/AXL/API/10.5">
4+
<return>
5+
<user uuid="{5B5C014F-63A8-412F-B793-782BDA987371}">
6+
<firstName>Samuel</firstName>
7+
<lastName>Riolo</lastName>
8+
</user>
9+
</return>
10+
</ns:listUserResponse>
11+
</soapenv:Body>
12+
</soapenv:Envelope>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
2+
<soapenv:Body>
3+
<ns:removeUserResponse xmlns:ns="http://www.cisco.com/AXL/API/10.5">
4+
<return>{5B5C014F-63A8-412F-B793-782BDA987371}</return>
5+
</ns:removeUserResponse>
6+
</soapenv:Body>
7+
</soapenv:Envelope>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
2+
<soapenv:Body>
3+
<ns:updateUserResponse xmlns:ns="http://www.cisco.com/AXL/API/10.5">
4+
<return>{5B5C014F-63A8-412F-B793-782BDA987371}</return>
5+
</ns:updateUserResponse>
6+
</soapenv:Body>
7+
</soapenv:Envelope>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
2+
<soapenv:Body>
3+
<ns:getUserResponse xmlns:ns="http://www.cisco.com/AXL/API/8.0">
4+
<return>
5+
<user uuid="{5B5C014F-63A8-412F-B793-782BDA987371}">
6+
<firstName>Samuel</firstName>
7+
<middleName/>
8+
<lastName>Riolo</lastName>
9+
<userid>RiolS</userid>
10+
<password/>
11+
<pin/>
12+
<mailid>Samuel.Riolo@biel-bienne.ch</mailid>
13+
<department>IUL IT (FID)</department>
14+
<manager/>
15+
<userLocale>German Germany</userLocale>
16+
<associatedDevices>
17+
<device>CSFRiolS</device>
18+
</associatedDevices>
19+
<primaryExtension>
20+
<pattern>\+41123456789</pattern>
21+
<routePartitionName>internal</routePartitionName>
22+
</primaryExtension>
23+
<associatedPc/>
24+
<associatedGroups>
25+
<userGroup>
26+
<name>Standard CTI Enabled</name>
27+
<userRoles>
28+
<userRole>Standard CTI Enabled</userRole>
29+
</userRoles>
30+
</userGroup>
31+
<userGroup>
32+
<name>Standard CTI Allow Control of Phones supporting Connected Xfer and conf</name>
33+
<userRoles>
34+
<userRole>Standard CTI Allow Control of Phones supporting Connected Xfer and conf</userRole>
35+
</userRoles>
36+
</userGroup>
37+
<userGroup>
38+
<name>Standard CCM Super Users</name>
39+
<userRoles>
40+
<userRole>Standard CCMADMIN Administration</userRole>
41+
<userRole>Standard SERVICEABILITY Administration</userRole>
42+
<userRole>Standard CCM Admin Users</userRole>
43+
<userRole>Standard Admin Rep Tool Admin</userRole>
44+
<userRole>Standard AXL API Access</userRole>
45+
<userRole>Standard SSO Config Admin</userRole>
46+
<userRole>Standard EM Authentication Proxy Rights</userRole>
47+
<userRole>Standard CUReporting</userRole>
48+
</userRoles>
49+
</userGroup>
50+
<userGroup>
51+
<name>Standard CTI Allow Control of Phones supporting Rollover Mode</name>
52+
<userRoles>
53+
<userRole>Standard CTI Allow Control of Phones supporting Rollover Mode</userRole>
54+
</userRoles>
55+
</userGroup>
56+
<userGroup>
57+
<name>Standard CCM End Users</name>
58+
<userRoles>
59+
<userRole>Standard CCM End Users</userRole>
60+
<userRole>Standard CCMUSER Administration</userRole>
61+
</userRoles>
62+
</userGroup>
63+
<userGroup>
64+
<name>Standard RealtimeAndTraceCollection</name>
65+
<userRoles>
66+
<userRole>Standard RealtimeAndTraceCollection</userRole>
67+
</userRoles>
68+
</userGroup>
69+
</associatedGroups>
70+
<enableCti>true</enableCti>
71+
<digestCredentials/>
72+
<phoneProfiles>
73+
<profileName uuid="a44f421a-9749-1942-6d5a-43d23d5fe186">RiolS-dp</profileName>
74+
</phoneProfiles>
75+
<defaultProfile/>
76+
<presenceGroupName uuid="{FDD1D95B-F10E-BF5C-4093-33A3D374E145}">SC Presence Group</presenceGroupName>
77+
<subscribeCallingSearchSpaceName/>
78+
<enableMobility>true</enableMobility>
79+
<enableMobileVoiceAccess>false</enableMobileVoiceAccess>
80+
<maxDeskPickupWaitTime>10000</maxDeskPickupWaitTime>
81+
<remoteDestinationLimit>4</remoteDestinationLimit>
82+
<associatedRemoteDestinationProfiles>
83+
<remoteDestinationProfile>RiolS-rdp</remoteDestinationProfile>
84+
</associatedRemoteDestinationProfiles>
85+
<passwordCredentials>
86+
<pwdCredPolicyName>Default Credential Policy</pwdCredPolicyName>
87+
<pwdCredUserCantChange>false</pwdCredUserCantChange>
88+
<pwdCredUserMustChange>true</pwdCredUserMustChange>
89+
<pwdCredDoesNotExpire>false</pwdCredDoesNotExpire>
90+
<pwdCredTimeChanged>November 15, 2013 00:03:31 CET</pwdCredTimeChanged>
91+
<pwdCredTimeAdminLockout/>
92+
<pwdCredLockedByAdministrator>false</pwdCredLockedByAdministrator>
93+
</passwordCredentials>
94+
<pinCredentials>
95+
<pinCredPolicyName>Default Credential Policy</pinCredPolicyName>
96+
<pinCredUserCantChange>true</pinCredUserCantChange>
97+
<pinCredUserMustChange>false</pinCredUserMustChange>
98+
<pinCredDoesNotExpire>false</pinCredDoesNotExpire>
99+
<pinCredTimeChanged>May 26, 2015 16:56:56 CEST</pinCredTimeChanged>
100+
<pinCredTimeAdminLockout/>
101+
<pinCredLockedByAdministrator>false</pinCredLockedByAdministrator>
102+
</pinCredentials>
103+
<primaryDevice uuid="{86B1FE06-DD3B-15C1-9D18-DCCD0C47EF7A}">CSFRiolS</primaryDevice>
104+
<associatedTodAccess>
105+
<todAccess>TOD-RD-e9f8cfa3-5958-ea98-e7b0-70a488710934</todAccess>
106+
</associatedTodAccess>
107+
<status>1</status>
108+
<enableEmcc>false</enableEmcc>
109+
<associatedCapfProfiles/>
110+
<ctiControlledDeviceProfiles>
111+
<profileName uuid="a44f421a-9749-1942-6d5a-43d23d5fe186">RiolS-dp</profileName>
112+
</ctiControlledDeviceProfiles>
113+
<telephoneNumber>+41123456789</telephoneNumber>
114+
</user>
115+
</return>
116+
</ns:getUserResponse>
117+
</soapenv:Body>
118+
</soapenv:Envelope>

‎src/pyaxl/testing/testing.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import doctest
2+
import unittest
3+
4+
5+
optionflags = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS + doctest.IGNORE_EXCEPTION_DETAIL
6+
7+
8+
def test_suite():
9+
suite = unittest.TestSuite()
10+
suite.addTests([
11+
doctest.DocFileSuite('../../README.rst',
12+
package='pyaxl',
13+
)
14+
])
15+
return suite

‎src/pyaxl/testing/transport.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import os
2+
from suds.transport import Reply
3+
from suds.transport import Transport
4+
from xml.dom.minidom import parseString
5+
6+
7+
class TestingTransport(Transport):
8+
9+
_output_file = None
10+
_lastrequest = None
11+
12+
def define(self, xmlfile):
13+
""" Define a xml file to use for the reply
14+
as return value from the send method.
15+
Format: <xmlfile>_<method>.xml
16+
"""
17+
self._output_file = xmlfile
18+
19+
def lastrequest(self):
20+
""" Returns the last used request which was been sent
21+
to the callmanager, so that we can validate it
22+
"""
23+
return self._lastrequest
24+
25+
def send(self, request):
26+
""" Returns a fake Reply builded with data from a xml file.
27+
The xml file must be defined with the method "define"
28+
before the soap request is send.
29+
"""
30+
dom = parseString(request.message)
31+
body = dom.documentElement.getElementsByTagNameNS('*', 'Body')[0]
32+
method = body.firstChild.localName
33+
34+
if method.startswith('get'):
35+
filename = '%s_get.xml' % self._output_file
36+
elif method.startswith('list'):
37+
filename = '%s_list.xml' % self._output_file
38+
elif method.startswith('update'):
39+
filename = '%s_update.xml' % self._output_file
40+
elif method.startswith('remove'):
41+
filename = '%s_remove.xml' % self._output_file
42+
elif method.startswith('add'):
43+
filename = '%s_add.xml' % self._output_file
44+
else:
45+
filename = '%s.xml' % self._output_file
46+
with open(os.path.join(os.path.dirname(__file__), 'soap', filename), 'rb') as f:
47+
message = f.read()
48+
self._lastrequest = request
49+
return Reply(200, request.headers, message)

‎src/pyaxl/testing/validate.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from xml.dom import minidom
2+
3+
4+
def validateSOAPRequest(request, method, tags):
5+
""" request: the request used by suds
6+
method: soap method name, e.g. "getUser"
7+
tags: a dict with tag name and value, e.g. dict(userid='riols')
8+
"""
9+
dom = minidom.parseString(request.message)
10+
assert dom.documentElement.tagName == 'SOAP-ENV:Envelope'
11+
dom.documentElement.childNodes[0].localName
12+
header = dom.documentElement.getElementsByTagNameNS('*', 'Header')
13+
body = dom.documentElement.getElementsByTagNameNS('*', 'Body')
14+
assert len(header) == 1
15+
assert len(body) == 1
16+
body, header = body.pop(), header.pop()
17+
assert body.firstChild.localName == method
18+
for tag, value in tags.items():
19+
nodes = body.getElementsByTagNameNS('*', tag)
20+
assert len(nodes) == 1
21+
assert nodes[0].firstChild.nodeValue == value
22+
23+
24+
def _printSOAPRequest(node, level):
25+
output = list()
26+
for subnode in node.childNodes:
27+
spaces = ' ' * 4 * level
28+
if isinstance(subnode.firstChild, minidom.Text):
29+
output.append('%s%s=%s' % (spaces, subnode.localName, subnode.firstChild.nodeValue))
30+
else:
31+
output.append('%s%s:' % (spaces, subnode.localName))
32+
output.append(_printSOAPRequest(subnode, level + 1))
33+
return '\n'.join(output)
34+
35+
36+
def printSOAPRequest(request):
37+
dom = minidom.parseString(request.message)
38+
assert dom.documentElement.tagName == 'SOAP-ENV:Envelope'
39+
dom.documentElement.childNodes[0].localName
40+
header = dom.documentElement.getElementsByTagNameNS('*', 'Header')
41+
body = dom.documentElement.getElementsByTagNameNS('*', 'Body')
42+
assert len(header) == 1
43+
assert len(body) == 1
44+
body, header = body.pop(), header.pop()
45+
nodemethod = body.firstChild
46+
47+
print('%s:\n%s' % (nodemethod.localName, _printSOAPRequest(nodemethod, 1)))

‎src/pyaxl/utils.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import re
2+
3+
4+
REGEX_UUID = re.compile(r'^\{?([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})\}?$')
5+
6+
7+
def uuid(value):
8+
""" parse uuid and return a blank uuid without "{}".
9+
if the uuid is wrong it will raise an exception.
10+
"""
11+
re = REGEX_UUID.match(value.lower())
12+
if re is None:
13+
raise ValueError('uuid is wrong')
14+
returnvalue, = re.groups()
15+
return returnvalue
16+
17+
18+
def axlbool(value):
19+
""" convert suds.sax.text.Text to python bool
20+
"""
21+
if value is None:
22+
return None
23+
if not value:
24+
return False
25+
if value.lower() == 'true':
26+
return True
27+
return False

‎tox.ini

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[tox]
2+
envlist =
3+
py34
4+
5+
6+
[testenv]
7+
commands =
8+
{envpython} setup.py install
9+
pyaxl_import_wsdl -p -c default {posargs}/10.5/AXLAPI.wsdl
10+
pyaxl_import_wsdl -p -c testing {posargs}/8.0/AXLAPI.wsdl
11+
{envpython} setup.py test
12+
deps =

0 commit comments

Comments
 (0)
Please sign in to comment.