Skip to content

Commit df29332

Browse files
committed
refactor(postgres-cli): decouple CLI output from core logic using OutputProvider
Refactor the `__main__.py` CLI tool to separate output presentation from core query execution logic. Introduces an `OutputProvider` strategy pattern (`PlainTextOutputProvider` and `RichOutputProvider`) to encapsulate output formatting. This removes the pervasive `if-else` checks for `rich` availability, making the code cleaner and more maintainable. Adds a `--plain` argument to force plain text output even when `rich` is installed, providing explicit control over the output format.
1 parent 49c2932 commit df29332

2 files changed

Lines changed: 230 additions & 146 deletions

File tree

Lines changed: 51 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
# src/rhosocial/activerecord/backend/impl/postgres/__main__.py
22
import argparse
33
import asyncio
4-
import datetime
5-
import decimal
64
import logging
7-
import json
85
import os
96
import sys
107

8+
from .backend import PostgresBackend, AsyncPostgresBackend
9+
from .config import PostgresConnectionConfig
10+
from rhosocial.activerecord.backend.errors import ConnectionError, QueryError
11+
from .output import PlainTextOutputProvider
12+
13+
# Attempt to import rich and the RichOutputProvider
1114
try:
1215
from rich.console import Console
13-
from rich.table import Table
14-
from rich.panel import Panel
1516
from rich.logging import RichHandler
16-
from rich import box
17+
from .output import RichOutputProvider
1718
RICH_AVAILABLE = True
1819
except ImportError:
1920
RICH_AVAILABLE = False
2021

21-
from .backend import PostgresBackend, AsyncPostgresBackend
22-
from .config import PostgresConnectionConfig
23-
from rhosocial.activerecord.backend.errors import ConnectionError, QueryError
24-
25-
2622
logger = logging.getLogger(__name__)
2723

2824

@@ -32,205 +28,114 @@ def parse_args():
3228
formatter_class=argparse.RawTextHelpFormatter
3329
)
3430

35-
# Connection parameters with defaults from environment variables
36-
parser.add_argument(
37-
'--host',
38-
default=os.getenv('POSTGRES_HOST', 'localhost'),
39-
help='Database host (default: POSTGRES_HOST environment variable or localhost)'
40-
)
41-
parser.add_argument(
42-
'--port',
43-
type=int,
44-
default=int(os.getenv('POSTGRES_PORT', 5432)),
45-
help='Database port (default: POSTGRES_PORT environment variable or 5432)'
46-
)
47-
parser.add_argument(
48-
'--database',
49-
default=os.getenv('POSTGRES_DATABASE'),
50-
help='Database name (optional, default: POSTGRES_DATABASE environment variable)'
51-
)
52-
parser.add_argument(
53-
'--user',
54-
default=os.getenv('POSTGRES_USER', 'postgres'),
55-
help='Database user (default: POSTGRES_USER environment variable or postgres)'
56-
)
57-
parser.add_argument(
58-
'--password',
59-
default=os.getenv('POSTGRES_PASSWORD', ''),
60-
help='Database password (default: POSTGRES_PASSWORD environment variable or empty string)'
61-
)
62-
# Positional argument for the SQL query
63-
parser.add_argument(
64-
'query', # Changed to positional argument
65-
help='SQL query to execute. Must be enclosed in quotes.'
66-
)
31+
# Connection parameters
32+
parser.add_argument('--host', default=os.getenv('POSTGRES_HOST', 'localhost'), help='Database host')
33+
parser.add_argument('--port', type=int, default=int(os.getenv('POSTGRES_PORT', 5432)), help='Database port')
34+
parser.add_argument('--database', default=os.getenv('POSTGRES_DATABASE'), help='Database name')
35+
parser.add_argument('--user', default=os.getenv('POSTGRES_USER', 'postgres'), help='Database user')
36+
parser.add_argument('--password', default=os.getenv('POSTGRES_PASSWORD', ''), help='Database password')
6737

38+
# Execution options
39+
parser.add_argument('query', help='SQL query to execute. Must be enclosed in quotes.')
6840
parser.add_argument('--use-async', action='store_true', help='Use asynchronous backend')
69-
parser.add_argument('--log-level', default='INFO', help='Set logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)')
41+
42+
# Output and logging options
43+
parser.add_argument('--log-level', default='INFO', help='Set logging level (e.g., DEBUG, INFO)')
44+
parser.add_argument('--plain', action='store_true', help='Use plain text output even if rich is installed.')
7045
parser.add_argument('--rich-ascii', action='store_true', help='Use ASCII characters for rich table borders.')
7146

7247
return parser.parse_args()
7348

74-
def execute_query_sync(args, backend):
49+
def execute_query_sync(args, backend, provider):
7550
try:
7651
backend.connect()
77-
if RICH_AVAILABLE:
78-
Console().print(f"Executing synchronous query: [bold cyan]{args.query}[/bold cyan]")
79-
else:
80-
logger.info(f"Executing synchronous query: {args.query}")
52+
provider.display_query(args.query, is_async=False)
8153
result = backend.execute(args.query)
82-
handle_result(args, result)
54+
handle_result(result, provider, use_ascii=args.rich_ascii)
8355
except ConnectionError as e:
84-
if RICH_AVAILABLE:
85-
Console().print(Panel(f"[bold]Database Connection Error[/bold]\n[red]{e}[/red]", title="[bold red]Error[/bold red]", border_style="red"))
86-
else:
87-
logger.error(f"Database connection error: {e}")
56+
provider.display_connection_error(e)
8857
sys.exit(1)
8958
except QueryError as e:
90-
if RICH_AVAILABLE:
91-
Console().print(Panel(f"[bold]Database Query Error[/bold]\n[red]{e}[/red]", title="[bold red]Error[/bold red]", border_style="red"))
92-
else:
93-
logger.error(f"Database query error: {e}")
59+
provider.display_query_error(e)
9460
sys.exit(1)
9561
except Exception as e:
96-
if RICH_AVAILABLE:
97-
Console().print(Panel(f"[bold]An unexpected error occurred during synchronous execution[/bold]\n[red]{e}[/red]", title="[bold red]Error[/bold red]", border_style="red"))
98-
else:
99-
logger.error(f"An unexpected error occurred during synchronous execution: {e}", exc_info=True)
62+
provider.display_unexpected_error(e, is_async=False)
10063
sys.exit(1)
10164
finally:
10265
if backend._connection:
10366
backend.disconnect()
104-
if RICH_AVAILABLE:
105-
Console().print("[dim]Disconnected from database (synchronous).[/dim]")
106-
else:
107-
logger.info("Disconnected from database (synchronous).")
67+
provider.display_disconnect(is_async=False)
10868

109-
async def execute_query_async(args, backend):
69+
async def execute_query_async(args, backend, provider):
11070
try:
11171
await backend.connect()
112-
if RICH_AVAILABLE:
113-
Console().print(f"Executing asynchronous query: [bold cyan]{args.query}[/bold cyan]")
114-
else:
115-
logger.info(f"Executing asynchronous query: {args.query}")
72+
provider.display_query(args.query, is_async=True)
11673
result = await backend.execute(args.query)
117-
handle_result(args, result)
74+
handle_result(result, provider, use_ascii=args.rich_ascii)
11875
except ConnectionError as e:
119-
if RICH_AVAILABLE:
120-
Console().print(Panel(f"[bold]Database Connection Error[/bold]\n[red]{e}[/red]", title="[bold red]Error[/bold red]", border_style="red"))
121-
else:
122-
logger.error(f"Database connection error: {e}")
76+
provider.display_connection_error(e)
12377
sys.exit(1)
12478
except QueryError as e:
125-
if RICH_AVAILABLE:
126-
Console().print(Panel(f"[bold]Database Query Error[/bold]\n[red]{e}[/red]", title="[bold red]Error[/bold red]", border_style="red"))
127-
else:
128-
logger.error(f"Database query error: {e}")
79+
provider.display_query_error(e)
12980
sys.exit(1)
13081
except Exception as e:
131-
if RICH_AVAILABLE:
132-
Console().print(Panel(f"[bold]An unexpected error occurred during asynchronous execution[/bold]\n[red]{e}[/red]", title="[bold red]Error[/bold red]", border_style="red"))
133-
else:
134-
logger.error(f"An unexpected error occurred during asynchronous execution: {e}", exc_info=True)
82+
provider.display_unexpected_error(e, is_async=True)
13583
sys.exit(1)
13684
finally:
13785
if backend._connection:
13886
await backend.disconnect()
139-
if RICH_AVAILABLE:
140-
Console().print("[dim]Disconnected from database (asynchronous).[/dim]")
141-
else:
142-
logger.info("Disconnected from database (asynchronous).")
143-
144-
def json_serializer(obj):
145-
"""Handles serialization of types not supported by default JSON encoder."""
146-
if isinstance(obj, (datetime.datetime, datetime.date)):
147-
return obj.isoformat()
148-
if isinstance(obj, datetime.timedelta):
149-
return str(obj)
150-
if isinstance(obj, decimal.Decimal):
151-
return str(obj)
152-
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
153-
154-
155-
def handle_result(args, result):
87+
provider.display_disconnect(is_async=True)
88+
89+
90+
def handle_result(result, provider, **kwargs):
15691
if not result:
157-
if RICH_AVAILABLE:
158-
Console().print("[yellow]Query executed, but no result object returned.[/yellow]")
159-
else:
160-
logger.info("Query executed, but no result object returned.")
92+
provider.display_no_result_object()
16193
return
16294

163-
if RICH_AVAILABLE:
164-
console = Console()
165-
console.print(f"[bold green]Query executed successfully.[/bold green] "
166-
f"Affected rows: [bold cyan]{result.affected_rows}[/bold cyan], "
167-
f"Duration: [bold cyan]{result.duration:.4f}s[/bold cyan]")
168-
if result.data:
169-
# Ensure data is a list of dicts
170-
if not isinstance(result.data, list) or not result.data or not all(isinstance(i, dict) for i in result.data):
171-
console.print(result.data)
172-
return
173-
174-
box_style = box.ASCII if args.rich_ascii else box.SQUARE
175-
table = Table(show_header=True, header_style="bold magenta", box=box_style)
176-
headers = result.data[0].keys()
177-
for header in headers:
178-
table.add_column(header, style="dim", overflow="fold")
179-
180-
for row in result.data:
181-
table.add_row(*(str(v) for v in row.values()))
182-
183-
console.print(table)
184-
else:
185-
console.print("[yellow]No data returned.[/yellow]")
95+
provider.display_success(result.affected_rows, result.duration)
96+
if result.data:
97+
provider.display_results(result.data, **kwargs)
18698
else:
187-
logger.info(f"Query executed successfully. Affected rows: {result.affected_rows}, Duration: {result.duration:.4f}s")
188-
if result.data:
189-
logger.info("Results:")
190-
for row in result.data:
191-
if isinstance(row, (dict, list)):
192-
print(json.dumps(row, indent=2, ensure_ascii=False, default=json_serializer))
193-
else:
194-
print(row)
195-
else:
196-
logger.info("No data returned.")
99+
provider.display_no_data()
197100

198101
def main():
199102
args = parse_args()
200103

201-
# Set logging level
104+
# Setup logging
202105
numeric_level = getattr(logging, args.log_level.upper(), None)
203106
if not isinstance(numeric_level, int):
204107
raise ValueError(f'Invalid log level: {args.log_level}')
205108

206-
if RICH_AVAILABLE:
109+
# Choose output provider
110+
if RICH_AVAILABLE and not args.plain:
111+
provider = RichOutputProvider(console=Console(), ascii_borders=args.rich_ascii)
207112
logging.basicConfig(
208113
level=numeric_level,
209114
format="%(message)s",
210115
datefmt="[%X]",
211116
handlers=[RichHandler(rich_tracebacks=True, show_path=False)]
212117
)
213-
Console().print("[bold green]Rich library detected. Logging and output are beautified.[/bold green]")
214118
else:
119+
provider = PlainTextOutputProvider()
215120
logging.basicConfig(level=numeric_level, format='%(asctime)s - %(levelname)s - %(message)s')
216-
print("Rich library not found. We recommend installing rich for a more user-friendly output.")
121+
122+
provider.display_greeting()
217123

124+
# Setup backend
218125
config = PostgresConnectionConfig(
219126
host=args.host,
220127
port=args.port,
221128
database=args.database,
222129
username=args.user,
223130
password=args.password,
224-
225-
226131
)
227132

228133
if args.use_async:
229134
backend = AsyncPostgresBackend(connection_config=config)
230-
asyncio.run(execute_query_async(args, backend))
135+
asyncio.run(execute_query_async(args, backend, provider))
231136
else:
232137
backend = PostgresBackend(connection_config=config)
233-
execute_query_sync(args, backend)
138+
execute_query_sync(args, backend, provider)
234139

235140
if __name__ == "__main__":
236141
main()

0 commit comments

Comments
 (0)