d365fo-client 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. d365fo_client/__init__.py +305 -0
  2. d365fo_client/auth.py +93 -0
  3. d365fo_client/cli.py +700 -0
  4. d365fo_client/client.py +1454 -0
  5. d365fo_client/config.py +304 -0
  6. d365fo_client/crud.py +200 -0
  7. d365fo_client/exceptions.py +49 -0
  8. d365fo_client/labels.py +528 -0
  9. d365fo_client/main.py +502 -0
  10. d365fo_client/mcp/__init__.py +16 -0
  11. d365fo_client/mcp/client_manager.py +276 -0
  12. d365fo_client/mcp/main.py +98 -0
  13. d365fo_client/mcp/models.py +371 -0
  14. d365fo_client/mcp/prompts/__init__.py +43 -0
  15. d365fo_client/mcp/prompts/action_execution.py +480 -0
  16. d365fo_client/mcp/prompts/sequence_analysis.py +349 -0
  17. d365fo_client/mcp/resources/__init__.py +15 -0
  18. d365fo_client/mcp/resources/database_handler.py +555 -0
  19. d365fo_client/mcp/resources/entity_handler.py +176 -0
  20. d365fo_client/mcp/resources/environment_handler.py +132 -0
  21. d365fo_client/mcp/resources/metadata_handler.py +283 -0
  22. d365fo_client/mcp/resources/query_handler.py +135 -0
  23. d365fo_client/mcp/server.py +432 -0
  24. d365fo_client/mcp/tools/__init__.py +17 -0
  25. d365fo_client/mcp/tools/connection_tools.py +175 -0
  26. d365fo_client/mcp/tools/crud_tools.py +579 -0
  27. d365fo_client/mcp/tools/database_tools.py +813 -0
  28. d365fo_client/mcp/tools/label_tools.py +189 -0
  29. d365fo_client/mcp/tools/metadata_tools.py +766 -0
  30. d365fo_client/mcp/tools/profile_tools.py +706 -0
  31. d365fo_client/metadata_api.py +793 -0
  32. d365fo_client/metadata_v2/__init__.py +59 -0
  33. d365fo_client/metadata_v2/cache_v2.py +1372 -0
  34. d365fo_client/metadata_v2/database_v2.py +585 -0
  35. d365fo_client/metadata_v2/global_version_manager.py +573 -0
  36. d365fo_client/metadata_v2/search_engine_v2.py +423 -0
  37. d365fo_client/metadata_v2/sync_manager_v2.py +819 -0
  38. d365fo_client/metadata_v2/version_detector.py +439 -0
  39. d365fo_client/models.py +862 -0
  40. d365fo_client/output.py +181 -0
  41. d365fo_client/profile_manager.py +342 -0
  42. d365fo_client/profiles.py +178 -0
  43. d365fo_client/query.py +162 -0
  44. d365fo_client/session.py +60 -0
  45. d365fo_client/utils.py +196 -0
  46. d365fo_client-0.1.0.dist-info/METADATA +1084 -0
  47. d365fo_client-0.1.0.dist-info/RECORD +51 -0
  48. d365fo_client-0.1.0.dist-info/WHEEL +5 -0
  49. d365fo_client-0.1.0.dist-info/entry_points.txt +3 -0
  50. d365fo_client-0.1.0.dist-info/licenses/LICENSE +21 -0
  51. d365fo_client-0.1.0.dist-info/top_level.txt +1 -0
d365fo_client/cli.py ADDED
@@ -0,0 +1,700 @@
1
+ """CLI manager for d365fo-client commands."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+ import sys
7
+ from typing import Any, Dict, Optional
8
+
9
+ from .client import FOClient
10
+ from .config import ConfigManager
11
+ from .exceptions import FOClientError
12
+ from .models import FOClientConfig, QueryOptions
13
+ from .output import OutputFormatter, format_error_message, format_success_message
14
+ from .profiles import Profile
15
+
16
+
17
+ class CLIManager:
18
+ """Main CLI command manager."""
19
+
20
+ def __init__(self):
21
+ """Initialize CLI manager."""
22
+ self.config_manager = ConfigManager()
23
+ self.output_formatter = None
24
+ self.client = None
25
+
26
+ async def execute_command(self, args: argparse.Namespace) -> int:
27
+ """Execute the specified command.
28
+
29
+ Args:
30
+ args: Parsed command line arguments
31
+
32
+ Returns:
33
+ Exit code (0 for success, 1 for error)
34
+ """
35
+ try:
36
+ # Setup output formatter
37
+ output_format = getattr(args, "output", "table")
38
+ self.output_formatter = OutputFormatter(output_format)
39
+
40
+ # Handle legacy demo mode first
41
+ if getattr(args, "demo", False):
42
+ return await self._run_demo()
43
+
44
+ # Handle configuration commands (no client needed)
45
+ if getattr(args, "command", None) == "config":
46
+ return await self._handle_config_commands(args)
47
+
48
+ # For other commands, we need a base URL
49
+ # Get effective configuration
50
+ config = self.config_manager.get_effective_config(args)
51
+
52
+ # Validate required configuration
53
+ if not config.base_url:
54
+ print(
55
+ format_error_message(
56
+ "Base URL is required. Use --base-url or configure a profile."
57
+ )
58
+ )
59
+ return 1
60
+
61
+ # Create and initialize client
62
+ async with FOClient(config) as client:
63
+ self.client = client
64
+
65
+ # Route to appropriate command handler
66
+ return await self._route_command(args)
67
+
68
+ except KeyboardInterrupt:
69
+ print("\n\nOperation cancelled by user")
70
+ return 130
71
+ except Exception as e:
72
+ self._handle_error(e, getattr(args, "verbose", False))
73
+ return 1
74
+
75
+ async def _run_demo(self) -> int:
76
+ """Run the legacy demo mode."""
77
+ from .main import example_usage
78
+
79
+ print("Running demo mode...")
80
+ try:
81
+ await example_usage()
82
+ return 0
83
+ except Exception as e:
84
+ print(f"Demo error: {e}")
85
+ return 1
86
+
87
+ async def _route_command(self, args: argparse.Namespace) -> int:
88
+ """Route command to appropriate handler.
89
+
90
+ Args:
91
+ args: Parsed command line arguments
92
+
93
+ Returns:
94
+ Exit code
95
+ """
96
+ command_handlers = {
97
+ "test": self._handle_test_command,
98
+ "version": self._handle_version_command,
99
+ "metadata": self._handle_metadata_commands,
100
+ "entity": self._handle_entity_commands,
101
+ "action": self._handle_action_commands,
102
+ }
103
+
104
+ command = getattr(args, "command", None)
105
+ handler = command_handlers.get(command)
106
+
107
+ if handler:
108
+ return await handler(args)
109
+ else:
110
+ print(format_error_message(f"Unknown command: {command}"))
111
+ return 1
112
+
113
+ async def _handle_test_command(self, args: argparse.Namespace) -> int:
114
+ """Handle test connectivity command."""
115
+ success = True
116
+
117
+ # Test OData connection
118
+ if not getattr(args, "metadata_only", False):
119
+ try:
120
+ if await self.client.test_connection():
121
+ print(format_success_message("OData API connection successful"))
122
+ else:
123
+ print(format_error_message("OData API connection failed"))
124
+ success = False
125
+ except Exception as e:
126
+ print(format_error_message(f"OData API connection error: {e}"))
127
+ success = False
128
+
129
+ # Test metadata connection
130
+ if not getattr(args, "odata_only", False):
131
+ try:
132
+ if await self.client.test_metadata_connection():
133
+ print(format_success_message("Metadata API connection successful"))
134
+ else:
135
+ print(format_error_message("Metadata API connection failed"))
136
+ success = False
137
+ except Exception as e:
138
+ print(format_error_message(f"Metadata API connection error: {e}"))
139
+ success = False
140
+
141
+ return 0 if success else 1
142
+
143
+ async def _handle_version_command(self, args: argparse.Namespace) -> int:
144
+ """Handle version information command."""
145
+ try:
146
+ version_info = {}
147
+
148
+ # Get different version types based on args
149
+ if getattr(args, "application", False) or getattr(args, "all", False):
150
+ version_info["application"] = (
151
+ await self.client.get_application_version()
152
+ )
153
+
154
+ if getattr(args, "platform", False) or getattr(args, "all", False):
155
+ version_info["platform_build"] = (
156
+ await self.client.get_platform_build_version()
157
+ )
158
+
159
+ if getattr(args, "build", False) or getattr(args, "all", False):
160
+ version_info["application_build"] = (
161
+ await self.client.get_application_build_version()
162
+ )
163
+
164
+ # If no specific version requested, get application version
165
+ if not version_info:
166
+ version_info["application"] = (
167
+ await self.client.get_application_version()
168
+ )
169
+
170
+ output = self.output_formatter.format_output(version_info)
171
+ print(output)
172
+ return 0
173
+
174
+ except Exception as e:
175
+ print(format_error_message(f"Error retrieving version information: {e}"))
176
+ return 1
177
+
178
+ async def _handle_metadata_commands(self, args: argparse.Namespace) -> int:
179
+ """Handle metadata operations."""
180
+ subcommand = getattr(args, "metadata_subcommand", None)
181
+
182
+ if subcommand == "sync":
183
+ return await self._handle_metadata_sync(args)
184
+ elif subcommand == "search":
185
+ return await self._handle_metadata_search(args)
186
+ elif subcommand == "info":
187
+ return await self._handle_metadata_info(args)
188
+ else:
189
+ print(format_error_message(f"Unknown metadata subcommand: {subcommand}"))
190
+ return 1
191
+
192
+ async def _handle_metadata_sync(self, args: argparse.Namespace) -> int:
193
+ """Handle metadata sync command."""
194
+ try:
195
+ force_refresh = getattr(args, "force", False)
196
+ success = await self.client.download_metadata(force_refresh=force_refresh)
197
+
198
+ if success:
199
+ print(format_success_message("Metadata synchronized successfully"))
200
+ return 0
201
+ else:
202
+ print(format_error_message("Metadata synchronization failed"))
203
+ return 1
204
+ except Exception as e:
205
+ print(format_error_message(f"Error syncing metadata: {e}"))
206
+ return 1
207
+
208
+ async def _handle_metadata_search(self, args: argparse.Namespace) -> int:
209
+ """Handle metadata search command."""
210
+ try:
211
+ pattern = getattr(args, "pattern", "")
212
+ search_type = getattr(args, "type", "entities")
213
+ limit = getattr(args, "limit", None)
214
+
215
+ results = []
216
+
217
+ if search_type in ["entities", "all"]:
218
+ entities = self.client.search_entities(pattern)
219
+ if limit:
220
+ entities = entities[:limit]
221
+ results.extend([{"type": "entity", "name": name} for name in entities])
222
+
223
+ if search_type in ["actions", "all"]:
224
+ actions = self.client.search_actions(pattern)
225
+ if limit:
226
+ actions = actions[:limit]
227
+ results.extend([{"type": "action", "name": name} for name in actions])
228
+
229
+ if not results:
230
+ print(f"No {search_type} found matching pattern: {pattern}")
231
+ return 0
232
+
233
+ # If table format and mixed types, show type column
234
+ if self.output_formatter.format_type == "table" and search_type == "all":
235
+ output = self.output_formatter.format_output(results, ["type", "name"])
236
+ else:
237
+ # For specific types or other formats, just show names
238
+ names = [r["name"] for r in results]
239
+ output = self.output_formatter.format_output(names)
240
+
241
+ print(output)
242
+ return 0
243
+
244
+ except Exception as e:
245
+ print(format_error_message(f"Error searching metadata: {e}"))
246
+ return 1
247
+
248
+ async def _handle_metadata_info(self, args: argparse.Namespace) -> int:
249
+ """Handle metadata info command."""
250
+ try:
251
+ entity_name = getattr(args, "entity_name", "")
252
+
253
+ entity_info = await self.client.get_entity_info_with_labels(entity_name)
254
+ if not entity_info:
255
+ print(format_error_message(f"Entity not found: {entity_name}"))
256
+ return 1
257
+
258
+ # Build info dictionary
259
+ info = {
260
+ "name": entity_info.name,
261
+ "label": entity_info.label_text or entity_info.label_id,
262
+ "properties_count": len(entity_info.enhanced_properties),
263
+ }
264
+
265
+ # Add properties if requested
266
+ if getattr(args, "properties", False):
267
+ properties = []
268
+ for prop in entity_info.enhanced_properties:
269
+ prop_info = {
270
+ "name": prop.name,
271
+ "type": prop.type,
272
+ "label": prop.label_text or prop.label_id,
273
+ }
274
+ if getattr(args, "keys", False):
275
+ prop_info["is_key"] = prop.is_key
276
+ properties.append(prop_info)
277
+ info["properties"] = properties
278
+
279
+ output = self.output_formatter.format_output(info)
280
+ print(output)
281
+ return 0
282
+
283
+ except Exception as e:
284
+ print(format_error_message(f"Error getting entity info: {e}"))
285
+ return 1
286
+
287
+ async def _handle_entity_commands(self, args: argparse.Namespace) -> int:
288
+ """Handle entity operations."""
289
+ subcommand = getattr(args, "entity_subcommand", None)
290
+
291
+ if subcommand == "get":
292
+ return await self._handle_entity_get(args)
293
+ elif subcommand == "create":
294
+ return await self._handle_entity_create(args)
295
+ elif subcommand == "update":
296
+ return await self._handle_entity_update(args)
297
+ elif subcommand == "delete":
298
+ return await self._handle_entity_delete(args)
299
+ else:
300
+ print(format_error_message(f"Unknown entity subcommand: {subcommand}"))
301
+ return 1
302
+
303
+ async def _handle_entity_get(self, args: argparse.Namespace) -> int:
304
+ """Handle entity get command."""
305
+ try:
306
+ entity_name = getattr(args, "entity_name", "")
307
+ key = getattr(args, "key", None)
308
+
309
+ # Build query options
310
+ query_options = None
311
+ if any(
312
+ [
313
+ getattr(args, "select", None),
314
+ getattr(args, "filter", None),
315
+ getattr(args, "top", None),
316
+ getattr(args, "orderby", None),
317
+ ]
318
+ ):
319
+ query_options = QueryOptions(
320
+ select=(
321
+ getattr(args, "select", "").split(",")
322
+ if getattr(args, "select", "")
323
+ else None
324
+ ),
325
+ filter=getattr(args, "filter", None),
326
+ top=getattr(args, "top", None),
327
+ orderby=(
328
+ getattr(args, "orderby", "").split(",")
329
+ if getattr(args, "orderby", "")
330
+ else None
331
+ ),
332
+ )
333
+
334
+ # Execute query
335
+ if key:
336
+ result = await self.client.get_entity(entity_name, key, query_options)
337
+ else:
338
+ result = await self.client.get_entities(entity_name, query_options)
339
+
340
+ # Format and output
341
+ if isinstance(result, dict) and "value" in result:
342
+ # OData response format
343
+ output = self.output_formatter.format_output(result["value"])
344
+ else:
345
+ output = self.output_formatter.format_output(result)
346
+
347
+ print(output)
348
+ return 0
349
+
350
+ except Exception as e:
351
+ print(format_error_message(f"Error getting entity data: {e}"))
352
+ return 1
353
+
354
+ async def _handle_entity_create(self, args: argparse.Namespace) -> int:
355
+ """Handle entity create command."""
356
+ try:
357
+ entity_name = getattr(args, "entity_name", "")
358
+
359
+ # Get data from args
360
+ data_json = getattr(args, "data", None)
361
+ data_file = getattr(args, "file", None)
362
+
363
+ if data_json:
364
+ data = json.loads(data_json)
365
+ elif data_file:
366
+ with open(data_file, "r") as f:
367
+ data = json.load(f)
368
+ else:
369
+ print(format_error_message("Either --data or --file must be provided"))
370
+ return 1
371
+
372
+ result = await self.client.create_entity(entity_name, data)
373
+
374
+ print(format_success_message(f"Entity created successfully"))
375
+ output = self.output_formatter.format_output(result)
376
+ print(output)
377
+ return 0
378
+
379
+ except Exception as e:
380
+ print(format_error_message(f"Error creating entity: {e}"))
381
+ return 1
382
+
383
+ async def _handle_entity_update(self, args: argparse.Namespace) -> int:
384
+ """Handle entity update command."""
385
+ try:
386
+ entity_name = getattr(args, "entity_name", "")
387
+ key = getattr(args, "key", "")
388
+
389
+ # Get data from args
390
+ data_json = getattr(args, "data", None)
391
+ data_file = getattr(args, "file", None)
392
+
393
+ if data_json:
394
+ data = json.loads(data_json)
395
+ elif data_file:
396
+ with open(data_file, "r") as f:
397
+ data = json.load(f)
398
+ else:
399
+ print(format_error_message("Either --data or --file must be provided"))
400
+ return 1
401
+
402
+ result = await self.client.update_entity(entity_name, key, data)
403
+
404
+ print(format_success_message(f"Entity updated successfully"))
405
+ output = self.output_formatter.format_output(result)
406
+ print(output)
407
+ return 0
408
+
409
+ except Exception as e:
410
+ print(format_error_message(f"Error updating entity: {e}"))
411
+ return 1
412
+
413
+ async def _handle_entity_delete(self, args: argparse.Namespace) -> int:
414
+ """Handle entity delete command."""
415
+ try:
416
+ entity_name = getattr(args, "entity_name", "")
417
+ key = getattr(args, "key", "")
418
+
419
+ # Check for confirmation if not provided
420
+ if not getattr(args, "confirm", False):
421
+ response = input(
422
+ f"Are you sure you want to delete {entity_name} with key '{key}'? (y/N): "
423
+ )
424
+ if response.lower() not in ["y", "yes"]:
425
+ print("Delete operation cancelled")
426
+ return 0
427
+
428
+ success = await self.client.delete_entity(entity_name, key)
429
+
430
+ if success:
431
+ print(format_success_message(f"Entity deleted successfully"))
432
+ return 0
433
+ else:
434
+ print(format_error_message("Delete operation failed"))
435
+ return 1
436
+
437
+ except Exception as e:
438
+ print(format_error_message(f"Error deleting entity: {e}"))
439
+ return 1
440
+
441
+ async def _handle_action_commands(self, args: argparse.Namespace) -> int:
442
+ """Handle action operations."""
443
+ subcommand = getattr(args, "action_subcommand", None)
444
+
445
+ if subcommand == "list":
446
+ return await self._handle_action_list(args)
447
+ elif subcommand == "call":
448
+ return await self._handle_action_call(args)
449
+ else:
450
+ print(format_error_message(f"Unknown action subcommand: {subcommand}"))
451
+ return 1
452
+
453
+ async def _handle_action_list(self, args: argparse.Namespace) -> int:
454
+ """Handle action list command."""
455
+ try:
456
+ pattern = getattr(args, "pattern", "")
457
+ entity = getattr(args, "entity", None)
458
+
459
+ actions = self.client.search_actions(pattern)
460
+
461
+ if entity:
462
+ # Filter actions for specific entity (this is a simplified approach)
463
+ actions = [
464
+ action for action in actions if entity.lower() in action.lower()
465
+ ]
466
+
467
+ if not actions:
468
+ print(f"No actions found matching pattern: {pattern}")
469
+ return 0
470
+
471
+ output = self.output_formatter.format_output(actions)
472
+ print(output)
473
+ return 0
474
+
475
+ except Exception as e:
476
+ print(format_error_message(f"Error listing actions: {e}"))
477
+ return 1
478
+
479
+ async def _handle_action_call(self, args: argparse.Namespace) -> int:
480
+ """Handle action call command."""
481
+ try:
482
+ action_name = getattr(args, "action_name", "")
483
+ entity_name = getattr(args, "entity", None)
484
+ parameters_json = getattr(args, "parameters", None)
485
+
486
+ parameters = {}
487
+ if parameters_json:
488
+ parameters = json.loads(parameters_json)
489
+
490
+ result = await self.client.call_action(action_name, parameters, entity_name)
491
+
492
+ print(
493
+ format_success_message(f"Action '{action_name}' executed successfully")
494
+ )
495
+ output = self.output_formatter.format_output(result)
496
+ print(output)
497
+ return 0
498
+
499
+ except Exception as e:
500
+ print(format_error_message(f"Error calling action: {e}"))
501
+ return 1
502
+
503
+ async def _handle_config_commands(self, args: argparse.Namespace) -> int:
504
+ """Handle configuration management commands."""
505
+ subcommand = getattr(args, "config_subcommand", None)
506
+
507
+ if subcommand == "list":
508
+ return self._handle_config_list(args)
509
+ elif subcommand == "show":
510
+ return self._handle_config_show(args)
511
+ elif subcommand == "create":
512
+ return self._handle_config_create(args)
513
+ elif subcommand == "update":
514
+ return self._handle_config_update(args)
515
+ elif subcommand == "delete":
516
+ return self._handle_config_delete(args)
517
+ elif subcommand == "set-default":
518
+ return self._handle_config_set_default(args)
519
+ else:
520
+ print(format_error_message(f"Unknown config subcommand: {subcommand}"))
521
+ return 1
522
+
523
+ def _handle_config_list(self, args: argparse.Namespace) -> int:
524
+ """Handle config list command."""
525
+ try:
526
+ profiles = self.config_manager.list_profiles()
527
+ default_profile = self.config_manager.get_default_profile()
528
+
529
+ if not profiles:
530
+ print("No configuration profiles found")
531
+ return 0
532
+
533
+ profile_list = []
534
+ for name, profile in profiles.items():
535
+ profile_info = {
536
+ "name": name,
537
+ "base_url": profile.base_url,
538
+ "auth_mode": profile.auth_mode,
539
+ "default": (
540
+ "✓" if default_profile and default_profile.name == name else ""
541
+ ),
542
+ }
543
+ profile_list.append(profile_info)
544
+
545
+ output = self.output_formatter.format_output(
546
+ profile_list, ["name", "base_url", "auth_mode", "default"]
547
+ )
548
+ print(output)
549
+ return 0
550
+
551
+ except Exception as e:
552
+ print(format_error_message(f"Error listing profiles: {e}"))
553
+ return 1
554
+
555
+ def _handle_config_show(self, args: argparse.Namespace) -> int:
556
+ """Handle config show command."""
557
+ try:
558
+ profile_name = getattr(args, "profile_name", "")
559
+ profile = self.config_manager.get_profile(profile_name)
560
+
561
+ if not profile:
562
+ print(format_error_message(f"Profile not found: {profile_name}"))
563
+ return 1
564
+
565
+ # Convert profile to dict for display
566
+ profile_dict = {
567
+ "name": profile.name,
568
+ "base_url": profile.base_url,
569
+ "auth_mode": profile.auth_mode,
570
+ "verify_ssl": profile.verify_ssl,
571
+ "output_format": profile.output_format,
572
+ "label_cache": profile.label_cache,
573
+ "label_expiry": profile.label_expiry,
574
+ "language": profile.language,
575
+ }
576
+
577
+ # Only show auth details if they exist
578
+ if profile.client_id:
579
+ profile_dict["client_id"] = profile.client_id
580
+ if profile.tenant_id:
581
+ profile_dict["tenant_id"] = profile.tenant_id
582
+
583
+ output = self.output_formatter.format_output(profile_dict)
584
+ print(output)
585
+ return 0
586
+
587
+ except Exception as e:
588
+ print(format_error_message(f"Error showing profile: {e}"))
589
+ return 1
590
+
591
+ def _handle_config_create(self, args: argparse.Namespace) -> int:
592
+ """Handle config create command."""
593
+ try:
594
+ profile_name = getattr(args, "profile_name", "")
595
+ base_url = getattr(args, "base_url", "")
596
+
597
+ if not base_url:
598
+ print(
599
+ format_error_message(
600
+ "--base-url is required when creating a profile"
601
+ )
602
+ )
603
+ return 1
604
+
605
+ # Check if profile already exists
606
+ if self.config_manager.get_profile(profile_name):
607
+ print(format_error_message(f"Profile already exists: {profile_name}"))
608
+ return 1
609
+
610
+ # Create new profile
611
+ profile = Profile(
612
+ name=profile_name,
613
+ base_url=base_url,
614
+ auth_mode=getattr(args, "auth_mode", "default"),
615
+ client_id=getattr(args, "client_id", None),
616
+ client_secret=getattr(args, "client_secret", None),
617
+ tenant_id=getattr(args, "tenant_id", None),
618
+ verify_ssl=getattr(args, "verify_ssl", True),
619
+ output_format=getattr(args, "output_format", "table"),
620
+ use_label_cache=getattr(args, "label_cache", True),
621
+ label_cache_expiry_minutes=getattr(args, "label_expiry", 60),
622
+ use_cache_first=getattr(args, "use_cache_first", True),
623
+ timeout=getattr(args, "timeout", 60),
624
+ language=getattr(args, "language", "en-US"),
625
+ )
626
+
627
+ self.config_manager.save_profile(profile)
628
+ print(
629
+ format_success_message(f"Profile '{profile_name}' created successfully")
630
+ )
631
+ return 0
632
+
633
+ except Exception as e:
634
+ print(format_error_message(f"Error creating profile: {e}"))
635
+ return 1
636
+
637
+ def _handle_config_update(self, args: argparse.Namespace) -> int:
638
+ """Handle config update command."""
639
+ # Similar to create but updates existing profile
640
+ print(format_error_message("Config update not yet implemented"))
641
+ return 1
642
+
643
+ def _handle_config_delete(self, args: argparse.Namespace) -> int:
644
+ """Handle config delete command."""
645
+ try:
646
+ profile_name = getattr(args, "profile_name", "")
647
+
648
+ if not self.config_manager.get_profile(profile_name):
649
+ print(format_error_message(f"Profile not found: {profile_name}"))
650
+ return 1
651
+
652
+ success = self.config_manager.delete_profile(profile_name)
653
+ if success:
654
+ print(
655
+ format_success_message(
656
+ f"Profile '{profile_name}' deleted successfully"
657
+ )
658
+ )
659
+ return 0
660
+ else:
661
+ print(format_error_message(f"Failed to delete profile: {profile_name}"))
662
+ return 1
663
+
664
+ except Exception as e:
665
+ print(format_error_message(f"Error deleting profile: {e}"))
666
+ return 1
667
+
668
+ def _handle_config_set_default(self, args: argparse.Namespace) -> int:
669
+ """Handle config set-default command."""
670
+ try:
671
+ profile_name = getattr(args, "profile_name", "")
672
+
673
+ success = self.config_manager.set_default_profile(profile_name)
674
+ if success:
675
+ print(format_success_message(f"Default profile set to: {profile_name}"))
676
+ return 0
677
+ else:
678
+ print(format_error_message(f"Profile not found: {profile_name}"))
679
+ return 1
680
+
681
+ except Exception as e:
682
+ print(format_error_message(f"Error setting default profile: {e}"))
683
+ return 1
684
+
685
+ def _handle_error(self, error: Exception, verbose: bool = False) -> None:
686
+ """Handle and display errors consistently.
687
+
688
+ Args:
689
+ error: Exception that occurred
690
+ verbose: Whether to show detailed error information
691
+ """
692
+ if isinstance(error, FOClientError):
693
+ print(format_error_message(str(error)))
694
+ else:
695
+ if verbose:
696
+ import traceback
697
+
698
+ traceback.print_exc()
699
+ else:
700
+ print(format_error_message(f"Unexpected error: {error}"))