77from  __future__ import  annotations 
88
99import  asyncio 
10+ import  configparser 
1011import  collections 
1112from  collections .abc  import  Callable 
1213import  enum 
@@ -87,6 +88,9 @@ class SSLNegotiation(compat.StrEnum):
8788    PGPASSFILE  =  '.pgpass' 
8889
8990
91+ PG_SERVICEFILE  =  '.pg_service.conf' 
92+ 
93+ 
9094def  _read_password_file (passfile : pathlib .Path ) \
9195        ->  typing .List [typing .Tuple [str , ...]]:
9296
@@ -271,6 +275,7 @@ def _dot_postgresql_path(filename) -> typing.Optional[pathlib.Path]:
271275
272276def  _parse_connect_dsn_and_args (* , dsn , host , port , user ,
273277                                password , passfile , database , ssl ,
278+                                 service , servicefile ,
274279                                direct_tls , server_settings ,
275280                                target_session_attrs , krbsrvname , gsslib ):
276281    # `auth_hosts` is the version of host information for the purposes 
@@ -283,6 +288,32 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
283288    if  dsn :
284289        parsed  =  urllib .parse .urlparse (dsn )
285290
291+         query  =  None 
292+         if  parsed .query :
293+             query  =  urllib .parse .parse_qs (parsed .query , strict_parsing = True )
294+             for  key , val  in  query .items ():
295+                 if  isinstance (val , list ):
296+                     query [key ] =  val [- 1 ]
297+ 
298+             if  'service'  in  query :
299+                 val  =  query .pop ('service' )
300+                 if  not  service  and  val :
301+                     service  =  val 
302+ 
303+         connection_service_file  =  servicefile 
304+ 
305+         if  connection_service_file  is  None :
306+             connection_service_file  =  os .getenv ('PGSERVICEFILE' )
307+ 
308+         if  connection_service_file  is  None :
309+             homedir  =  compat .get_pg_home_directory ()
310+             if  homedir :
311+                 connection_service_file  =  homedir  /  PG_SERVICEFILE 
312+             else :
313+                 connection_service_file  =  None 
314+         else :
315+             connection_service_file  =  pathlib .Path (connection_service_file )
316+ 
286317        if  parsed .scheme  not  in   {'postgresql' , 'postgres' }:
287318            raise  exceptions .ClientConfigurationError (
288319                'invalid DSN: scheme is expected to be either ' 
@@ -317,11 +348,7 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
317348        if  password  is  None  and  dsn_password :
318349            password  =  urllib .parse .unquote (dsn_password )
319350
320-         if  parsed .query :
321-             query  =  urllib .parse .parse_qs (parsed .query , strict_parsing = True )
322-             for  key , val  in  query .items ():
323-                 if  isinstance (val , list ):
324-                     query [key ] =  val [- 1 ]
351+         if  query :
325352
326353            if  'port'  in  query :
327354                val  =  query .pop ('port' )
@@ -408,12 +435,124 @@ def _parse_connect_dsn_and_args(*, dsn, host, port, user,
408435                if  gsslib  is  None :
409436                    gsslib  =  val 
410437
438+             if  'service'  in  query :
439+                 val  =  query .pop ('service' )
440+                 if  service  is  None :
441+                     service  =  val 
442+ 
411443            if  query :
412444                if  server_settings  is  None :
413445                    server_settings  =  query 
414446                else :
415447                    server_settings  =  {** query , ** server_settings }
416448
449+         if  connection_service_file  is  not   None  and  service  is  not   None :
450+             pg_service  =  configparser .ConfigParser ()
451+             pg_service .read (connection_service_file )
452+             if  service  in  pg_service .sections ():
453+                 service_params  =  pg_service [service ]
454+                 if  'port'  in  service_params :
455+                     val  =  service_params .pop ('port' )
456+                     if  not  port  and  val :
457+                         port  =  [int (p ) for  p  in  val .split (',' )]
458+ 
459+                 if  'host'  in  service_params :
460+                     val  =  service_params .pop ('host' )
461+                     if  not  host  and  val :
462+                         host , port  =  _parse_hostlist (val , port )
463+ 
464+                 if  'dbname'  in  service_params :
465+                     val  =  service_params .pop ('dbname' )
466+                     if  database  is  None :
467+                         database  =  val 
468+ 
469+                 if  'database'  in  service_params :
470+                     val  =  service_params .pop ('database' )
471+                     if  database  is  None :
472+                         database  =  val 
473+ 
474+                 if  'user'  in  service_params :
475+                     val  =  service_params .pop ('user' )
476+                     if  user  is  None :
477+                         user  =  val 
478+ 
479+                 if  'password'  in  service_params :
480+                     val  =  service_params .pop ('password' )
481+                     if  password  is  None :
482+                         password  =  val 
483+ 
484+                 if  'passfile'  in  service_params :
485+                     val  =  service_params .pop ('passfile' )
486+                     if  passfile  is  None :
487+                         passfile  =  val 
488+ 
489+                 if  'sslmode'  in  service_params :
490+                     val  =  service_params .pop ('sslmode' )
491+                     if  ssl  is  None :
492+                         ssl  =  val 
493+ 
494+                 if  'sslcert'  in  service_params :
495+                     val  =  service_params .pop ('sslcert' )
496+                     if  sslcert  is  None :
497+                         sslcert  =  val 
498+ 
499+                 if  'sslkey'  in  service_params :
500+                     val  =  service_params .pop ('sslkey' )
501+                     if  sslkey  is  None :
502+                         sslkey  =  val 
503+ 
504+                 if  'sslrootcert'  in  service_params :
505+                     val  =  service_params .pop ('sslrootcert' )
506+                     if  sslrootcert  is  None :
507+                         sslrootcert  =  val 
508+ 
509+                 if  'sslnegotiation'  in  service_params :
510+                     val  =  service_params .pop ('sslnegotiation' )
511+                     if  sslnegotiation  is  None :
512+                         sslnegotiation  =  val 
513+ 
514+                 if  'sslcrl'  in  service_params :
515+                     val  =  service_params .pop ('sslcrl' )
516+                     if  sslcrl  is  None :
517+                         sslcrl  =  val 
518+ 
519+                 if  'sslpassword'  in  service_params :
520+                     val  =  service_params .pop ('sslpassword' )
521+                     if  sslpassword  is  None :
522+                         sslpassword  =  val 
523+ 
524+                 if  'ssl_min_protocol_version'  in  service_params :
525+                     val  =  service_params .pop (
526+                         'ssl_min_protocol_version' 
527+                     )
528+                     if  ssl_min_protocol_version  is  None :
529+                         ssl_min_protocol_version  =  val 
530+ 
531+                 if  'ssl_max_protocol_version'  in  service_params :
532+                     val  =  service_params .pop (
533+                         'ssl_max_protocol_version' 
534+                     )
535+                     if  ssl_max_protocol_version  is  None :
536+                         ssl_max_protocol_version  =  val 
537+ 
538+                 if  'target_session_attrs'  in  service_params :
539+                     dsn_target_session_attrs  =  service_params .pop (
540+                         'target_session_attrs' 
541+                     )
542+                     if  target_session_attrs  is  None :
543+                         target_session_attrs  =  dsn_target_session_attrs 
544+ 
545+                 if  'krbsrvname'  in  service_params :
546+                     val  =  service_params .pop ('krbsrvname' )
547+                     if  krbsrvname  is  None :
548+                         krbsrvname  =  val 
549+ 
550+                 if  'gsslib'  in  service_params :
551+                     val  =  service_params .pop ('gsslib' )
552+                     if  gsslib  is  None :
553+                         gsslib  =  val 
554+     if  not  service :
555+         service  =  os .environ .get ('PGSERVICE' )
417556    if  not  host :
418557        hostspec  =  os .environ .get ('PGHOST' )
419558        if  hostspec :
@@ -726,7 +865,8 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile,
726865                             max_cached_statement_lifetime ,
727866                             max_cacheable_statement_size ,
728867                             ssl , direct_tls , server_settings ,
729-                              target_session_attrs , krbsrvname , gsslib ):
868+                              target_session_attrs , krbsrvname , gsslib ,
869+                              service , servicefile ):
730870    local_vars  =  locals ()
731871    for  var_name  in  {'max_cacheable_statement_size' ,
732872                     'max_cached_statement_lifetime' ,
@@ -756,7 +896,8 @@ def _parse_connect_arguments(*, dsn, host, port, user, password, passfile,
756896        direct_tls = direct_tls , database = database ,
757897        server_settings = server_settings ,
758898        target_session_attrs = target_session_attrs ,
759-         krbsrvname = krbsrvname , gsslib = gsslib )
899+         krbsrvname = krbsrvname , gsslib = gsslib ,
900+         service = service , servicefile = servicefile )
760901
761902    config  =  _ClientConfiguration (
762903        command_timeout = command_timeout ,
0 commit comments