Adds onos_vpls to install in Dockerfile-local 61/8361/4
authorBenjamin Diaz <bdiaz@whitestack.com>
Mon, 9 Dec 2019 23:12:42 +0000 (20:12 -0300)
committerBenjamin Diaz <bdiaz@whitestack.com>
Thu, 12 Dec 2019 12:11:24 +0000 (09:11 -0300)
Change-Id: Iee9869ecd53cf3daef76cd3c693e92cb7a60c5d2
Signed-off-by: Benjamin Diaz <bdiaz@whitestack.com>
Dockerfile-local
RO-SDN-onos_vpls/Makefile
RO-SDN-onos_vpls/osm_rosdn_onos_vpls/sdn_assist_onos_vpls.py [new file with mode: 0644]
RO-SDN-onos_vpls/osm_rosdn_onosvpls/sdn_assist_onos_vpls.py [deleted file]
RO-SDN-onos_vpls/setup.py
RO-SDN-onos_vpls/tox.ini

index 5ad7b4b..3c9540e 100644 (file)
@@ -56,6 +56,7 @@ RUN /root/RO/RO/osm_ro/scripts/install-osm-im.sh --develop && \
     python3 -m pip install -e /root/RO/RO-VIM-fos && \
     python3 -m pip install -e /root/RO/RO-SDN-dynpac && \
     python3 -m pip install -e /root/RO/RO-SDN-tapi && \
     python3 -m pip install -e /root/RO/RO-VIM-fos && \
     python3 -m pip install -e /root/RO/RO-SDN-dynpac && \
     python3 -m pip install -e /root/RO/RO-SDN-tapi && \
+    python3 -m pip install -e /root/RO/RO-SDN-onos_vpls && \
     rm -rf /root/.cache && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
     rm -rf /root/.cache && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/*
index b2f4d85..952d6ef 100644 (file)
@@ -16,9 +16,9 @@
 all: clean package
 
 clean:
 all: clean package
 
 clean:
-       rm -rf dist deb_dist osm_rosdn_onosvpls-*.tar.gz osm_rosdn_tapi.egg-info .eggs
+       rm -rf dist deb_dist osm_rosdn_onos_vpls-*.tar.gz osm_rosdn_onos_vpls.egg-info .eggs
 
 package:
        python3 setup.py --command-packages=stdeb.command sdist_dsc
 
 package:
        python3 setup.py --command-packages=stdeb.command sdist_dsc
-       cd deb_dist/osm-rosdn-onosvpls*/ && dpkg-buildpackage -rfakeroot -uc -us
+       cd deb_dist/osm-rosdn-onos-vpls*/ && dpkg-buildpackage -rfakeroot -uc -us
 
 
diff --git a/RO-SDN-onos_vpls/osm_rosdn_onos_vpls/sdn_assist_onos_vpls.py b/RO-SDN-onos_vpls/osm_rosdn_onos_vpls/sdn_assist_onos_vpls.py
new file mode 100644 (file)
index 0000000..8531444
--- /dev/null
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2018 Whitestack, LLC
+# *************************************************************
+
+# This file is part of OSM RO module
+# All Rights Reserved to Whitestack, LLC
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+
+#         http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact: bdiaz@whitestack.com or glavado@whitestack.com
+##
+import logging
+import uuid
+
+import requests
+from requests.auth import HTTPBasicAuth
+
+from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError
+
+log = logging.getLogger(__name__)
+
+
+class OnosVpls(SdnConnectorBase):
+    """
+    https://wiki.onosproject.org/display/ONOS/VPLS+User+Guide
+    """
+
+    def __init__(self, wim, wim_account, config=None, logger=None):
+
+        super().__init__(wim, wim_account, config, log)
+        self.user = wim_account.get("user")
+        self.password = wim_account.get("password")
+        url = wim_account.get("wim_url")
+        if not url:
+            raise ValueError("'url' must be provided")
+        if not url.startswith("http"):
+            url = "http://" + url
+        if not url.endswith("/"):
+            url = url + "/"
+        self.url = url + "onos/v1/network/configuration"
+        log.info("ONOS VPLS Connector Initialized.")
+
+    def check_credentials(self):
+        status_code = 503
+        onos_config_req = None
+        try:
+            onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
+            onos_config_req.raise_for_status()
+        except Exception as e:
+            if onos_config_req:
+                status_code = onos_config_req.status_code
+            log.exception('Error checking credentials')
+            raise SdnConnectorError('Error checking credentials', http_code=status_code)
+
+    def get_connectivity_service_status(self, service_uuid, conn_info=None):
+        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
+        onos_config_req.raise_for_status()
+        onos_config = onos_config_req.json()
+        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
+            if vpls['name'] == service_uuid:
+                return vpls
+        raise SdnConnectorError('VPLS {} not found'.format(service_uuid), http_code=404)
+
+    def create_connectivity_service(self, service_type, connection_points):
+        if service_type.lower() != 'elan':
+            raise SdnConnectorError('Only ELAN network type is supported by ONOS VPLS.')
+        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
+        onos_config_req.raise_for_status()
+        onos_config = onos_config_req.json()
+        service_uuid = uuid.uuid4()
+
+        if 'org.onosproject.vpls' in onos_config['apps']:
+            if 'vpls' not in onos_config['apps']['org.onosproject.vpls']:
+                onos_config['apps']['org.onosproject.vpls']['vpls'] = {
+                    'vplsList': []
+                }
+            for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
+                if vpls['name'] == service_uuid:
+                    raise SdnConnectorError('Network {} already exists.'.format(service_uuid))
+            onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList'].append({
+                'name': service_uuid,
+                'interfaces': []
+            })
+            self._pop_last_update_time(onos_config)
+        else:
+            onos_config['apps'] = {
+                'org.onosproject.vpls': {
+                    'vpls': {
+                        "vplsList": [
+                            {
+                                'name': service_uuid,
+                                'interfaces': []
+                            }
+                        ]
+                    }
+                }
+            }
+        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
+        log.info(onos_config)
+        response.raise_for_status()
+        for connection_point in connection_points:
+            self._add_network_port(service_uuid, connection_point)
+        return service_uuid, onos_config
+
+    def edit_connectivity_service(self, service_uuid,
+                                  conn_info, connection_points,
+                                  **kwargs):
+        raise SdnConnectorError('Not supported', http_code=501)
+
+    def delete_connectivity_service(self, service_uuid, conn_info=None):
+        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
+        onos_config_req.raise_for_status()
+        onos_config = onos_config_req.json()
+        # Removes ports used by network from onos config
+        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
+            if vpls['name'] == service_uuid:
+                for interface in vpls['interfaces']:
+                    for port in onos_config['ports'].values():
+                        for port_interface in port['interfaces']:
+                            if port_interface['name'] == interface:
+                                port['interfaces'].remove(port_interface)
+                onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList'].remove(vpls)
+                break
+        self._pop_last_update_time(onos_config)
+        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
+        response.raise_for_status()
+
+    def _delete_network_port(self, net_id, port):
+        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
+        onos_config_req.raise_for_status()
+        onos_config = onos_config_req.json()
+        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
+            if vpls['name'] == net_id:
+                for interface in vpls['interfaces']:
+                    if interface == port['service_endpoint_id']:
+                        vpls['interfaces'].remove(interface)
+                        break
+        for onos_port in onos_config['ports'].values():
+            for port_interface in onos_port['interfaces']:
+                if port_interface['name'] == port['service_endpoint_id']:
+                    onos_port['interfaces'].remove(port_interface)
+                    break
+        self._pop_last_update_time(onos_config)
+        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
+        response.raise_for_status()
+
+    def _add_network_port(self, net_id, port):
+        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
+        onos_config_req.raise_for_status()
+        onos_config = onos_config_req.json()
+        self._append_port_to_onos_config(port, onos_config)
+        # Interfaces need to be registered before adding them to VPLS
+        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
+        response.raise_for_status()
+        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
+            if vpls['name'] == net_id:
+                vpls['interfaces'].append(port['service_endpoint_id'])
+                break
+        self._pop_last_update_time(onos_config)
+        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
+        response.raise_for_status()
+
+    def _pop_last_update_time(self, onos_config):
+        if 'lastUpdateTime' in onos_config['apps']['org.onosproject.vpls']['vpls']:
+            onos_config['apps']['org.onosproject.vpls']['vpls'].pop('lastUpdateTime')
+
+    def _append_port_to_onos_config(self, port, onos_config):
+        port_name = 'of:{}/{}'.format(port['service_endpoint_encapsulation_info']['switch_dpid'],
+                                  port['service_endpoint_encapsulation_info']['switch_port'])
+        interface_config = {'name': port['service_endpoint_id']}
+        if 'vlan' in port['service_endpoint_encapsulation_info'] and port['service_endpoint_encapsulation_info'][
+            'vlan']:
+            interface_config['vlan'] = port['service_endpoint_encapsulation_info']['vlan']
+        if port_name in onos_config['ports'] and 'interfaces' in onos_config['ports'][port_name]:
+            for interface in onos_config['ports'][port_name]['interfaces']:
+                if interface['name'] == port['service_endpoint_id']:
+                    onos_config['ports'][port_name]['interfaces'].remove(interface)
+            onos_config['ports'][port_name]['interfaces'].append(interface_config)
+        else:
+            onos_config['ports'][port_name] = {
+                'interfaces': [interface_config]
+            }
+
+
+if __name__ == '__main__':
+    pass
+    # host = '198.204.228.85'
+    # port = 8181
+    # onos_vpls = OnosVpls(host, port, 'onos', 'rocks')
+    # ports = [
+    #     {
+    #         'uuid': '0a43961d',
+    #         'switch_dpid': '0000000000000001',
+    #         'switch_port': '1',
+    #         'vlan': 100
+    #     },
+    #     {
+    #         'uuid': 'ade3eefc',
+    #         'switch_dpid': '0000000000000003',
+    #         'switch_port': '1',
+    #         'vlan': 100
+    #     }
+    # ]
+    # onos_vpls.create_network('94979b37-3875-4f77-b620-01ff78f9c4fa', 'data')
+    # onos_vpls.add_network_port('94979b37-3875-4f77-b620-01ff78f9c4fa', ports[0])
+    # onos_vpls.add_network_port('94979b37-3875-4f77-b620-01ff78f9c4fa', ports[1])
+    # onos_vpls.delete_network_port('94979b37-3875-4f77-b620-01ff78f9c4fa', ports[1])
+    # onos_vpls.delete_network('94979b37-3875-4f77-b620-01ff78f9c4fa')
diff --git a/RO-SDN-onos_vpls/osm_rosdn_onosvpls/sdn_assist_onos_vpls.py b/RO-SDN-onos_vpls/osm_rosdn_onosvpls/sdn_assist_onos_vpls.py
deleted file mode 100644 (file)
index 8531444..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2018 Whitestack, LLC
-# *************************************************************
-
-# This file is part of OSM RO module
-# All Rights Reserved to Whitestack, LLC
-
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-
-#         http://www.apache.org/licenses/LICENSE-2.0
-
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-# For those usages not covered by the Apache License, Version 2.0 please
-# contact: bdiaz@whitestack.com or glavado@whitestack.com
-##
-import logging
-import uuid
-
-import requests
-from requests.auth import HTTPBasicAuth
-
-from osm_ro.wim.sdnconn import SdnConnectorBase, SdnConnectorError
-
-log = logging.getLogger(__name__)
-
-
-class OnosVpls(SdnConnectorBase):
-    """
-    https://wiki.onosproject.org/display/ONOS/VPLS+User+Guide
-    """
-
-    def __init__(self, wim, wim_account, config=None, logger=None):
-
-        super().__init__(wim, wim_account, config, log)
-        self.user = wim_account.get("user")
-        self.password = wim_account.get("password")
-        url = wim_account.get("wim_url")
-        if not url:
-            raise ValueError("'url' must be provided")
-        if not url.startswith("http"):
-            url = "http://" + url
-        if not url.endswith("/"):
-            url = url + "/"
-        self.url = url + "onos/v1/network/configuration"
-        log.info("ONOS VPLS Connector Initialized.")
-
-    def check_credentials(self):
-        status_code = 503
-        onos_config_req = None
-        try:
-            onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
-            onos_config_req.raise_for_status()
-        except Exception as e:
-            if onos_config_req:
-                status_code = onos_config_req.status_code
-            log.exception('Error checking credentials')
-            raise SdnConnectorError('Error checking credentials', http_code=status_code)
-
-    def get_connectivity_service_status(self, service_uuid, conn_info=None):
-        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
-        onos_config_req.raise_for_status()
-        onos_config = onos_config_req.json()
-        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
-            if vpls['name'] == service_uuid:
-                return vpls
-        raise SdnConnectorError('VPLS {} not found'.format(service_uuid), http_code=404)
-
-    def create_connectivity_service(self, service_type, connection_points):
-        if service_type.lower() != 'elan':
-            raise SdnConnectorError('Only ELAN network type is supported by ONOS VPLS.')
-        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
-        onos_config_req.raise_for_status()
-        onos_config = onos_config_req.json()
-        service_uuid = uuid.uuid4()
-
-        if 'org.onosproject.vpls' in onos_config['apps']:
-            if 'vpls' not in onos_config['apps']['org.onosproject.vpls']:
-                onos_config['apps']['org.onosproject.vpls']['vpls'] = {
-                    'vplsList': []
-                }
-            for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
-                if vpls['name'] == service_uuid:
-                    raise SdnConnectorError('Network {} already exists.'.format(service_uuid))
-            onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList'].append({
-                'name': service_uuid,
-                'interfaces': []
-            })
-            self._pop_last_update_time(onos_config)
-        else:
-            onos_config['apps'] = {
-                'org.onosproject.vpls': {
-                    'vpls': {
-                        "vplsList": [
-                            {
-                                'name': service_uuid,
-                                'interfaces': []
-                            }
-                        ]
-                    }
-                }
-            }
-        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
-        log.info(onos_config)
-        response.raise_for_status()
-        for connection_point in connection_points:
-            self._add_network_port(service_uuid, connection_point)
-        return service_uuid, onos_config
-
-    def edit_connectivity_service(self, service_uuid,
-                                  conn_info, connection_points,
-                                  **kwargs):
-        raise SdnConnectorError('Not supported', http_code=501)
-
-    def delete_connectivity_service(self, service_uuid, conn_info=None):
-        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
-        onos_config_req.raise_for_status()
-        onos_config = onos_config_req.json()
-        # Removes ports used by network from onos config
-        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
-            if vpls['name'] == service_uuid:
-                for interface in vpls['interfaces']:
-                    for port in onos_config['ports'].values():
-                        for port_interface in port['interfaces']:
-                            if port_interface['name'] == interface:
-                                port['interfaces'].remove(port_interface)
-                onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList'].remove(vpls)
-                break
-        self._pop_last_update_time(onos_config)
-        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
-        response.raise_for_status()
-
-    def _delete_network_port(self, net_id, port):
-        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
-        onos_config_req.raise_for_status()
-        onos_config = onos_config_req.json()
-        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
-            if vpls['name'] == net_id:
-                for interface in vpls['interfaces']:
-                    if interface == port['service_endpoint_id']:
-                        vpls['interfaces'].remove(interface)
-                        break
-        for onos_port in onos_config['ports'].values():
-            for port_interface in onos_port['interfaces']:
-                if port_interface['name'] == port['service_endpoint_id']:
-                    onos_port['interfaces'].remove(port_interface)
-                    break
-        self._pop_last_update_time(onos_config)
-        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
-        response.raise_for_status()
-
-    def _add_network_port(self, net_id, port):
-        onos_config_req = requests.get(self.url, auth=HTTPBasicAuth(self.user, self.password))
-        onos_config_req.raise_for_status()
-        onos_config = onos_config_req.json()
-        self._append_port_to_onos_config(port, onos_config)
-        # Interfaces need to be registered before adding them to VPLS
-        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
-        response.raise_for_status()
-        for vpls in onos_config['apps']['org.onosproject.vpls']['vpls']['vplsList']:
-            if vpls['name'] == net_id:
-                vpls['interfaces'].append(port['service_endpoint_id'])
-                break
-        self._pop_last_update_time(onos_config)
-        response = requests.post(self.url, json=onos_config, auth=HTTPBasicAuth(self.user, self.password))
-        response.raise_for_status()
-
-    def _pop_last_update_time(self, onos_config):
-        if 'lastUpdateTime' in onos_config['apps']['org.onosproject.vpls']['vpls']:
-            onos_config['apps']['org.onosproject.vpls']['vpls'].pop('lastUpdateTime')
-
-    def _append_port_to_onos_config(self, port, onos_config):
-        port_name = 'of:{}/{}'.format(port['service_endpoint_encapsulation_info']['switch_dpid'],
-                                  port['service_endpoint_encapsulation_info']['switch_port'])
-        interface_config = {'name': port['service_endpoint_id']}
-        if 'vlan' in port['service_endpoint_encapsulation_info'] and port['service_endpoint_encapsulation_info'][
-            'vlan']:
-            interface_config['vlan'] = port['service_endpoint_encapsulation_info']['vlan']
-        if port_name in onos_config['ports'] and 'interfaces' in onos_config['ports'][port_name]:
-            for interface in onos_config['ports'][port_name]['interfaces']:
-                if interface['name'] == port['service_endpoint_id']:
-                    onos_config['ports'][port_name]['interfaces'].remove(interface)
-            onos_config['ports'][port_name]['interfaces'].append(interface_config)
-        else:
-            onos_config['ports'][port_name] = {
-                'interfaces': [interface_config]
-            }
-
-
-if __name__ == '__main__':
-    pass
-    # host = '198.204.228.85'
-    # port = 8181
-    # onos_vpls = OnosVpls(host, port, 'onos', 'rocks')
-    # ports = [
-    #     {
-    #         'uuid': '0a43961d',
-    #         'switch_dpid': '0000000000000001',
-    #         'switch_port': '1',
-    #         'vlan': 100
-    #     },
-    #     {
-    #         'uuid': 'ade3eefc',
-    #         'switch_dpid': '0000000000000003',
-    #         'switch_port': '1',
-    #         'vlan': 100
-    #     }
-    # ]
-    # onos_vpls.create_network('94979b37-3875-4f77-b620-01ff78f9c4fa', 'data')
-    # onos_vpls.add_network_port('94979b37-3875-4f77-b620-01ff78f9c4fa', ports[0])
-    # onos_vpls.add_network_port('94979b37-3875-4f77-b620-01ff78f9c4fa', ports[1])
-    # onos_vpls.delete_network_port('94979b37-3875-4f77-b620-01ff78f9c4fa', ports[1])
-    # onos_vpls.delete_network('94979b37-3875-4f77-b620-01ff78f9c4fa')
index 83630d4..0785cb5 100644 (file)
@@ -22,7 +22,7 @@ _name = "osm_rosdn_onos_vpls"
 
 README = """
 ===========
 
 README = """
 ===========
-osm-rosdn_onosvpls
+osm-rosdn_onos_vpls
 ===========
 
 osm-ro pluging for ONOS VPLS SDN
 ===========
 
 osm-ro pluging for ONOS VPLS SDN
@@ -48,6 +48,6 @@ setup(
     install_requires=["requests", "osm-ro"],
     setup_requires=['setuptools-version-command'],
     entry_points={
     install_requires=["requests", "osm-ro"],
     setup_requires=['setuptools-version-command'],
     entry_points={
-        'osm_rosdn.plugins': ['rosdn_onosvpls = osm_rosdn_onosvpls.sdn_assist_onos_vpls:OnosVpls'],
+        'osm_rosdn.plugins': ['rosdn_onos_vpls = osm_rosdn_onos_vpls.sdn_assist_onos_vpls:OnosVpls'],
     },
 )
     },
 )
index 00ac655..e72bc16 100644 (file)
@@ -26,12 +26,12 @@ commands=python3 -m unittest discover -v
 [testenv:flake8]
 basepython = python3
 deps = flake8
 [testenv:flake8]
 basepython = python3
 deps = flake8
-commands = flake8 osm_rosdn_onosvpls --max-line-length 120 \
+commands = flake8 osm_rosdn_onos_vpls --max-line-length 120 \
     --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504
 
 [testenv:unittest]
 basepython = python3
     --exclude .svn,CVS,.gz,.git,__pycache__,.tox,local,temp --ignore W291,W293,E226,W504
 
 [testenv:unittest]
 basepython = python3
-commands = python3 -m unittest osm_rosdn_onosvpls.tests
+commands = python3 -m unittest osm_rosdn_onos_vpls.tests
 
 [testenv:build]
 basepython = python3
 
 [testenv:build]
 basepython = python3