opensipscli 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ """
21
+ Default configuration for OpenSIPS CLI
22
+ """
23
+
24
+ import os
25
+ import time
26
+
27
+ DEFAULT_SECTION = 'default'
28
+ DEFAULT_NAME = 'opensips-cli'
29
+ try:
30
+ home_dir = os.environ["HOME"]
31
+ except:
32
+ # default home dir to root
33
+ home_dir = "/"
34
+
35
+ """
36
+ Default history file is in ~/.opensips-cli.history
37
+ """
38
+ HISTORY_FILE = os.path.join(home_dir, ".{}.history".format(DEFAULT_NAME))
39
+
40
+ """
41
+ Try configuration files in this order:
42
+ * ~/.opensips-cli.cfg
43
+ * /etc/opensips-cli.cfg
44
+ * /etc/opensips/opensips-cli.cfg
45
+ """
46
+ CFG_PATHS = [
47
+ os.path.join(home_dir, ".{}.cfg".format(DEFAULT_NAME)),
48
+ "/etc/{}.cfg".format(DEFAULT_NAME),
49
+ "/etc/opensips/{}.cfg".format(DEFAULT_NAME),
50
+ ]
51
+
52
+ DEFAULT_VALUES = {
53
+ # CLI settings
54
+ "prompt_name": "opensips-cli",
55
+ "prompt_intro": "Welcome to OpenSIPS Command Line Interface!",
56
+ "prompt_emptyline_repeat_cmd": "False",
57
+ "history_file": HISTORY_FILE,
58
+ "history_file_size": "1000",
59
+ "output_type": "pretty-print",
60
+ "log_level": "INFO",
61
+
62
+ # communication information
63
+ "communication_type": "fifo",
64
+ "fifo_reply_dir": "/tmp",
65
+ "fifo_file": "/var/run/opensips/opensips_fifo",
66
+ "fifo_file_fallback": "/tmp/opensips_fifo",
67
+ "url": "http://127.0.0.1:8888/mi",
68
+ "datagram_ip": "127.0.0.1",
69
+ "datagram_port": "8080",
70
+
71
+ # database module
72
+ "database_url": "mysql://opensips:opensipsrw@localhost",
73
+ "database_name": "opensips",
74
+ "database_schema_path": "/usr/share/opensips",
75
+
76
+ # user module
77
+ "plain_text_passwords": "False",
78
+
79
+ # diagnose module
80
+ "diagnose_listen_ip": "127.0.0.1",
81
+ "diagnose_listen_port": "8899",
82
+
83
+ # trace module
84
+ "trace_listen_ip": "127.0.0.1",
85
+ "trace_listen_port": "0",
86
+
87
+ # trap module
88
+ "trap_file": '/tmp/gdb_opensips_{}'.format(time.strftime('%Y%m%d_%H%M%S'))
89
+ }
90
+
91
+ # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ from opensipscli.libs import sqlalchemy_utils
@@ -0,0 +1,244 @@
1
+ ## Copyright (c) 2012, Konsta Vesterinen
2
+ ##
3
+ ## All rights reserved.
4
+ ##
5
+ ## Redistribution and use in source and binary forms, with or without
6
+ ## modification, are permitted provided that the following conditions are met:
7
+ ##
8
+ ## * Redistributions of source code must retain the above copyright notice, this
9
+ ## list of conditions and the following disclaimer.
10
+ ##
11
+ ## * Redistributions in binary form must reproduce the above copyright notice,
12
+ ## this list of conditions and the following disclaimer in the documentation
13
+ ## and/or other materials provided with the distribution.
14
+ ##
15
+ ## * The names of the contributors may not be used to endorse or promote products
16
+ ## derived from this software without specific prior written permission.
17
+ ##
18
+ ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ## ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ ## WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ ## DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT,
22
+ ## INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23
+ ## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25
+ ## LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26
+ ## OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
27
+ ## ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+ ##
29
+ ## Copied from https://github.com/kvesteri/sqlalchemy-utils/blob/2e8ee0093f4a33a5c7479bc9aaf16d7863a74a16/sqlalchemy_utils/functions/database.py
30
+ ## Please check LICENSE
31
+
32
+ from copy import copy
33
+
34
+ import os
35
+ import sqlalchemy as sa
36
+ from sqlalchemy.engine.url import make_url
37
+ from sqlalchemy.exc import OperationalError, ProgrammingError
38
+ from sqlalchemy.engine.interfaces import Dialect
39
+ from sqlalchemy.orm.session import object_session
40
+ from sqlalchemy.orm.exc import UnmappedInstanceError
41
+
42
+ def database_exists(url):
43
+ """Check if a database exists.
44
+ :param url: A SQLAlchemy engine URL.
45
+ Performs backend-specific testing to quickly determine if a database
46
+ exists on the server. ::
47
+ database_exists('postgresql://postgres@localhost/name') #=> False
48
+ create_database('postgresql://postgres@localhost/name')
49
+ database_exists('postgresql://postgres@localhost/name') #=> True
50
+ Supports checking against a constructed URL as well. ::
51
+ engine = create_engine('postgresql://postgres@localhost/name')
52
+ database_exists(engine.url) #=> False
53
+ create_database(engine.url)
54
+ database_exists(engine.url) #=> True
55
+ """
56
+
57
+ def get_scalar_result(engine, sql):
58
+ result_proxy = engine.execute(sql)
59
+ result = result_proxy.scalar()
60
+ result_proxy.close()
61
+ engine.dispose()
62
+ return result
63
+
64
+ def sqlite_file_exists(database):
65
+ if not os.path.isfile(database) or os.path.getsize(database) < 100:
66
+ return False
67
+
68
+ with open(database, 'rb') as f:
69
+ header = f.read(100)
70
+
71
+ return header[:16] == b'SQLite format 3\x00'
72
+
73
+ url = copy(make_url(url))
74
+ if hasattr(url, "_replace"):
75
+ database = url.database
76
+ url = url._replace(database=None)
77
+ else:
78
+ database, url.database = url.database, None
79
+
80
+ engine = sa.create_engine(url)
81
+
82
+ if engine.dialect.name == 'postgresql':
83
+ text = "SELECT 1 FROM pg_database WHERE datname='%s'" % database
84
+ return bool(get_scalar_result(engine, text))
85
+
86
+ elif engine.dialect.name == 'mysql':
87
+ text = ("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA "
88
+ "WHERE SCHEMA_NAME = '%s'" % database)
89
+ return bool(get_scalar_result(engine, text))
90
+
91
+ elif engine.dialect.name == 'sqlite':
92
+ if database:
93
+ return database == ':memory:' or sqlite_file_exists(database)
94
+ else:
95
+ # The default SQLAlchemy database is in memory,
96
+ # and :memory is not required, thus we should support that use-case
97
+ return True
98
+
99
+ else:
100
+ engine.dispose()
101
+ engine = None
102
+ text = 'SELECT 1'
103
+ try:
104
+ if hasattr(url, "_replace"):
105
+ url = url._replace(database=database)
106
+ else:
107
+ url.database = database
108
+
109
+ engine = sa.create_engine(url)
110
+ result = engine.execute(text)
111
+ result.close()
112
+ return True
113
+
114
+ except (ProgrammingError, OperationalError):
115
+ return False
116
+ finally:
117
+ if engine is not None:
118
+ engine.dispose()
119
+
120
+ def get_bind(obj):
121
+ """
122
+ Return the bind for given SQLAlchemy Engine / Connection / declarative
123
+ model object.
124
+ :param obj: SQLAlchemy Engine / Connection / declarative model object
125
+ ::
126
+ from sqlalchemy_utils import get_bind
127
+ get_bind(session) # Connection object
128
+ get_bind(user)
129
+ """
130
+ if hasattr(obj, 'bind'):
131
+ conn = obj.bind
132
+ else:
133
+ try:
134
+ conn = object_session(obj).bind
135
+ except UnmappedInstanceError:
136
+ conn = obj
137
+
138
+ if not hasattr(conn, 'execute'):
139
+ raise TypeError(
140
+ 'This method accepts only Session, Engine, Connection and '
141
+ 'declarative model objects.'
142
+ )
143
+ return conn
144
+
145
+ def quote(mixed, ident):
146
+ """
147
+ Conditionally quote an identifier.
148
+ ::
149
+ from sqlalchemy_utils import quote
150
+ engine = create_engine('sqlite:///:memory:')
151
+ quote(engine, 'order')
152
+ # '"order"'
153
+ quote(engine, 'some_other_identifier')
154
+ # 'some_other_identifier'
155
+ :param mixed: SQLAlchemy Session / Connection / Engine / Dialect object.
156
+ :param ident: identifier to conditionally quote
157
+ """
158
+ if isinstance(mixed, Dialect):
159
+ dialect = mixed
160
+ else:
161
+ dialect = get_bind(mixed).dialect
162
+ return dialect.preparer(dialect).quote(ident)
163
+
164
+ def drop_database(url):
165
+ """Issue the appropriate DROP DATABASE statement.
166
+ :param url: A SQLAlchemy engine URL.
167
+ Works similar to the :ref:`create_database` method in that both url text
168
+ and a constructed url are accepted. ::
169
+ drop_database('postgresql://postgres@localhost/name')
170
+ drop_database(engine.url)
171
+ """
172
+
173
+ url = copy(make_url(url))
174
+
175
+ database = url.database
176
+
177
+ if url.drivername.startswith('postgres'):
178
+ if hasattr(url, "set"):
179
+ url = url.set(database='postgres')
180
+ else:
181
+ url.database = 'postgres'
182
+
183
+ elif url.drivername.startswith('mssql'):
184
+ if hasattr(url, "set"):
185
+ url = url.set(database='master')
186
+ else:
187
+ url.database = 'master'
188
+
189
+ elif not url.drivername.startswith('sqlite'):
190
+ if hasattr(url, "_replace"):
191
+ url = url._replace(database=None)
192
+ else:
193
+ url.database = None
194
+
195
+ if url.drivername == 'mssql+pyodbc':
196
+ engine = sa.create_engine(url, connect_args={'autocommit': True})
197
+ elif url.drivername == 'postgresql+pg8000':
198
+ engine = sa.create_engine(url, isolation_level='AUTOCOMMIT')
199
+ else:
200
+ engine = sa.create_engine(url)
201
+ conn_resource = None
202
+
203
+ if engine.dialect.name == 'sqlite' and database != ':memory:':
204
+ if database:
205
+ os.remove(database)
206
+
207
+ elif (
208
+ engine.dialect.name == 'postgresql' and
209
+ engine.driver in {'psycopg2', 'psycopg2cffi'}
210
+ ):
211
+ if engine.driver == 'psycopg2':
212
+ from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
213
+ connection = engine.connect()
214
+ connection.connection.set_isolation_level(
215
+ ISOLATION_LEVEL_AUTOCOMMIT
216
+ )
217
+ else:
218
+ connection = engine.connect()
219
+ connection.connection.set_session(autocommit=True)
220
+
221
+ # Disconnect all users from the database we are dropping.
222
+ version = connection.dialect.server_version_info
223
+ pid_column = (
224
+ 'pid' if (version >= (9, 2)) else 'procpid'
225
+ )
226
+ text = '''
227
+ SELECT pg_terminate_backend(pg_stat_activity.%(pid_column)s)
228
+ FROM pg_stat_activity
229
+ WHERE pg_stat_activity.datname = '%(database)s'
230
+ AND %(pid_column)s <> pg_backend_pid();
231
+ ''' % {'pid_column': pid_column, 'database': database}
232
+ connection.execute(text)
233
+
234
+ # Drop the database.
235
+ text = 'DROP DATABASE {0}'.format(quote(connection, database))
236
+ connection.execute(text)
237
+ conn_resource = connection
238
+ else:
239
+ text = 'DROP DATABASE {0}'.format(quote(engine, database))
240
+ conn_resource = engine.execute(text)
241
+
242
+ if conn_resource is not None:
243
+ conn_resource.close()
244
+ engine.dispose()
opensipscli/logger.py ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ """
21
+ logger.py - implements coloured logging for the opensips-cli project
22
+ """
23
+
24
+ import logging
25
+
26
+ #These are the sequences need to get colored ouput
27
+ RESET_SEQ = "\033[0m"
28
+ COLOR_SEQ = "\033[1;%dm"
29
+ BOLD_SEQ = "\033[1m"
30
+
31
+ def formatter_message(message, use_color = True):
32
+ if use_color:
33
+ message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ)
34
+ else:
35
+ message = message.replace("$RESET", "").replace("$BOLD", "")
36
+ return message
37
+
38
+ # Custom logger class with multiple destinations
39
+ class ColoredLogger(logging.Logger):
40
+
41
+ BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
42
+
43
+ FORMAT = "$BOLD%(levelname)s$RESET: %(message)s"
44
+ COLOR_FORMAT = formatter_message(FORMAT, True)
45
+
46
+ def __init__(self, name):
47
+ logging.Logger.__init__(self, name)
48
+
49
+ color_formatter = ColoredFormatter(self.COLOR_FORMAT)
50
+
51
+ console = logging.StreamHandler()
52
+ console.setFormatter(color_formatter)
53
+
54
+ self.addHandler(console)
55
+ return
56
+
57
+ def color(self, color, message):
58
+ return COLOR_SEQ % (30 + color) + message + RESET_SEQ
59
+
60
+ class ColoredFormatter(logging.Formatter):
61
+
62
+ LEVELS_COLORS = {
63
+ 'WARNING': ColoredLogger.YELLOW,
64
+ 'INFO': ColoredLogger.MAGENTA,
65
+ 'DEBUG': ColoredLogger.BLUE,
66
+ 'CRITICAL': ColoredLogger.YELLOW,
67
+ 'ERROR': ColoredLogger.RED
68
+ }
69
+
70
+ def __init__(self, msg, use_color = True):
71
+ logging.Formatter.__init__(self, msg)
72
+ self.use_color = use_color
73
+
74
+ def format(self, record):
75
+ levelname = record.levelname
76
+ if self.use_color and levelname in self.LEVELS_COLORS:
77
+ levelname_color = COLOR_SEQ % (30 + self.LEVELS_COLORS[levelname]) + levelname + RESET_SEQ
78
+ record.levelname = levelname_color
79
+ return logging.Formatter.format(self, record)
80
+
81
+
82
+ logging.setLoggerClass(ColoredLogger)
83
+ logger = logging.getLogger(__name__)
84
+
85
+ # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
opensipscli/main.py ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ import sys
21
+ import argparse
22
+ from opensipscli import cli, defaults, version
23
+
24
+ parser = argparse.ArgumentParser(description='OpenSIPS CLI interactive tool',
25
+ prog=sys.argv[0],
26
+ usage='%(prog)s [OPTIONS]',
27
+ epilog='\n')
28
+
29
+ # Argument used to print the current version
30
+ parser.add_argument('-v', '--version',
31
+ action='version',
32
+ default=None,
33
+ version='OpenSIPS CLI {}'.format(version.__version__))
34
+ # Argument used to enable debugging
35
+ parser.add_argument('-d', '--debug',
36
+ action='store_true',
37
+ default=False,
38
+ help='enable debugging')
39
+ # Argument used to specify a configuration file
40
+ parser.add_argument('-f', '--config',
41
+ metavar='[FILE]',
42
+ type=str,
43
+ default=None,
44
+ help='used to specify a configuration file')
45
+ # Argument used to switch to a different instance
46
+ parser.add_argument('-i', '--instance',
47
+ metavar='[INSTANCE]',
48
+ type=str,
49
+ action='store',
50
+ default=defaults.DEFAULT_SECTION,
51
+ help='choose an opensips instance')
52
+ # Argument used to overwrite certain values in the config
53
+ parser.add_argument('-o', '--option',
54
+ metavar='[KEY=VALUE]',
55
+ action='append',
56
+ type=str,
57
+ dest="extra_options",
58
+ default=None,
59
+ help='overwrite certain values in the config')
60
+ # Argument used to dump the configuration
61
+ parser.add_argument('-p', '--print',
62
+ action='store_true',
63
+ default=False,
64
+ help='dump the configuration')
65
+ # Argument used to run the command in non-interactive mode
66
+ parser.add_argument('-x', '--execute',
67
+ action='store_true',
68
+ default=False,
69
+ help='run the command in non-interactive mode')
70
+ # Argument used to specify the command to run
71
+ parser.add_argument('command',
72
+ nargs='*',
73
+ default=[],
74
+ help='the command to run')
75
+
76
+ def main():
77
+
78
+ # Parse all arguments
79
+ args = parser.parse_args()
80
+
81
+ # Open the CLI
82
+ shell = cli.OpenSIPSCLI(args)
83
+ sys.exit(shell.cmdloop())
84
+
85
+ if __name__ == '__main__':
86
+ main()
opensipscli/module.py ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ class Module:
21
+ """
22
+ An abstract class, that has to be implemented by every Module that should be handled
23
+ """
24
+
25
+ def __exclude__(self):
26
+ """
27
+ indicates whether the module should be excluded
28
+ """
29
+ return (False, None)
30
+
31
+ def __invoke__(self, cmd, params=None, modifiers=None):
32
+ """
33
+ used to invoke a command from the module (starting with prefix 'do_')
34
+ """
35
+ f = getattr(self, 'do_' + cmd)
36
+ return f(params, modifiers)
37
+
38
+ def __get_methods__(self):
39
+ """
40
+ returns all the available methods of the module
41
+ if the method returns None, the do_`module_name`
42
+ method is called for each command
43
+ """
44
+ return ([x[3:] for x in dir(self)
45
+ if x.startswith('do_') and callable(getattr(self, x))])
46
+
47
+ def __get_modifiers__(self):
48
+ """
49
+ returns all the available modifiers of a specific module
50
+ """
51
+ return None
52
+
53
+ def __complete__(self, command, text, line, begidx, endidx):
54
+ """
55
+ returns a list with all the auto-completion values
56
+ """
57
+ if not command:
58
+ modifiers = self.__get_modifiers__()
59
+ return modifiers if modifiers else ['']
60
+ try:
61
+ compfunc = getattr(self, 'complete_' + command)
62
+ l = compfunc(text, line, begidx, endidx)
63
+ if not l:
64
+ return ['']
65
+ except AttributeError:
66
+ return None
67
+ if len(l) == 1:
68
+ l[0] += " "
69
+ return l
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python
2
+ ##
3
+ ## This file is part of OpenSIPS CLI
4
+ ## (see https://github.com/OpenSIPS/opensips-cli).
5
+ ##
6
+ ## This program is free software: you can redistribute it and/or modify
7
+ ## it under the terms of the GNU General Public License as published by
8
+ ## the Free Software Foundation, either version 3 of the License, or
9
+ ## (at your option) any later version.
10
+ ##
11
+ ## This program is distributed in the hope that it will be useful,
12
+ ## but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ ## GNU General Public License for more details.
15
+ ##
16
+ ## You should have received a copy of the GNU General Public License
17
+ ## along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ ##
19
+
20
+ import pkgutil
21
+
22
+ __path__ = pkgutil.extend_path(__path__, __name__)
23
+ for importer, modname, ispkg in pkgutil.walk_packages(path=__path__, prefix=__name__+'.'):
24
+ __import__(modname)