1# engine/url.py 
    2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors 
    3# <see AUTHORS file> 
    4# 
    5# This module is part of SQLAlchemy and is released under 
    6# the MIT License: https://www.opensource.org/licenses/mit-license.php 
    7 
    8"""Provides the :class:`~sqlalchemy.engine.url.URL` class which encapsulates 
    9information about a database connection specification. 
    10 
    11The URL object is created automatically when 
    12:func:`~sqlalchemy.engine.create_engine` is called with a string 
    13argument; alternatively, the URL is a public-facing construct which can 
    14be used directly and is also accepted directly by ``create_engine()``. 
    15""" 
    16 
    17from __future__ import annotations 
    18 
    19import collections.abc as collections_abc 
    20import re 
    21from typing import Any 
    22from typing import cast 
    23from typing import Dict 
    24from typing import Iterable 
    25from typing import List 
    26from typing import Mapping 
    27from typing import NamedTuple 
    28from typing import Optional 
    29from typing import overload 
    30from typing import Sequence 
    31from typing import Tuple 
    32from typing import Type 
    33from typing import Union 
    34from urllib.parse import parse_qsl 
    35from urllib.parse import quote 
    36from urllib.parse import quote_plus 
    37from urllib.parse import unquote 
    38 
    39from .interfaces import Dialect 
    40from .. import exc 
    41from .. import util 
    42from ..dialects import plugins 
    43from ..dialects import registry 
    44 
    45 
    46class URL(NamedTuple): 
    47    """ 
    48    Represent the components of a URL used to connect to a database. 
    49 
    50    URLs are typically constructed from a fully formatted URL string, where the 
    51    :func:`.make_url` function is used internally by the 
    52    :func:`_sa.create_engine` function in order to parse the URL string into 
    53    its individual components, which are then used to construct a new 
    54    :class:`.URL` object. When parsing from a formatted URL string, the parsing 
    55    format generally follows 
    56    `RFC-1738 <https://www.ietf.org/rfc/rfc1738.txt>`_, with some exceptions. 
    57 
    58    A :class:`_engine.URL` object may also be produced directly, either by 
    59    using the :func:`.make_url` function with a fully formed URL string, or 
    60    by using the :meth:`_engine.URL.create` constructor in order 
    61    to construct a :class:`_engine.URL` programmatically given individual 
    62    fields. The resulting :class:`.URL` object may be passed directly to 
    63    :func:`_sa.create_engine` in place of a string argument, which will bypass 
    64    the usage of :func:`.make_url` within the engine's creation process. 
    65 
    66    .. versionchanged:: 1.4 
    67 
    68        The :class:`_engine.URL` object is now an immutable object.  To 
    69        create a URL, use the :func:`_engine.make_url` or 
    70        :meth:`_engine.URL.create` function / method.  To modify 
    71        a :class:`_engine.URL`, use methods like 
    72        :meth:`_engine.URL.set` and 
    73        :meth:`_engine.URL.update_query_dict` to return a new 
    74        :class:`_engine.URL` object with modifications.   See notes for this 
    75        change at :ref:`change_5526`. 
    76 
    77    .. seealso:: 
    78 
    79        :ref:`database_urls` 
    80 
    81    :class:`_engine.URL` contains the following attributes: 
    82 
    83    * :attr:`_engine.URL.drivername`: database backend and driver name, such as 
    84      ``postgresql+psycopg2`` 
    85    * :attr:`_engine.URL.username`: username string 
    86    * :attr:`_engine.URL.password`: password string 
    87    * :attr:`_engine.URL.host`: string hostname 
    88    * :attr:`_engine.URL.port`: integer port number 
    89    * :attr:`_engine.URL.database`: string database name 
    90    * :attr:`_engine.URL.query`: an immutable mapping representing the query 
    91      string.  contains strings for keys and either strings or tuples of 
    92      strings for values. 
    93 
    94 
    95    """ 
    96 
    97    drivername: str 
    98    """database backend and driver name, such as 
    99    ``postgresql+psycopg2`` 
    100 
    101    """ 
    102 
    103    username: Optional[str] 
    104    "username string" 
    105 
    106    password: Optional[str] 
    107    """password, which is normally a string but may also be any 
    108    object that has a ``__str__()`` method.""" 
    109 
    110    host: Optional[str] 
    111    """hostname or IP number.  May also be a data source name for some 
    112    drivers.""" 
    113 
    114    port: Optional[int] 
    115    """integer port number""" 
    116 
    117    database: Optional[str] 
    118    """database name""" 
    119 
    120    query: util.immutabledict[str, Union[Tuple[str, ...], str]] 
    121    """an immutable mapping representing the query string.  contains strings 
    122       for keys and either strings or tuples of strings for values, e.g.:: 
    123 
    124            >>> from sqlalchemy.engine import make_url 
    125            >>> url = make_url( 
    126            ...     "postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt" 
    127            ... ) 
    128            >>> url.query 
    129            immutabledict({'alt_host': ('host1', 'host2'), 'ssl_cipher': '/path/to/crt'}) 
    130 
    131         To create a mutable copy of this mapping, use the ``dict`` constructor:: 
    132 
    133            mutable_query_opts = dict(url.query) 
    134 
    135       .. seealso:: 
    136 
    137          :attr:`_engine.URL.normalized_query` - normalizes all values into sequences 
    138          for consistent processing 
    139 
    140          Methods for altering the contents of :attr:`_engine.URL.query`: 
    141 
    142          :meth:`_engine.URL.update_query_dict` 
    143 
    144          :meth:`_engine.URL.update_query_string` 
    145 
    146          :meth:`_engine.URL.update_query_pairs` 
    147 
    148          :meth:`_engine.URL.difference_update_query` 
    149 
    150    """  # noqa: E501 
    151 
    152    @classmethod 
    153    def create( 
    154        cls, 
    155        drivername: str, 
    156        username: Optional[str] = None, 
    157        password: Optional[str] = None, 
    158        host: Optional[str] = None, 
    159        port: Optional[int] = None, 
    160        database: Optional[str] = None, 
    161        query: Mapping[str, Union[Sequence[str], str]] = util.EMPTY_DICT, 
    162    ) -> URL: 
    163        """Create a new :class:`_engine.URL` object. 
    164 
    165        .. seealso:: 
    166 
    167            :ref:`database_urls` 
    168 
    169        :param drivername: the name of the database backend. This name will 
    170          correspond to a module in sqlalchemy/databases or a third party 
    171          plug-in. 
    172        :param username: The user name. 
    173        :param password: database password.  Is typically a string, but may 
    174          also be an object that can be stringified with ``str()``. 
    175 
    176          .. note:: The password string should **not** be URL encoded when 
    177             passed as an argument to :meth:`_engine.URL.create`; the string 
    178             should contain the password characters exactly as they would be 
    179             typed. 
    180 
    181          .. note::  A password-producing object will be stringified only 
    182             **once** per :class:`_engine.Engine` object.  For dynamic password 
    183             generation per connect, see :ref:`engines_dynamic_tokens`. 
    184 
    185        :param host: The name of the host. 
    186        :param port: The port number. 
    187        :param database: The database name. 
    188        :param query: A dictionary of string keys to string values to be passed 
    189          to the dialect and/or the DBAPI upon connect.   To specify non-string 
    190          parameters to a Python DBAPI directly, use the 
    191          :paramref:`_sa.create_engine.connect_args` parameter to 
    192          :func:`_sa.create_engine`.   See also 
    193          :attr:`_engine.URL.normalized_query` for a dictionary that is 
    194          consistently string->list of string. 
    195        :return: new :class:`_engine.URL` object. 
    196 
    197        .. versionadded:: 1.4 
    198 
    199            The :class:`_engine.URL` object is now an **immutable named 
    200            tuple**.  In addition, the ``query`` dictionary is also immutable. 
    201            To create a URL, use the :func:`_engine.url.make_url` or 
    202            :meth:`_engine.URL.create` function/ method.  To modify a 
    203            :class:`_engine.URL`, use the :meth:`_engine.URL.set` and 
    204            :meth:`_engine.URL.update_query` methods. 
    205 
    206        """ 
    207 
    208        return cls( 
    209            cls._assert_str(drivername, "drivername"), 
    210            cls._assert_none_str(username, "username"), 
    211            password, 
    212            cls._assert_none_str(host, "host"), 
    213            cls._assert_port(port), 
    214            cls._assert_none_str(database, "database"), 
    215            cls._str_dict(query), 
    216        ) 
    217 
    218    @classmethod 
    219    def _assert_port(cls, port: Optional[int]) -> Optional[int]: 
    220        if port is None: 
    221            return None 
    222        try: 
    223            return int(port) 
    224        except TypeError: 
    225            raise TypeError("Port argument must be an integer or None") 
    226 
    227    @classmethod 
    228    def _assert_str(cls, v: str, paramname: str) -> str: 
    229        if not isinstance(v, str): 
    230            raise TypeError("%s must be a string" % paramname) 
    231        return v 
    232 
    233    @classmethod 
    234    def _assert_none_str( 
    235        cls, v: Optional[str], paramname: str 
    236    ) -> Optional[str]: 
    237        if v is None: 
    238            return v 
    239 
    240        return cls._assert_str(v, paramname) 
    241 
    242    @classmethod 
    243    def _str_dict( 
    244        cls, 
    245        dict_: Optional[ 
    246            Union[ 
    247                Sequence[Tuple[str, Union[Sequence[str], str]]], 
    248                Mapping[str, Union[Sequence[str], str]], 
    249            ] 
    250        ], 
    251    ) -> util.immutabledict[str, Union[Tuple[str, ...], str]]: 
    252        if dict_ is None: 
    253            return util.EMPTY_DICT 
    254 
    255        @overload 
    256        def _assert_value( 
    257            val: str, 
    258        ) -> str: ... 
    259 
    260        @overload 
    261        def _assert_value( 
    262            val: Sequence[str], 
    263        ) -> Union[str, Tuple[str, ...]]: ... 
    264 
    265        def _assert_value( 
    266            val: Union[str, Sequence[str]], 
    267        ) -> Union[str, Tuple[str, ...]]: 
    268            if isinstance(val, str): 
    269                return val 
    270            elif isinstance(val, collections_abc.Sequence): 
    271                return tuple(_assert_value(elem) for elem in val) 
    272            else: 
    273                raise TypeError( 
    274                    "Query dictionary values must be strings or " 
    275                    "sequences of strings" 
    276                ) 
    277 
    278        def _assert_str(v: str) -> str: 
    279            if not isinstance(v, str): 
    280                raise TypeError("Query dictionary keys must be strings") 
    281            return v 
    282 
    283        dict_items: Iterable[Tuple[str, Union[Sequence[str], str]]] 
    284        if isinstance(dict_, collections_abc.Sequence): 
    285            dict_items = dict_ 
    286        else: 
    287            dict_items = dict_.items() 
    288 
    289        return util.immutabledict( 
    290            { 
    291                _assert_str(key): _assert_value( 
    292                    value, 
    293                ) 
    294                for key, value in dict_items 
    295            } 
    296        ) 
    297 
    298    def set( 
    299        self, 
    300        drivername: Optional[str] = None, 
    301        username: Optional[str] = None, 
    302        password: Optional[str] = None, 
    303        host: Optional[str] = None, 
    304        port: Optional[int] = None, 
    305        database: Optional[str] = None, 
    306        query: Optional[Mapping[str, Union[Sequence[str], str]]] = None, 
    307    ) -> URL: 
    308        """return a new :class:`_engine.URL` object with modifications. 
    309 
    310        Values are used if they are non-None.  To set a value to ``None`` 
    311        explicitly, use the :meth:`_engine.URL._replace` method adapted 
    312        from ``namedtuple``. 
    313 
    314        :param drivername: new drivername 
    315        :param username: new username 
    316        :param password: new password 
    317        :param host: new hostname 
    318        :param port: new port 
    319        :param query: new query parameters, passed a dict of string keys 
    320         referring to string or sequence of string values.  Fully 
    321         replaces the previous list of arguments. 
    322 
    323        :return: new :class:`_engine.URL` object. 
    324 
    325        .. versionadded:: 1.4 
    326 
    327        .. seealso:: 
    328 
    329            :meth:`_engine.URL.update_query_dict` 
    330 
    331        """ 
    332 
    333        kw: Dict[str, Any] = {} 
    334        if drivername is not None: 
    335            kw["drivername"] = drivername 
    336        if username is not None: 
    337            kw["username"] = username 
    338        if password is not None: 
    339            kw["password"] = password 
    340        if host is not None: 
    341            kw["host"] = host 
    342        if port is not None: 
    343            kw["port"] = port 
    344        if database is not None: 
    345            kw["database"] = database 
    346        if query is not None: 
    347            kw["query"] = query 
    348 
    349        return self._assert_replace(**kw) 
    350 
    351    def _assert_replace(self, **kw: Any) -> URL: 
    352        """argument checks before calling _replace()""" 
    353 
    354        if "drivername" in kw: 
    355            self._assert_str(kw["drivername"], "drivername") 
    356        for name in "username", "host", "database": 
    357            if name in kw: 
    358                self._assert_none_str(kw[name], name) 
    359        if "port" in kw: 
    360            self._assert_port(kw["port"]) 
    361        if "query" in kw: 
    362            kw["query"] = self._str_dict(kw["query"]) 
    363 
    364        return self._replace(**kw) 
    365 
    366    def update_query_string( 
    367        self, query_string: str, append: bool = False 
    368    ) -> URL: 
    369        """Return a new :class:`_engine.URL` object with the :attr:`_engine.URL.query` 
    370        parameter dictionary updated by the given query string. 
    371 
    372        E.g.:: 
    373 
    374            >>> from sqlalchemy.engine import make_url 
    375            >>> url = make_url("postgresql+psycopg2://user:pass@host/dbname") 
    376            >>> url = url.update_query_string( 
    377            ...     "alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt" 
    378            ... ) 
    379            >>> str(url) 
    380            'postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt' 
    381 
    382        :param query_string: a URL escaped query string, not including the 
    383         question mark. 
    384 
    385        :param append: if True, parameters in the existing query string will 
    386         not be removed; new parameters will be in addition to those present. 
    387         If left at its default of False, keys present in the given query 
    388         parameters will replace those of the existing query string. 
    389 
    390        .. versionadded:: 1.4 
    391 
    392        .. seealso:: 
    393 
    394            :attr:`_engine.URL.query` 
    395 
    396            :meth:`_engine.URL.update_query_dict` 
    397 
    398        """  # noqa: E501 
    399        return self.update_query_pairs(parse_qsl(query_string), append=append) 
    400 
    401    def update_query_pairs( 
    402        self, 
    403        key_value_pairs: Iterable[Tuple[str, Union[str, List[str]]]], 
    404        append: bool = False, 
    405    ) -> URL: 
    406        """Return a new :class:`_engine.URL` object with the 
    407        :attr:`_engine.URL.query` 
    408        parameter dictionary updated by the given sequence of key/value pairs 
    409 
    410        E.g.:: 
    411 
    412            >>> from sqlalchemy.engine import make_url 
    413            >>> url = make_url("postgresql+psycopg2://user:pass@host/dbname") 
    414            >>> url = url.update_query_pairs( 
    415            ...     [ 
    416            ...         ("alt_host", "host1"), 
    417            ...         ("alt_host", "host2"), 
    418            ...         ("ssl_cipher", "/path/to/crt"), 
    419            ...     ] 
    420            ... ) 
    421            >>> str(url) 
    422            'postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt' 
    423 
    424        :param key_value_pairs: A sequence of tuples containing two strings 
    425         each. 
    426 
    427        :param append: if True, parameters in the existing query string will 
    428         not be removed; new parameters will be in addition to those present. 
    429         If left at its default of False, keys present in the given query 
    430         parameters will replace those of the existing query string. 
    431 
    432        .. versionadded:: 1.4 
    433 
    434        .. seealso:: 
    435 
    436            :attr:`_engine.URL.query` 
    437 
    438            :meth:`_engine.URL.difference_update_query` 
    439 
    440            :meth:`_engine.URL.set` 
    441 
    442        """  # noqa: E501 
    443 
    444        existing_query = self.query 
    445        new_keys: Dict[str, Union[str, List[str]]] = {} 
    446 
    447        for key, value in key_value_pairs: 
    448            if key in new_keys: 
    449                new_keys[key] = util.to_list(new_keys[key]) 
    450                cast("List[str]", new_keys[key]).append(cast(str, value)) 
    451            else: 
    452                new_keys[key] = ( 
    453                    list(value) if isinstance(value, (list, tuple)) else value 
    454                ) 
    455 
    456        new_query: Mapping[str, Union[str, Sequence[str]]] 
    457        if append: 
    458            new_query = {} 
    459 
    460            for k in new_keys: 
    461                if k in existing_query: 
    462                    new_query[k] = tuple( 
    463                        util.to_list(existing_query[k]) 
    464                        + util.to_list(new_keys[k]) 
    465                    ) 
    466                else: 
    467                    new_query[k] = new_keys[k] 
    468 
    469            new_query.update( 
    470                { 
    471                    k: existing_query[k] 
    472                    for k in set(existing_query).difference(new_keys) 
    473                } 
    474            ) 
    475        else: 
    476            new_query = self.query.union( 
    477                { 
    478                    k: tuple(v) if isinstance(v, list) else v 
    479                    for k, v in new_keys.items() 
    480                } 
    481            ) 
    482        return self.set(query=new_query) 
    483 
    484    def update_query_dict( 
    485        self, 
    486        query_parameters: Mapping[str, Union[str, List[str]]], 
    487        append: bool = False, 
    488    ) -> URL: 
    489        """Return a new :class:`_engine.URL` object with the 
    490        :attr:`_engine.URL.query` parameter dictionary updated by the given 
    491        dictionary. 
    492 
    493        The dictionary typically contains string keys and string values. 
    494        In order to represent a query parameter that is expressed multiple 
    495        times, pass a sequence of string values. 
    496 
    497        E.g.:: 
    498 
    499 
    500            >>> from sqlalchemy.engine import make_url 
    501            >>> url = make_url("postgresql+psycopg2://user:pass@host/dbname") 
    502            >>> url = url.update_query_dict( 
    503            ...     {"alt_host": ["host1", "host2"], "ssl_cipher": "/path/to/crt"} 
    504            ... ) 
    505            >>> str(url) 
    506            'postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt' 
    507 
    508 
    509        :param query_parameters: A dictionary with string keys and values 
    510         that are either strings, or sequences of strings. 
    511 
    512        :param append: if True, parameters in the existing query string will 
    513         not be removed; new parameters will be in addition to those present. 
    514         If left at its default of False, keys present in the given query 
    515         parameters will replace those of the existing query string. 
    516 
    517 
    518        .. versionadded:: 1.4 
    519 
    520        .. seealso:: 
    521 
    522            :attr:`_engine.URL.query` 
    523 
    524            :meth:`_engine.URL.update_query_string` 
    525 
    526            :meth:`_engine.URL.update_query_pairs` 
    527 
    528            :meth:`_engine.URL.difference_update_query` 
    529 
    530            :meth:`_engine.URL.set` 
    531 
    532        """  # noqa: E501 
    533        return self.update_query_pairs(query_parameters.items(), append=append) 
    534 
    535    def difference_update_query(self, names: Iterable[str]) -> URL: 
    536        """ 
    537        Remove the given names from the :attr:`_engine.URL.query` dictionary, 
    538        returning the new :class:`_engine.URL`. 
    539 
    540        E.g.:: 
    541 
    542            url = url.difference_update_query(["foo", "bar"]) 
    543 
    544        Equivalent to using :meth:`_engine.URL.set` as follows:: 
    545 
    546            url = url.set( 
    547                query={ 
    548                    key: url.query[key] 
    549                    for key in set(url.query).difference(["foo", "bar"]) 
    550                } 
    551            ) 
    552 
    553        .. versionadded:: 1.4 
    554 
    555        .. seealso:: 
    556 
    557            :attr:`_engine.URL.query` 
    558 
    559            :meth:`_engine.URL.update_query_dict` 
    560 
    561            :meth:`_engine.URL.set` 
    562 
    563        """ 
    564 
    565        if not set(names).intersection(self.query): 
    566            return self 
    567 
    568        return URL( 
    569            self.drivername, 
    570            self.username, 
    571            self.password, 
    572            self.host, 
    573            self.port, 
    574            self.database, 
    575            util.immutabledict( 
    576                { 
    577                    key: self.query[key] 
    578                    for key in set(self.query).difference(names) 
    579                } 
    580            ), 
    581        ) 
    582 
    583    @property 
    584    def normalized_query(self) -> Mapping[str, Sequence[str]]: 
    585        """Return the :attr:`_engine.URL.query` dictionary with values normalized 
    586        into sequences. 
    587 
    588        As the :attr:`_engine.URL.query` dictionary may contain either 
    589        string values or sequences of string values to differentiate between 
    590        parameters that are specified multiple times in the query string, 
    591        code that needs to handle multiple parameters generically will wish 
    592        to use this attribute so that all parameters present are presented 
    593        as sequences.   Inspiration is from Python's ``urllib.parse.parse_qs`` 
    594        function.  E.g.:: 
    595 
    596 
    597            >>> from sqlalchemy.engine import make_url 
    598            >>> url = make_url( 
    599            ...     "postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt" 
    600            ... ) 
    601            >>> url.query 
    602            immutabledict({'alt_host': ('host1', 'host2'), 'ssl_cipher': '/path/to/crt'}) 
    603            >>> url.normalized_query 
    604            immutabledict({'alt_host': ('host1', 'host2'), 'ssl_cipher': ('/path/to/crt',)}) 
    605 
    606        """  # noqa: E501 
    607 
    608        return util.immutabledict( 
    609            { 
    610                k: (v,) if not isinstance(v, tuple) else v 
    611                for k, v in self.query.items() 
    612            } 
    613        ) 
    614 
    615    @util.deprecated( 
    616        "1.4", 
    617        "The :meth:`_engine.URL.__to_string__ method is deprecated and will " 
    618        "be removed in a future release.  Please use the " 
    619        ":meth:`_engine.URL.render_as_string` method.", 
    620    ) 
    621    def __to_string__(self, hide_password: bool = True) -> str: 
    622        """Render this :class:`_engine.URL` object as a string. 
    623 
    624        :param hide_password: Defaults to True.   The password is not shown 
    625         in the string unless this is set to False. 
    626 
    627        """ 
    628        return self.render_as_string(hide_password=hide_password) 
    629 
    630    def render_as_string(self, hide_password: bool = True) -> str: 
    631        """Render this :class:`_engine.URL` object as a string. 
    632 
    633        This method is used when the ``__str__()`` or ``__repr__()`` 
    634        methods are used.   The method directly includes additional options. 
    635 
    636        :param hide_password: Defaults to True.   The password is not shown 
    637         in the string unless this is set to False. 
    638 
    639        """ 
    640        s = self.drivername + "://" 
    641        if self.username is not None: 
    642            s += quote(self.username, safe=" +") 
    643            if self.password is not None: 
    644                s += ":" + ( 
    645                    "***" 
    646                    if hide_password 
    647                    else quote(str(self.password), safe=" +") 
    648                ) 
    649            s += "@" 
    650        if self.host is not None: 
    651            if ":" in self.host: 
    652                s += f"[{self.host}]" 
    653            else: 
    654                s += self.host 
    655        if self.port is not None: 
    656            s += ":" + str(self.port) 
    657        if self.database is not None: 
    658            s += "/" + quote(self.database, safe=" +/") 
    659        if self.query: 
    660            keys = list(self.query) 
    661            keys.sort() 
    662            s += "?" + "&".join( 
    663                f"{quote_plus(k)}={quote_plus(element)}" 
    664                for k in keys 
    665                for element in util.to_list(self.query[k]) 
    666            ) 
    667        return s 
    668 
    669    def __repr__(self) -> str: 
    670        return self.render_as_string() 
    671 
    672    def __copy__(self) -> URL: 
    673        return self.__class__.create( 
    674            self.drivername, 
    675            self.username, 
    676            self.password, 
    677            self.host, 
    678            self.port, 
    679            self.database, 
    680            # note this is an immutabledict of str-> str / tuple of str, 
    681            # also fully immutable.  does not require deepcopy 
    682            self.query, 
    683        ) 
    684 
    685    def __deepcopy__(self, memo: Any) -> URL: 
    686        return self.__copy__() 
    687 
    688    def __hash__(self) -> int: 
    689        return hash(str(self)) 
    690 
    691    def __eq__(self, other: Any) -> bool: 
    692        return ( 
    693            isinstance(other, URL) 
    694            and self.drivername == other.drivername 
    695            and self.username == other.username 
    696            and self.password == other.password 
    697            and self.host == other.host 
    698            and self.database == other.database 
    699            and self.query == other.query 
    700            and self.port == other.port 
    701        ) 
    702 
    703    def __ne__(self, other: Any) -> bool: 
    704        return not self == other 
    705 
    706    def get_backend_name(self) -> str: 
    707        """Return the backend name. 
    708 
    709        This is the name that corresponds to the database backend in 
    710        use, and is the portion of the :attr:`_engine.URL.drivername` 
    711        that is to the left of the plus sign. 
    712 
    713        """ 
    714        if "+" not in self.drivername: 
    715            return self.drivername 
    716        else: 
    717            return self.drivername.split("+")[0] 
    718 
    719    def get_driver_name(self) -> str: 
    720        """Return the backend name. 
    721 
    722        This is the name that corresponds to the DBAPI driver in 
    723        use, and is the portion of the :attr:`_engine.URL.drivername` 
    724        that is to the right of the plus sign. 
    725 
    726        If the :attr:`_engine.URL.drivername` does not include a plus sign, 
    727        then the default :class:`_engine.Dialect` for this :class:`_engine.URL` 
    728        is imported in order to get the driver name. 
    729 
    730        """ 
    731 
    732        if "+" not in self.drivername: 
    733            return self.get_dialect().driver 
    734        else: 
    735            return self.drivername.split("+")[1] 
    736 
    737    def _instantiate_plugins( 
    738        self, kwargs: Mapping[str, Any] 
    739    ) -> Tuple[URL, List[Any], Dict[str, Any]]: 
    740        plugin_names = util.to_list(self.query.get("plugin", ())) 
    741        plugin_names += kwargs.get("plugins", []) 
    742 
    743        kwargs = dict(kwargs) 
    744 
    745        loaded_plugins = [ 
    746            plugins.load(plugin_name)(self, kwargs) 
    747            for plugin_name in plugin_names 
    748        ] 
    749 
    750        u = self.difference_update_query(["plugin", "plugins"]) 
    751 
    752        for plugin in loaded_plugins: 
    753            new_u = plugin.update_url(u) 
    754            if new_u is not None: 
    755                u = new_u 
    756 
    757        kwargs.pop("plugins", None) 
    758 
    759        return u, loaded_plugins, kwargs 
    760 
    761    def _get_entrypoint(self) -> Type[Dialect]: 
    762        """Return the "entry point" dialect class. 
    763 
    764        This is normally the dialect itself except in the case when the 
    765        returned class implements the get_dialect_cls() method. 
    766 
    767        """ 
    768        if "+" not in self.drivername: 
    769            name = self.drivername 
    770        else: 
    771            name = self.drivername.replace("+", ".") 
    772        cls = registry.load(name) 
    773        # check for legacy dialects that 
    774        # would return a module with 'dialect' as the 
    775        # actual class 
    776        if ( 
    777            hasattr(cls, "dialect") 
    778            and isinstance(cls.dialect, type) 
    779            and issubclass(cls.dialect, Dialect) 
    780        ): 
    781            return cls.dialect 
    782        else: 
    783            return cast("Type[Dialect]", cls) 
    784 
    785    def get_dialect(self, _is_async: bool = False) -> Type[Dialect]: 
    786        """Return the SQLAlchemy :class:`_engine.Dialect` class corresponding 
    787        to this URL's driver name. 
    788 
    789        """ 
    790        entrypoint = self._get_entrypoint() 
    791        if _is_async: 
    792            dialect_cls = entrypoint.get_async_dialect_cls(self) 
    793        else: 
    794            dialect_cls = entrypoint.get_dialect_cls(self) 
    795        return dialect_cls 
    796 
    797    def translate_connect_args( 
    798        self, names: Optional[List[str]] = None, **kw: Any 
    799    ) -> Dict[str, Any]: 
    800        r"""Translate url attributes into a dictionary of connection arguments. 
    801 
    802        Returns attributes of this url (`host`, `database`, `username`, 
    803        `password`, `port`) as a plain dictionary.  The attribute names are 
    804        used as the keys by default.  Unset or false attributes are omitted 
    805        from the final dictionary. 
    806 
    807        :param \**kw: Optional, alternate key names for url attributes. 
    808 
    809        :param names: Deprecated.  Same purpose as the keyword-based alternate 
    810            names, but correlates the name to the original positionally. 
    811        """ 
    812 
    813        if names is not None: 
    814            util.warn_deprecated( 
    815                "The `URL.translate_connect_args.name`s parameter is " 
    816                "deprecated. Please pass the " 
    817                "alternate names as kw arguments.", 
    818                "1.4", 
    819            ) 
    820 
    821        translated = {} 
    822        attribute_names = ["host", "database", "username", "password", "port"] 
    823        for sname in attribute_names: 
    824            if names: 
    825                name = names.pop(0) 
    826            elif sname in kw: 
    827                name = kw[sname] 
    828            else: 
    829                name = sname 
    830            if name is not None and getattr(self, sname, False): 
    831                if sname == "password": 
    832                    translated[name] = str(getattr(self, sname)) 
    833                else: 
    834                    translated[name] = getattr(self, sname) 
    835 
    836        return translated 
    837 
    838 
    839def make_url(name_or_url: Union[str, URL]) -> URL: 
    840    """Given a string, produce a new URL instance. 
    841 
    842    The format of the URL generally follows `RFC-1738 
    843    <https://www.ietf.org/rfc/rfc1738.txt>`_, with some exceptions, including 
    844    that underscores, and not dashes or periods, are accepted within the 
    845    "scheme" portion. 
    846 
    847    If a :class:`.URL` object is passed, it is returned as is. 
    848 
    849    .. seealso:: 
    850 
    851        :ref:`database_urls` 
    852 
    853    """ 
    854 
    855    if isinstance(name_or_url, str): 
    856        return _parse_url(name_or_url) 
    857    elif not isinstance(name_or_url, URL) and not hasattr( 
    858        name_or_url, "_sqla_is_testing_if_this_is_a_mock_object" 
    859    ): 
    860        raise exc.ArgumentError( 
    861            f"Expected string or URL object, got {name_or_url!r}" 
    862        ) 
    863    else: 
    864        return name_or_url 
    865 
    866 
    867def _parse_url(name: str) -> URL: 
    868    pattern = re.compile( 
    869        r""" 
    870            (?P<name>[\w\+]+):// 
    871            (?: 
    872                (?P<username>[^:/]*) 
    873                (?::(?P<password>[^@]*))? 
    874            @)? 
    875            (?: 
    876                (?: 
    877                    \[(?P<ipv6host>[^/\?]+)\] | 
    878                    (?P<ipv4host>[^/:\?]+) 
    879                )? 
    880                (?::(?P<port>[^/\?]*))? 
    881            )? 
    882            (?:/(?P<database>[^\?]*))? 
    883            (?:\?(?P<query>.*))? 
    884            """, 
    885        re.X, 
    886    ) 
    887 
    888    m = pattern.match(name) 
    889    if m is not None: 
    890        components = m.groupdict() 
    891        query: Optional[Dict[str, Union[str, List[str]]]] 
    892        if components["query"] is not None: 
    893            query = {} 
    894 
    895            for key, value in parse_qsl(components["query"]): 
    896                if key in query: 
    897                    query[key] = util.to_list(query[key]) 
    898                    cast("List[str]", query[key]).append(value) 
    899                else: 
    900                    query[key] = value 
    901        else: 
    902            query = None 
    903        components["query"] = query 
    904 
    905        for comp in "username", "password", "database": 
    906            if components[comp] is not None: 
    907                components[comp] = unquote(components[comp]) 
    908 
    909        ipv4host = components.pop("ipv4host") 
    910        ipv6host = components.pop("ipv6host") 
    911        components["host"] = ipv4host or ipv6host 
    912        name = components.pop("name") 
    913 
    914        if components["port"]: 
    915            components["port"] = int(components["port"]) 
    916 
    917        return URL.create(name, **components)  # type: ignore 
    918 
    919    else: 
    920        raise exc.ArgumentError( 
    921            "Could not parse SQLAlchemy URL from given URL string" 
    922        )