summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAli Sabil <ali.sabil@gmail.com>2007-01-12 17:22:17 (GMT)
committerAli Sabil <ali.sabil@gmail.com>2007-01-12 17:22:17 (GMT)
commite9d01e423efe85e1b54d2532d69a9e13e4b6451e (patch)
tree476bef4793c33b847f830d83f2c814723590392f
parent530a83d1822ac3ad6fbf13750de88de1f860c38b (diff)
downloadpapyon-e9d01e423efe85e1b54d2532d69a9e13e4b6451e.tar.gz
papyon-e9d01e423efe85e1b54d2532d69a9e13e4b6451e.tar.xz
- Added documentation generation
- Started implementing transport.py
-rw-r--r--doc.py87
-rwxr-xr-xdoc/fix_encoding.sh9
-rw-r--r--pymsn/__init__.py2
-rw-r--r--pymsn/gnet/message/HTTP.py2
-rw-r--r--pymsn/gnet/protocol/HTTP.py4
-rw-r--r--pymsn/service/SOAPService.py35
-rw-r--r--pymsn/service/SingleSignOn.py3
-rw-r--r--pymsn/tests/test_https.py7
-rw-r--r--pymsn/tests/test_soap.py2
-rw-r--r--pymsn/transport.py212
-rw-r--r--setup.py57
11 files changed, 398 insertions, 22 deletions
diff --git a/doc.py b/doc.py
new file mode 100644
index 0000000..89c3c78
--- /dev/null
+++ b/doc.py
@@ -0,0 +1,87 @@
+
+
+"""this module was based on setup.py from pygame-ctype branch.
+author: Alex Holkner <aholkner@cs.rmit.edu.au>"""
+
+import os
+from os.path import join, abspath, dirname, splitext
+import sys
+import subprocess
+
+from distutils.cmd import Command
+
+
+# A "do-everything" command class for building any type of documentation.
+class BuildDocCommand(Command):
+ user_options = [('doc-dir=', None, 'directory to build documentation'),
+ ('epydoc=', None, 'epydoc executable')]
+
+ def initialize_options(self):
+ self.doc_dir = join(abspath(dirname(sys.argv[0])), 'doc')
+ self.epydoc = 'epydoc'
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ if 'pre' in self.doc:
+ subprocess.call(self.doc['pre'], shell=True)
+
+ prev_dir = os.getcwd()
+ if 'chdir' in self.doc:
+ dir = abspath(join(self.doc_dir, self.doc['chdir']))
+ try:
+ os.makedirs(dir)
+ except:
+ pass
+ os.chdir(dir)
+
+ if 'packages' in self.doc:
+ if 'url' in self.doc:
+ url = '--url=%s' % self.doc['url']
+ else:
+ url = ''
+ cmd = [self.epydoc,
+ '--parse-only',
+ '--no-private',
+ '--no-frames',
+ '--html',
+ url,
+ '-v']
+ cmd.append('--name="%s"' % self.doc['description'])
+ if 'output' in self.doc:
+ cmd.append('-o %s' % \
+ abspath(join(self.doc_dir, self.doc['output'])))
+ cmd.append(self.doc['packages'])
+ subprocess.call(' '.join(cmd), shell=True)
+
+ os.chdir(prev_dir)
+
+ if 'post' in self.doc:
+ subprocess.call(self.doc['post'], shell=True)
+
+# Fudge a command class given a dictionary description
+def make_doc_command(**kwargs):
+ class c(BuildDocCommand):
+ doc = dict(**kwargs)
+ description = 'build %s' % doc['description']
+ c.__name__ = 'build_doc_%s' % c.doc['name'].replace('-', '_')
+ return c
+
+# This command does nothing but run all the other doc commands.
+# (sub_commands are set later)
+class BuildAllDocCommand(Command):
+ description = 'build all documentation'
+ user_options = []
+ sub_commands = []
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ for cmd_name in self.get_sub_commands():
+ self.run_command(cmd_name)
+
diff --git a/doc/fix_encoding.sh b/doc/fix_encoding.sh
new file mode 100755
index 0000000..2284455
--- /dev/null
+++ b/doc/fix_encoding.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+sed -i \
+ 's/<?xml version="1.0" encoding="iso-8859-1"?>//' \
+ $@
+
+sed -i \
+ 's/<?xml version="1.0" encoding="ascii"?>//' \
+ $@
diff --git a/pymsn/__init__.py b/pymsn/__init__.py
index e6a7654..2f5035c 100644
--- a/pymsn/__init__.py
+++ b/pymsn/__init__.py
@@ -21,5 +21,3 @@
pymsn is a library, written in Python, for accessing the MSN
instant messaging service.
"""
-
-import gnet
diff --git a/pymsn/gnet/message/HTTP.py b/pymsn/gnet/message/HTTP.py
index 1287d3a..3bfcc95 100644
--- a/pymsn/gnet/message/HTTP.py
+++ b/pymsn/gnet/message/HTTP.py
@@ -95,7 +95,7 @@ class HTTPResponse(HTTPMessage):
return start_line + "\r\n" + message
class HTTPRequest(HTTPMessage):
- def __init__(self, headers=None, body="", method="GET", resource="/", version="1.0"):
+ def __init__(self, headers=None, body="", method="GET", resource="/", version="1.1"):
if headers is None:
headers = {}
HTTPMessage.__init__(self)
diff --git a/pymsn/gnet/protocol/HTTP.py b/pymsn/gnet/protocol/HTTP.py
index 145aff4..aa40ad3 100644
--- a/pymsn/gnet/protocol/HTTP.py
+++ b/pymsn/gnet/protocol/HTTP.py
@@ -113,12 +113,10 @@ class HTTP(gobject.GObject):
if headers is None:
headers = {}
headers['Host'] = self._host + ':' + str(self._port)
+ headers['Content-Length'] = str(len(data))
if 'User-Agent' not in headers:
headers['User-Agent'] = GNet.NAME + '/' + GNet.VERSION
- if len(data) > 0:
- headers['Content-Length'] = str(len(data))
-
if self.__proxy is not None:
url = 'http://%s:%d%s' % (self._host, self._port, resource)
if self.__proxy.user:
diff --git a/pymsn/service/SOAPService.py b/pymsn/service/SOAPService.py
index 22ce713..ad44c7b 100644
--- a/pymsn/service/SOAPService.py
+++ b/pymsn/service/SOAPService.py
@@ -20,9 +20,8 @@
import gnet.protocol
import gnet.message.SOAP as SOAP
-class SOAPService(object):
- """Base class for all Windows Live Services."""
-
+
+class BaseSOAPService(object):
DEFAULT_PROTOCOL = "http"
def __init__(self, url, proxy=None):
@@ -32,6 +31,7 @@ class SOAPService(object):
self.transport = gnet.protocol.ProtocolFactory(protocol, host, proxy=proxy)
self.transport.connect("response-received", self._response_handler)
self.transport.connect("request-sent", self._request_handler)
+ self.transport.connect("error", self._error_handler)
def _url_split(self, url):
from urlparse import urlsplit, urlunsplit
@@ -47,6 +47,26 @@ class SOAPService(object):
def _request_handler(self, transport, request):
print request
+
+ def _error_handler(self, transport, error):
+ print "Error", error
+
+ def _send_request(self):
+ """This method sends the SOAP request over the wire"""
+ self.transport.request(resource = self.resource,
+ headers = self.http_headers,
+ data = str(self.request),
+ method = 'POST')
+ self.http_headers = {}
+ self.soap_headers = None
+ self.request = None
+
+
+class SOAPService(BaseSOAPService):
+ """Base class for all Windows Live Services."""
+
+ def __init__(self, url, proxy=None):
+ BaseSOAPService.__init__(self, url, proxy)
def __getattr__(self, name):
def method(*params):
@@ -74,15 +94,6 @@ class SOAPService(object):
self._method(method_name, {}, *params)
self._send_request()
- def _send_request(self):
- """This method sends the SOAP request over the wire"""
- self.transport.request(resource = self.resource,
- headers = self.http_headers,
- data = str(self.request),
- method = 'POST')
- self.http_headers = {}
- self.soap_headers = None
- self.request = None
def _soap_action(self, method):
"""return the SOAPAction header value to be used
diff --git a/pymsn/service/SingleSignOn.py b/pymsn/service/SingleSignOn.py
index 6802235..351e3eb 100644
--- a/pymsn/service/SingleSignOn.py
+++ b/pymsn/service/SingleSignOn.py
@@ -33,7 +33,7 @@ NS_WS_ADDRESSING = "http://schemas.xmlsoap.org/ws/2004/03/addressing"
NS_WS_POLICY = "http://schemas.xmlsoap.org/ws/2002/12/policy"
NS_WS_ISSUE = "http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue"
-MSN_USER_AGENT = "MSN Explorer/9.0 (MSN 8.0; TmstmpExt)"
+MSN_USER_AGENT = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; IDCRL 4.100.313.1; IDCRL-cfg 4.0.5633.0; App MsnMsgr.Exe, 8.1.168.0, {7108E71A-9926-4FCB-BCC9-9A9D3F32E423})"
class LiveService(object):
CONTACTS = ("contacts.msn.com", "?fs=1&id=24000&kv=7&rn=93S9SWWw&tw=0&ver=2.1.6000.1")
@@ -85,6 +85,7 @@ class SingleSignOn(SOAPService):
def _http_headers(self, method):
SOAPService._http_headers(self, method)
self.http_headers['User-Agent'] = MSN_USER_AGENT
+ self.http_headers['Accept'] = "text/*"
def __serialize_request_params(self, params):
s = struct.pack("<L", len(params))
diff --git a/pymsn/tests/test_https.py b/pymsn/tests/test_https.py
index db58e47..9495033 100644
--- a/pymsn/tests/test_https.py
+++ b/pymsn/tests/test_https.py
@@ -19,9 +19,12 @@ def response(http, resp):
def request(http, req):
print req
-c = gnet.protocol.HTTPS("www.gmail.com")
+data='<?xml version="1.0" encoding="UTF-8"?><Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsse="http://schemas.xmlsoap.org/ws/2003/06/secext" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing" xmlns:wssc="http://schemas.xmlsoap.org/ws/2004/04/sc" xmlns:wst="http://schemas.xmlsoap.org/ws/2004/04/trust"><Header><ps:AuthInfo xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="PPAuthInfo"><ps:HostingApp>{7108E71A-9926-4FCB-BCC9-9A9D3F32E423}</ps:HostingApp><ps:BinaryVersion>4</ps:BinaryVersion><ps:UIVersion>1</ps:UIVersion><ps:Cookies></ps:Cookies><ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams></ps:AuthInfo><wsse:Security><wsse:UsernameToken Id="user"><wsse:Username>kimbix@hotmail.com</wsse:Username><wsse:Password>linox45</wsse:Password></wsse:UsernameToken></wsse:Security></Header><Body><ps:RequestMultipleSecurityTokens xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL" Id="RSTS"><wst:RequestSecurityToken Id="RST0"><wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference><wsa:Address>http://Passport.NET/tb</wsa:Address></wsa:EndpointReference></wsp:AppliesTo></wst:RequestSecurityToken><wst:RequestSecurityToken Id="RST2"><wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference><wsa:Address>messenger.msn.com</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><wsse:PolicyReference URI="?id=507"></wsse:PolicyReference></wst:RequestSecurityToken><wst:RequestSecurityToken Id="RST3"><wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference><wsa:Address>contacts.msn.com</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><wsse:PolicyReference URI="MBI"></wsse:PolicyReference></wst:RequestSecurityToken><wst:RequestSecurityToken Id="RST4"><wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference><wsa:Address>messengersecure.live.com</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><wsse:PolicyReference URI="MBI_SSL"></wsse:PolicyReference></wst:RequestSecurityToken><wst:RequestSecurityToken Id="RST5"><wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference><wsa:Address>spaces.live.com</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><wsse:PolicyReference URI="MBI"></wsse:PolicyReference></wst:RequestSecurityToken><wst:RequestSecurityToken Id="RST6"><wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType><wsp:AppliesTo><wsa:EndpointReference><wsa:Address>storage.msn.com</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><wsse:PolicyReference URI="MBI"></wsse:PolicyReference></wst:RequestSecurityToken></ps:RequestMultipleSecurityTokens></Body></Envelope>'
+
+uagent = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; IDCRL 4.100.313.1; IDCRL-cfg 4.0.5633.0; App MsnMsgr.Exe, 8.1.168.0, {7108E71A-9926-4FCB-BCC9-9A9D3F32E423})"
+c = gnet.protocol.HTTPS("login.live.com")
c.connect("response-received", response)
c.connect("request-sent", request)
-c.request("/")
+c.request("/RST.srf", headers={'User-Agent':uagent, 'Cookie':"MUID=FF4FF37AE48E491E96862397A5FD9AC4"}, data=data, method="POST")
mainloop.run()
diff --git a/pymsn/tests/test_soap.py b/pymsn/tests/test_soap.py
index 574aa29..d92564e 100644
--- a/pymsn/tests/test_soap.py
+++ b/pymsn/tests/test_soap.py
@@ -26,6 +26,6 @@ class TemperatureService(SOAPService.SOAPService):
#print '------------------------------------------------'
sso = SSO.SingleSignOn("kimbix@hotmail.com", "linox45")
-sso.RequestMultipleSecurityTokens(SSO.LiveService.TB, SSO.LiveService.MESSENGER_CLEAR)
+sso.RequestMultipleSecurityTokens(SSO.LiveService.TB, SSO.LiveService.CONTACTS)
gobject.MainLoop().run()
diff --git a/pymsn/transport.py b/pymsn/transport.py
new file mode 100644
index 0000000..4a1c551
--- /dev/null
+++ b/pymsn/transport.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+#
+# pymsn - a python client library for Msn
+#
+# Copyright (C) 2005-2006 Ali Sabil <ali.sabil@gmail.com>
+# Copyright (C) 2006 Johann Prieur <johann.prieur@gmail.com>
+# Copyright (C) 2006 Ole André Vadla Ravnås <oleavr@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""Network Transport Layer
+
+This module provides an abstraction of the transport to be used to communicate
+with the MSN servers, actually MSN servers can communicate either through direct
+connection using TCP/1863 or using TCP/80 by tunelling the protocol inside HTTP
+POST requests.
+
+The classes of this module are structured as follow:
+G{classtree BaseTransport}"""
+
+import gnet
+
+import logging
+import gobject
+
+__all__=['ServerType', 'BaseTransport']
+
+logger = logging.getLogger('Connection')
+
+class ServerType(object):
+ """"""
+ SWITCHBOARD = 'SB'
+ NOTIFICATION = 'NS'
+
+
+class BaseTransport(gobject.GObject):
+ """Abstract Base Class that modelize a connection to the MSN service, this
+ abstraction is used to build various transports that expose the same
+ interface, basically a transport is created using its constructor then it
+ simply emits signals when network events (or even abstracted network events)
+ occur, for example a Transport that successfully connected to the MSN
+ service will emit a connection-success signal, and when that transport
+ received a meaningful message it would emit a command-received signal.
+
+ @ivar server: the server being used to connect to
+ @type server: tuple(host, port)
+
+ @ivar server_type: the server that we are connecting to, either
+ Notification or switchboard.
+ @type server_type: L{ServerType}
+
+ @ivar proxies: proxies that we can use to connect
+ @type proxies: dict(type => L{gnet.proxy.ProxyInfos})
+
+ @ivar transaction_id: the current transaction ID
+ @type transaction_id: integer
+
+
+ @cvar connection-failure: signal emitted when the connection fails
+ @type connection-failure: ()
+
+ @cvar connection-success: signal emitted when the connection succeed
+ @type connection-success: ()
+
+ @cvar connection-reset: signal emitted when the connection is being
+ reset
+ @type connection-reset: ()
+
+ @cvar connection-lost: signal emitted when the connection was lost
+ @type connection-lost: ()
+
+ @cvar command-received: signal emitted when a command is received
+ @type command-received: FIXME-DOC
+
+ @cvar command-sent: signal emitted when a command was successfully
+ transmitted to the server
+ @type command-sent: FIXME-DOC
+
+ @undocumented: __gsignals__"""
+
+ __gsignals__ = {
+ "connection-failure" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+
+ "connection-success" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+
+ "connection-reset" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+
+ "connection-lost" : (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ ()),
+
+ "command-received": (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+
+ "command-sent": (gobject.SIGNAL_RUN_FIRST,
+ gobject.TYPE_NONE,
+ (object,)),
+ }
+
+ def __init__(self, server, server_type=ServerType.NOTIFICATION, proxies={}):
+ """Connection initialization
+
+ @param server: the server to connect to.
+ @type server: (host: string, port: integer)
+
+ @param server_type: the server that we are connecting to, either
+ Notification or switchboard.
+ @type server_type: L{ServerType}
+
+ @param proxies: proxies that we can use to connect
+ @type proxies: {type: string => L{gnet.network.ProxyInfos}}"""
+ gobject.GObject.__init__(self)
+ self.server = server
+ self.server_type = server_type
+ self.proxies = proxies
+ self._transaction_id = 0
+
+ def __get_transaction_id(self):
+ return self._transaction_id
+ transaction_id = property(__get_transaction_id)
+
+ # Connection
+ def establish_connection(self):
+ """Connect to the server server"""
+ raise NotImplementedError
+
+ def lose_connection(self):
+ """Disconnect from the server"""
+ raise NotImplementedError
+
+ def reset_connection(self, server=None):
+ """Reset the connection
+
+ @param server: when set, reset the connection and
+ connect to this new server
+ @type server: tuple(host, port)"""
+ raise NotImplementedError
+
+ # Command Sending
+ def send_command(self, command, increment=True, callback=None, cb_args=()):
+ """
+ Sends a L{structure.Command} to the server.
+
+ @param command: command to send
+ @type command: L{structure.Command}
+
+ @param increment: if False, the transaction ID is not incremented
+ @type increment: bool
+
+ @param callback: callback to be used when the command has been
+ transmitted
+ @type callback: callable
+
+ @param cb_args: callback arguments
+ @type cb_args: tuple
+ """
+ raise NotImplementedError
+
+ def send_command_ex(self, command, arguments=None, payload=None,
+ callback=None, cb_args=()):
+ """
+ Builds a command object then send it to the server.
+
+ @param command: the command name, must be a 3 letters
+ uppercase string.
+ @type command: string
+
+ @param arguments: command arguments
+ @type arguments: (string, ...)
+
+ @param payload: payload data
+ @type payload: string
+
+ @param callback: callback to be used when the command has been
+ transmitted
+ @type callback: callable
+
+ @param cb_args: callback arguments
+ @type cb_args: tuple
+ """
+ transaction_id = self._transaction_id
+ cmd = structure.Command()
+ cmd.build(command, transaction_id, arguments, payload)
+ self.send_command(cmd, increment, callback, cb_args)
+
+ def _increment_transaction_id(self):
+ """Increments the Transaction ID then return it.
+
+ @rtype: integer"""
+ self._transaction_id += 1
+ return self._transaction_id
+gobject.type_register(BaseTransport)
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..2982b25
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,57 @@
+from distutils.core import setup
+from doc import make_doc_command
+from sys import version
+
+# Metadata
+NAME="pymsn"
+VERSION="0.2.1"
+DESCRIPTION="Python msn client library"
+AUTHOR="Ali Sabil"
+AUTHOR_EMAIL="ali.sabil@gmail.com"
+URL="http://telepathy.freedesktop.org/wiki/Pymsn",
+LICENSE="GNU GPL",
+
+
+# compatibility with python < 2.2.3
+if version < '2.2.3':
+ from distutils.dist import DistributionMetadata
+ DistributionMetadata.classifiers = None
+ DistributionMetadata.download_url = None
+
+# Documentation
+doc_commands = {
+ 'build_doc': make_doc_command(
+ name='pymsn',
+ description='Python msn client library',
+ url=URL,
+ output='pymsn',
+ post='doc/fix_encoding.sh doc/pymsn/*.html',
+ packages='pymsn')
+}
+
+# Setup
+setup(name=NAME,
+ version=VERSION,
+ description=DESCRIPTION,
+ author=AUTHOR,
+ author_email=AUTHOR_EMAIL,
+ url=URL,
+ license=LICENSE,
+ platforms=["any"],
+ packages=["pymsn", "pymsn.gio"],
+ cmdclass=doc_commands,
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Environment :: Console',
+ 'Intended Audience :: Developers',
+ 'Intended Audience :: Telecommunications Industry',
+ 'License :: OSI Approved :: GNU General Public License (GPL)',
+ 'Operating System :: POSIX',
+ 'Operating System :: MacOS :: MacOS X',
+ 'Operating System :: Microsoft :: Windows',
+ 'Programming Language :: Python',
+ 'Topic :: Communications :: Chat',
+ 'Topic :: Communications :: Telephony',
+ 'Topic :: Internet',
+ 'Topic :: Software Development :: Libraries :: Python Modules'
+ ])