1# engine/url.py
2# Copyright (C) 2005-2026 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,
368 query_string: str,
369 append: bool = False,
370 *,
371 keep_blank_values: bool = False,
372 ) -> URL:
373 """Return a new :class:`_engine.URL` object with the :attr:`_engine.URL.query`
374 parameter dictionary updated by the given query string.
375
376 E.g.::
377
378 >>> from sqlalchemy.engine import make_url
379 >>> url = make_url("postgresql+psycopg2://user:pass@host/dbname")
380 >>> url = url.update_query_string(
381 ... "alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt"
382 ... )
383 >>> str(url)
384 'postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt'
385
386 :param query_string: a URL escaped query string, not including the
387 question mark.
388
389 :param append: if True, parameters in the existing query string will
390 not be removed; new parameters will be in addition to those present.
391 If left at its default of False, keys present in the given query
392 parameters will replace those of the existing query string.
393
394 :param keep_blank_values: if True, parameters with blank values
395 such as ``key=`` are retained in the query dictionary as empty
396 strings. If left at its default of False, parameters with blank
397 values are omitted.
398
399 .. versionadded:: 2.1.6
400
401 .. versionadded:: 1.4
402
403 .. seealso::
404
405 :attr:`_engine.URL.query`
406
407 :meth:`_engine.URL.update_query_dict`
408
409 """ # noqa: E501
410 return self.update_query_pairs(
411 parse_qsl(query_string, keep_blank_values=keep_blank_values),
412 append=append,
413 )
414
415 def update_query_pairs(
416 self,
417 key_value_pairs: Iterable[Tuple[str, Union[str, List[str]]]],
418 append: bool = False,
419 ) -> URL:
420 """Return a new :class:`_engine.URL` object with the
421 :attr:`_engine.URL.query`
422 parameter dictionary updated by the given sequence of key/value pairs
423
424 E.g.::
425
426 >>> from sqlalchemy.engine import make_url
427 >>> url = make_url("postgresql+psycopg2://user:pass@host/dbname")
428 >>> url = url.update_query_pairs(
429 ... [
430 ... ("alt_host", "host1"),
431 ... ("alt_host", "host2"),
432 ... ("ssl_cipher", "/path/to/crt"),
433 ... ]
434 ... )
435 >>> str(url)
436 'postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt'
437
438 :param key_value_pairs: A sequence of tuples containing two strings
439 each.
440
441 :param append: if True, parameters in the existing query string will
442 not be removed; new parameters will be in addition to those present.
443 If left at its default of False, keys present in the given query
444 parameters will replace those of the existing query string.
445
446 .. versionadded:: 1.4
447
448 .. seealso::
449
450 :attr:`_engine.URL.query`
451
452 :meth:`_engine.URL.difference_update_query`
453
454 :meth:`_engine.URL.set`
455
456 """ # noqa: E501
457
458 existing_query = self.query
459 new_keys: Dict[str, Union[str, List[str]]] = {}
460
461 for key, value in key_value_pairs:
462 if key in new_keys:
463 new_keys[key] = util.to_list(new_keys[key])
464 cast("List[str]", new_keys[key]).append(cast(str, value))
465 else:
466 new_keys[key] = (
467 list(value) if isinstance(value, (list, tuple)) else value
468 )
469
470 new_query: Mapping[str, Union[str, Sequence[str]]]
471 if append:
472 new_query = {}
473
474 for k in new_keys:
475 if k in existing_query:
476 new_query[k] = tuple(
477 util.to_list(existing_query[k])
478 + util.to_list(new_keys[k])
479 )
480 else:
481 new_query[k] = new_keys[k]
482
483 new_query.update(
484 {
485 k: existing_query[k]
486 for k in set(existing_query).difference(new_keys)
487 }
488 )
489 else:
490 new_query = self.query.union(
491 {
492 k: tuple(v) if isinstance(v, list) else v
493 for k, v in new_keys.items()
494 }
495 )
496 return self.set(query=new_query)
497
498 def update_query_dict(
499 self,
500 query_parameters: Mapping[str, Union[str, List[str]]],
501 append: bool = False,
502 ) -> URL:
503 """Return a new :class:`_engine.URL` object with the
504 :attr:`_engine.URL.query` parameter dictionary updated by the given
505 dictionary.
506
507 The dictionary typically contains string keys and string values.
508 In order to represent a query parameter that is expressed multiple
509 times, pass a sequence of string values.
510
511 E.g.::
512
513
514 >>> from sqlalchemy.engine import make_url
515 >>> url = make_url("postgresql+psycopg2://user:pass@host/dbname")
516 >>> url = url.update_query_dict(
517 ... {"alt_host": ["host1", "host2"], "ssl_cipher": "/path/to/crt"}
518 ... )
519 >>> str(url)
520 'postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt'
521
522
523 :param query_parameters: A dictionary with string keys and values
524 that are either strings, or sequences of strings.
525
526 :param append: if True, parameters in the existing query string will
527 not be removed; new parameters will be in addition to those present.
528 If left at its default of False, keys present in the given query
529 parameters will replace those of the existing query string.
530
531
532 .. versionadded:: 1.4
533
534 .. seealso::
535
536 :attr:`_engine.URL.query`
537
538 :meth:`_engine.URL.update_query_string`
539
540 :meth:`_engine.URL.update_query_pairs`
541
542 :meth:`_engine.URL.difference_update_query`
543
544 :meth:`_engine.URL.set`
545
546 """ # noqa: E501
547 return self.update_query_pairs(query_parameters.items(), append=append)
548
549 def difference_update_query(self, names: Iterable[str]) -> URL:
550 """
551 Remove the given names from the :attr:`_engine.URL.query` dictionary,
552 returning the new :class:`_engine.URL`.
553
554 E.g.::
555
556 url = url.difference_update_query(["foo", "bar"])
557
558 Equivalent to using :meth:`_engine.URL.set` as follows::
559
560 url = url.set(
561 query={
562 key: url.query[key]
563 for key in set(url.query).difference(["foo", "bar"])
564 }
565 )
566
567 .. versionadded:: 1.4
568
569 .. seealso::
570
571 :attr:`_engine.URL.query`
572
573 :meth:`_engine.URL.update_query_dict`
574
575 :meth:`_engine.URL.set`
576
577 """
578
579 if not set(names).intersection(self.query):
580 return self
581
582 return URL(
583 self.drivername,
584 self.username,
585 self.password,
586 self.host,
587 self.port,
588 self.database,
589 util.immutabledict(
590 {
591 key: self.query[key]
592 for key in set(self.query).difference(names)
593 }
594 ),
595 )
596
597 @property
598 def normalized_query(self) -> Mapping[str, Sequence[str]]:
599 """Return the :attr:`_engine.URL.query` dictionary with values normalized
600 into sequences.
601
602 As the :attr:`_engine.URL.query` dictionary may contain either
603 string values or sequences of string values to differentiate between
604 parameters that are specified multiple times in the query string,
605 code that needs to handle multiple parameters generically will wish
606 to use this attribute so that all parameters present are presented
607 as sequences. Inspiration is from Python's ``urllib.parse.parse_qs``
608 function. E.g.::
609
610
611 >>> from sqlalchemy.engine import make_url
612 >>> url = make_url(
613 ... "postgresql+psycopg2://user:pass@host/dbname?alt_host=host1&alt_host=host2&ssl_cipher=%2Fpath%2Fto%2Fcrt"
614 ... )
615 >>> url.query
616 immutabledict({'alt_host': ('host1', 'host2'), 'ssl_cipher': '/path/to/crt'})
617 >>> url.normalized_query
618 immutabledict({'alt_host': ('host1', 'host2'), 'ssl_cipher': ('/path/to/crt',)})
619
620 """ # noqa: E501
621
622 return util.immutabledict(
623 {
624 k: (v,) if not isinstance(v, tuple) else v
625 for k, v in self.query.items()
626 }
627 )
628
629 @util.deprecated(
630 "1.4",
631 "The :meth:`_engine.URL.__to_string__ method is deprecated and will "
632 "be removed in a future release. Please use the "
633 ":meth:`_engine.URL.render_as_string` method.",
634 )
635 def __to_string__(self, hide_password: bool = True) -> str:
636 """Render this :class:`_engine.URL` object as a string.
637
638 :param hide_password: Defaults to True. The password is not shown
639 in the string unless this is set to False.
640
641 """
642 return self.render_as_string(hide_password=hide_password)
643
644 def render_as_string(self, hide_password: bool = True) -> str:
645 """Render this :class:`_engine.URL` object as a string.
646
647 This method is used when the ``__str__()`` or ``__repr__()``
648 methods are used. The method directly includes additional options.
649
650 :param hide_password: Defaults to True. The password is not shown
651 in the string unless this is set to False.
652
653 """
654 s = self.drivername + "://"
655 if self.username is not None:
656 s += quote(self.username, safe=" +")
657 if self.password is not None:
658 s += ":" + (
659 "***"
660 if hide_password
661 else quote(str(self.password), safe=" +")
662 )
663 s += "@"
664 if self.host is not None:
665 if ":" in self.host:
666 s += f"[{self.host}]"
667 else:
668 s += self.host
669 if self.port is not None:
670 s += ":" + str(self.port)
671 if self.database is not None:
672 s += "/" + quote(self.database, safe=" +/")
673 if self.query:
674 keys = list(self.query)
675 keys.sort()
676 s += "?" + "&".join(
677 f"{quote_plus(k)}={quote_plus(element)}"
678 for k in keys
679 for element in util.to_list(self.query[k])
680 )
681 return s
682
683 def __repr__(self) -> str:
684 return self.render_as_string()
685
686 def __copy__(self) -> URL:
687 return self.__class__.create(
688 self.drivername,
689 self.username,
690 self.password,
691 self.host,
692 self.port,
693 self.database,
694 # note this is an immutabledict of str-> str / tuple of str,
695 # also fully immutable. does not require deepcopy
696 self.query,
697 )
698
699 def __deepcopy__(self, memo: Any) -> URL:
700 return self.__copy__()
701
702 def __hash__(self) -> int:
703 return hash(str(self))
704
705 def __eq__(self, other: Any) -> bool:
706 return (
707 isinstance(other, URL)
708 and self.drivername == other.drivername
709 and self.username == other.username
710 and self.password == other.password
711 and self.host == other.host
712 and self.database == other.database
713 and self.query == other.query
714 and self.port == other.port
715 )
716
717 def __ne__(self, other: Any) -> bool:
718 return not self == other
719
720 def get_backend_name(self) -> str:
721 """Return the backend name.
722
723 This is the name that corresponds to the database backend in
724 use, and is the portion of the :attr:`_engine.URL.drivername`
725 that is to the left of the plus sign.
726
727 """
728 if "+" not in self.drivername:
729 return self.drivername
730 else:
731 return self.drivername.split("+")[0]
732
733 def get_driver_name(self) -> str:
734 """Return the backend name.
735
736 This is the name that corresponds to the DBAPI driver in
737 use, and is the portion of the :attr:`_engine.URL.drivername`
738 that is to the right of the plus sign.
739
740 If the :attr:`_engine.URL.drivername` does not include a plus sign,
741 then the default :class:`_engine.Dialect` for this :class:`_engine.URL`
742 is imported in order to get the driver name.
743
744 """
745
746 if "+" not in self.drivername:
747 return self.get_dialect().driver
748 else:
749 return self.drivername.split("+")[1]
750
751 def _instantiate_plugins(
752 self, kwargs: Mapping[str, Any]
753 ) -> Tuple[URL, List[Any], Dict[str, Any]]:
754 plugin_names = util.to_list(self.query.get("plugin", ()))
755 plugin_names += kwargs.get("plugins", [])
756
757 kwargs = dict(kwargs)
758
759 loaded_plugins = [
760 plugins.load(plugin_name)(self, kwargs)
761 for plugin_name in plugin_names
762 ]
763
764 u = self.difference_update_query(["plugin", "plugins"])
765
766 for plugin in loaded_plugins:
767 new_u = plugin.update_url(u)
768 if new_u is not None:
769 u = new_u
770
771 kwargs.pop("plugins", None)
772
773 return u, loaded_plugins, kwargs
774
775 def _get_entrypoint(self) -> Type[Dialect]:
776 """Return the "entry point" dialect class.
777
778 This is normally the dialect itself except in the case when the
779 returned class implements the get_dialect_cls() method.
780
781 """
782 if "+" not in self.drivername:
783 name = self.drivername
784 else:
785 name = self.drivername.replace("+", ".")
786 cls = registry.load(name)
787 # check for legacy dialects that
788 # would return a module with 'dialect' as the
789 # actual class
790 if (
791 hasattr(cls, "dialect")
792 and isinstance(cls.dialect, type)
793 and issubclass(cls.dialect, Dialect)
794 ):
795 return cls.dialect
796 else:
797 return cast("Type[Dialect]", cls)
798
799 def get_dialect(self, _is_async: bool = False) -> Type[Dialect]:
800 """Return the SQLAlchemy :class:`_engine.Dialect` class corresponding
801 to this URL's driver name.
802
803 """
804 entrypoint = self._get_entrypoint()
805 if _is_async:
806 dialect_cls = entrypoint.get_async_dialect_cls(self)
807 else:
808 dialect_cls = entrypoint.get_dialect_cls(self)
809 return dialect_cls
810
811 def translate_connect_args(
812 self, names: Optional[List[str]] = None, **kw: Any
813 ) -> Dict[str, Any]:
814 r"""Translate url attributes into a dictionary of connection arguments.
815
816 Returns attributes of this url (`host`, `database`, `username`,
817 `password`, `port`) as a plain dictionary. The attribute names are
818 used as the keys by default. Unset or false attributes are omitted
819 from the final dictionary.
820
821 :param \**kw: Optional, alternate key names for url attributes.
822
823 :param names: Deprecated. Same purpose as the keyword-based alternate
824 names, but correlates the name to the original positionally.
825 """
826
827 if names is not None:
828 util.warn_deprecated(
829 "The `URL.translate_connect_args.name`s parameter is "
830 "deprecated. Please pass the "
831 "alternate names as kw arguments.",
832 "1.4",
833 )
834
835 translated = {}
836 attribute_names = ["host", "database", "username", "password", "port"]
837 for sname in attribute_names:
838 if names:
839 name = names.pop(0)
840 elif sname in kw:
841 name = kw[sname]
842 else:
843 name = sname
844 if name is not None and getattr(self, sname, False):
845 if sname == "password":
846 translated[name] = str(getattr(self, sname))
847 else:
848 translated[name] = getattr(self, sname)
849
850 return translated
851
852
853def make_url(
854 name_or_url: Union[str, URL], *, keep_blank_values: bool = False
855) -> URL:
856 """Given a string, produce a new URL instance.
857
858 The format of the URL generally follows `RFC-1738
859 <https://www.ietf.org/rfc/rfc1738.txt>`_, with some exceptions, including
860 that underscores, and not dashes or periods, are accepted within the
861 "scheme" portion.
862
863 If a :class:`.URL` object is passed, it is returned as is.
864
865 :param name_or_url: a string URL or :class:`.URL` object.
866
867 :param keep_blank_values: if True, query string parameters with blank
868 values such as ``key=`` are retained in the :attr:`.URL.query`
869 dictionary as empty strings. If left at its default of False,
870 parameters with blank values are omitted. Has no effect when a
871 :class:`.URL` object is passed.
872
873 As :func:`_sa.create_engine` parses string URLs using the default
874 setting, a string URL with blank values to be retained should be
875 parsed with :func:`.make_url` first, and the resulting :class:`.URL`
876 passed to :func:`_sa.create_engine`.
877
878 .. versionadded:: 2.1.6
879
880 .. seealso::
881
882 :ref:`database_urls`
883
884 """
885
886 if isinstance(name_or_url, str):
887 return _parse_url(name_or_url, keep_blank_values=keep_blank_values)
888 elif not isinstance(name_or_url, URL) and not hasattr(
889 name_or_url, "_sqla_is_testing_if_this_is_a_mock_object"
890 ):
891 raise exc.ArgumentError(
892 f"Expected string or URL object, got {name_or_url!r}"
893 )
894 else:
895 return name_or_url
896
897
898def _parse_url(name: str, keep_blank_values: bool = False) -> URL:
899 pattern = re.compile(
900 r"""
901 (?P<name>[\w\+]+)://
902 (?:
903 (?P<username>[^:/]*)
904 (?::(?P<password>[^@]*))?
905 @)?
906 (?:
907 (?:
908 \[(?P<ipv6host>[^/\?]+)\] |
909 (?P<ipv4host>[^/:\?]+)
910 )?
911 (?::(?P<port>[^/\?]*))?
912 )?
913 (?:/(?P<database>[^\?]*))?
914 (?:\?(?P<query>.*))?
915 """,
916 re.X,
917 )
918
919 m = pattern.match(name)
920 if m is not None:
921 components = m.groupdict()
922 query: Optional[Dict[str, Union[str, List[str]]]]
923 if components["query"] is not None:
924 query = {}
925
926 for key, value in parse_qsl(
927 components["query"], keep_blank_values=keep_blank_values
928 ):
929 if key in query:
930 query[key] = util.to_list(query[key])
931 cast("List[str]", query[key]).append(value)
932 else:
933 query[key] = value
934 else:
935 query = None
936 components["query"] = query
937
938 for comp in "username", "password", "database":
939 if components[comp] is not None:
940 components[comp] = unquote(components[comp])
941
942 ipv4host = components.pop("ipv4host")
943 ipv6host = components.pop("ipv6host")
944 components["host"] = ipv4host or ipv6host
945 name = components.pop("name")
946
947 if components["port"]:
948 components["port"] = int(components["port"])
949
950 return URL.create(name, **components) # type: ignore[arg-type]
951
952 else:
953 raise exc.ArgumentError(
954 "Could not parse SQLAlchemy URL from given URL string"
955 )