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