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 )