Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
conninfo.py379 linesDownload Raw Back to psycopg
1"""2Functions to manipulate conninfo strings3"""4 5# Copyright (C) 2020 The Psycopg Team6 7import os8import re9import socket10import asyncio11from typing import Any, Dict, List, Optional12from pathlib import Path13from datetime import tzinfo14from functools import lru_cache15from ipaddress import ip_address16 17from . import pq18from . import errors as e19from ._tz import get_tzinfo20from ._encodings import pgconn_encoding21 22 23def make_conninfo(conninfo: str = "", **kwargs: Any) -> str:24    """25    Merge a string and keyword params into a single conninfo string.26 27    :param conninfo: A `connection string`__ as accepted by PostgreSQL.28    :param kwargs: Parameters overriding the ones specified in `!conninfo`.29    :return: A connection string valid for PostgreSQL, with the `!kwargs`30        parameters merged.31 32    Raise `~psycopg.ProgrammingError` if the input doesn't make a valid33    conninfo string.34 35    .. __: https://www.postgresql.org/docs/current/libpq-connect.html36           #LIBPQ-CONNSTRING37    """38    if not conninfo and not kwargs:39        return ""40 41    # If no kwarg specified don't mung the conninfo but check if it's correct.42    # Make sure to return a string, not a subtype, to avoid making Liskov sad.43    if not kwargs:44        _parse_conninfo(conninfo)45        return str(conninfo)46 47    # Override the conninfo with the parameters48    # Drop the None arguments49    kwargs = {k: v for (k, v) in kwargs.items() if v is not None}50 51    if conninfo:52        tmp = conninfo_to_dict(conninfo)53        tmp.update(kwargs)54        kwargs = tmp55 56    conninfo = " ".join(f"{k}={_param_escape(str(v))}" for (k, v) in kwargs.items())57 58    # Verify the result is valid59    _parse_conninfo(conninfo)60 61    return conninfo62 63 64def conninfo_to_dict(conninfo: str = "", **kwargs: Any) -> Dict[str, Any]:65    """66    Convert the `!conninfo` string into a dictionary of parameters.67 68    :param conninfo: A `connection string`__ as accepted by PostgreSQL.69    :param kwargs: Parameters overriding the ones specified in `!conninfo`.70    :return: Dictionary with the parameters parsed from `!conninfo` and71        `!kwargs`.72 73    Raise `~psycopg.ProgrammingError` if `!conninfo` is not a a valid connection74    string.75 76    .. __: https://www.postgresql.org/docs/current/libpq-connect.html77           #LIBPQ-CONNSTRING78    """79    opts = _parse_conninfo(conninfo)80    rv = {opt.keyword.decode(): opt.val.decode() for opt in opts if opt.val is not None}81    for k, v in kwargs.items():82        if v is not None:83            rv[k] = v84    return rv85 86 87def _parse_conninfo(conninfo: str) -> List[pq.ConninfoOption]:88    """89    Verify that `!conninfo` is a valid connection string.90 91    Raise ProgrammingError if the string is not valid.92 93    Return the result of pq.Conninfo.parse() on success.94    """95    try:96        return pq.Conninfo.parse(conninfo.encode())97    except e.OperationalError as ex:98        raise e.ProgrammingError(str(ex))99 100 101re_escape = re.compile(r"([\\'])")102re_space = re.compile(r"\s")103 104 105def _param_escape(s: str) -> str:106    """107    Apply the escaping rule required by PQconnectdb108    """109    if not s:110        return "''"111 112    s = re_escape.sub(r"\\\1", s)113    if re_space.search(s):114        s = "'" + s + "'"115 116    return s117 118 119class ConnectionInfo:120    """Allow access to information about the connection."""121 122    __module__ = "psycopg"123 124    def __init__(self, pgconn: pq.abc.PGconn):125        self.pgconn = pgconn126 127    @property128    def vendor(self) -> str:129        """A string representing the database vendor connected to."""130        return "PostgreSQL"131 132    @property133    def host(self) -> str:134        """The server host name of the active connection. See :pq:`PQhost()`."""135        return self._get_pgconn_attr("host")136 137    @property138    def hostaddr(self) -> str:139        """The server IP address of the connection. See :pq:`PQhostaddr()`."""140        return self._get_pgconn_attr("hostaddr")141 142    @property143    def port(self) -> int:144        """The port of the active connection. See :pq:`PQport()`."""145        return int(self._get_pgconn_attr("port"))146 147    @property148    def dbname(self) -> str:149        """The database name of the connection. See :pq:`PQdb()`."""150        return self._get_pgconn_attr("db")151 152    @property153    def user(self) -> str:154        """The user name of the connection. See :pq:`PQuser()`."""155        return self._get_pgconn_attr("user")156 157    @property158    def password(self) -> str:159        """The password of the connection. See :pq:`PQpass()`."""160        return self._get_pgconn_attr("password")161 162    @property163    def options(self) -> str:164        """165        The command-line options passed in the connection request.166        See :pq:`PQoptions`.167        """168        return self._get_pgconn_attr("options")169 170    def get_parameters(self) -> Dict[str, str]:171        """Return the connection parameters values.172 173        Return all the parameters set to a non-default value, which might come174        either from the connection string and parameters passed to175        `~Connection.connect()` or from environment variables. The password176        is never returned (you can read it using the `password` attribute).177        """178        pyenc = self.encoding179 180        # Get the known defaults to avoid reporting them181        defaults = {182            i.keyword: i.compiled183            for i in pq.Conninfo.get_defaults()184            if i.compiled is not None185        }186        # Not returned by the libq. Bug? Bet we're using SSH.187        defaults.setdefault(b"channel_binding", b"prefer")188        defaults[b"passfile"] = str(Path.home() / ".pgpass").encode()189 190        return {191            i.keyword.decode(pyenc): i.val.decode(pyenc)192            for i in self.pgconn.info193            if i.val is not None194            and i.keyword != b"password"195            and i.val != defaults.get(i.keyword)196        }197 198    @property199    def dsn(self) -> str:200        """Return the connection string to connect to the database.201 202        The string contains all the parameters set to a non-default value,203        which might come either from the connection string and parameters204        passed to `~Connection.connect()` or from environment variables. The205        password is never returned (you can read it using the `password`206        attribute).207        """208        return make_conninfo(**self.get_parameters())209 210    @property211    def status(self) -> pq.ConnStatus:212        """The status of the connection. See :pq:`PQstatus()`."""213        return pq.ConnStatus(self.pgconn.status)214 215    @property216    def transaction_status(self) -> pq.TransactionStatus:217        """218        The current in-transaction status of the session.219        See :pq:`PQtransactionStatus()`.220        """221        return pq.TransactionStatus(self.pgconn.transaction_status)222 223    @property224    def pipeline_status(self) -> pq.PipelineStatus:225        """226        The current pipeline status of the client.227        See :pq:`PQpipelineStatus()`.228        """229        return pq.PipelineStatus(self.pgconn.pipeline_status)230 231    def parameter_status(self, param_name: str) -> Optional[str]:232        """233        Return a parameter setting of the connection.234 235        Return `None` is the parameter is unknown.236        """237        res = self.pgconn.parameter_status(param_name.encode(self.encoding))238        return res.decode(self.encoding) if res is not None else None239 240    @property241    def server_version(self) -> int:242        """243        An integer representing the server version. See :pq:`PQserverVersion()`.244        """245        return self.pgconn.server_version246 247    @property248    def backend_pid(self) -> int:249        """250        The process ID (PID) of the backend process handling this connection.251        See :pq:`PQbackendPID()`.252        """253        return self.pgconn.backend_pid254 255    @property256    def error_message(self) -> str:257        """258        The error message most recently generated by an operation on the connection.259        See :pq:`PQerrorMessage()`.260        """261        return self._get_pgconn_attr("error_message")262 263    @property264    def timezone(self) -> tzinfo:265        """The Python timezone info of the connection's timezone."""266        return get_tzinfo(self.pgconn)267 268    @property269    def encoding(self) -> str:270        """The Python codec name of the connection's client encoding."""271        return pgconn_encoding(self.pgconn)272 273    def _get_pgconn_attr(self, name: str) -> str:274        value: bytes = getattr(self.pgconn, name)275        return value.decode(self.encoding)276 277 278async def resolve_hostaddr_async(params: Dict[str, Any]) -> Dict[str, Any]:279    """280    Perform async DNS lookup of the hosts and return a new params dict.281 282    :param params: The input parameters, for instance as returned by283        `~psycopg.conninfo.conninfo_to_dict()`.284 285    If a ``host`` param is present but not ``hostname``, resolve the host286    addresses dynamically.287 288    The function may change the input ``host``, ``hostname``, ``port`` to allow289    connecting without further DNS lookups, eventually removing hosts that are290    not resolved, keeping the lists of hosts and ports consistent.291 292    Raise `~psycopg.OperationalError` if connection is not possible (e.g. no293    host resolve, inconsistent lists length).294    """295    hostaddr_arg = params.get("hostaddr", os.environ.get("PGHOSTADDR", ""))296    if hostaddr_arg:297        # Already resolved298        return params299 300    host_arg: str = params.get("host", os.environ.get("PGHOST", ""))301    if not host_arg:302        # Nothing to resolve303        return params304 305    hosts_in = host_arg.split(",")306    port_arg: str = str(params.get("port", os.environ.get("PGPORT", "")))307    ports_in = port_arg.split(",") if port_arg else []308    default_port = "5432"309 310    if len(ports_in) == 1:311        # If only one port is specified, the libpq will apply it to all312        # the hosts, so don't mangle it.313        default_port = ports_in.pop()314 315    elif len(ports_in) > 1:316        if len(ports_in) != len(hosts_in):317            # ProgrammingError would have been more appropriate, but this is318            # what the raise if the libpq fails connect in the same case.319            raise e.OperationalError(320                f"cannot match {len(hosts_in)} hosts with {len(ports_in)} port numbers"321            )322        ports_out = []323 324    hosts_out = []325    hostaddr_out = []326    loop = asyncio.get_running_loop()327    for i, host in enumerate(hosts_in):328        if not host or host.startswith("/") or host[1:2] == ":":329            # Local path330            hosts_out.append(host)331            hostaddr_out.append("")332            if ports_in:333                ports_out.append(ports_in[i])334            continue335 336        # If the host is already an ip address don't try to resolve it337        if is_ip_address(host):338            hosts_out.append(host)339            hostaddr_out.append(host)340            if ports_in:341                ports_out.append(ports_in[i])342            continue343 344        try:345            port = ports_in[i] if ports_in else default_port346            ans = await loop.getaddrinfo(347                host, port, proto=socket.IPPROTO_TCP, type=socket.SOCK_STREAM348            )349        except OSError as ex:350            last_exc = ex351        else:352            for item in ans:353                hosts_out.append(host)354                hostaddr_out.append(item[4][0])355                if ports_in:356                    ports_out.append(ports_in[i])357 358    # Throw an exception if no host could be resolved359    if not hosts_out:360        raise e.OperationalError(str(last_exc))361 362    out = params.copy()363    out["host"] = ",".join(hosts_out)364    out["hostaddr"] = ",".join(hostaddr_out)365    if ports_in:366        out["port"] = ",".join(ports_out)367 368    return out369 370 371@lru_cache()372def is_ip_address(s: str) -> bool:373    """Return True if the string represent a valid ip address."""374    try:375        ip_address(s)376    except ValueError:377        return False378    return True379 
codekingpro/portable-devtools · Team Ai