mcp-ticketer 0.1.39__py3-none-any.whl → 0.3.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.
mcp_ticketer/cli/main.py CHANGED
@@ -317,13 +317,161 @@ def get_adapter(
317
317
  return AdapterRegistry.get_adapter(adapter_type, adapter_config)
318
318
 
319
319
 
320
+ def _prompt_for_adapter_selection(console: Console) -> str:
321
+ """Interactive prompt for adapter selection.
322
+
323
+ Args:
324
+ console: Rich console for output
325
+
326
+ Returns:
327
+ Selected adapter type
328
+ """
329
+ console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
330
+ console.print("Choose which ticket system you want to connect to:\n")
331
+
332
+ # Define adapter options with descriptions
333
+ adapters = [
334
+ {
335
+ "name": "linear",
336
+ "title": "Linear",
337
+ "description": "Modern project management (linear.app)",
338
+ "requirements": "API key and team ID"
339
+ },
340
+ {
341
+ "name": "github",
342
+ "title": "GitHub Issues",
343
+ "description": "GitHub repository issues",
344
+ "requirements": "Personal access token, owner, and repo"
345
+ },
346
+ {
347
+ "name": "jira",
348
+ "title": "JIRA",
349
+ "description": "Atlassian JIRA project management",
350
+ "requirements": "Server URL, email, and API token"
351
+ },
352
+ {
353
+ "name": "aitrackdown",
354
+ "title": "Local Files (AITrackdown)",
355
+ "description": "Store tickets in local files (no external service)",
356
+ "requirements": "None - works offline"
357
+ }
358
+ ]
359
+
360
+ # Display options
361
+ for i, adapter in enumerate(adapters, 1):
362
+ console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
363
+ console.print(f" {adapter['description']}")
364
+ console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
365
+
366
+ # Get user selection
367
+ while True:
368
+ try:
369
+ choice = typer.prompt(
370
+ "Select adapter (1-4)",
371
+ type=int,
372
+ default=1
373
+ )
374
+ if 1 <= choice <= len(adapters):
375
+ selected_adapter = adapters[choice - 1]
376
+ console.print(f"\n[green]✓ Selected: {selected_adapter['title']}[/green]")
377
+ return selected_adapter["name"]
378
+ else:
379
+ console.print(f"[red]Please enter a number between 1 and {len(adapters)}[/red]")
380
+ except (ValueError, typer.Abort):
381
+ console.print("[yellow]Setup cancelled.[/yellow]")
382
+ raise typer.Exit(0)
383
+
384
+
385
+ @app.command()
386
+ def setup(
387
+ adapter: Optional[str] = typer.Option(
388
+ None,
389
+ "--adapter",
390
+ "-a",
391
+ help="Adapter type to use (interactive prompt if not specified)",
392
+ ),
393
+ project_path: Optional[str] = typer.Option(
394
+ None, "--path", help="Project path (default: current directory)"
395
+ ),
396
+ global_config: bool = typer.Option(
397
+ False,
398
+ "--global",
399
+ "-g",
400
+ help="Save to global config instead of project-specific",
401
+ ),
402
+ base_path: Optional[str] = typer.Option(
403
+ None,
404
+ "--base-path",
405
+ "-p",
406
+ help="Base path for ticket storage (AITrackdown only)",
407
+ ),
408
+ api_key: Optional[str] = typer.Option(
409
+ None, "--api-key", help="API key for Linear or API token for JIRA"
410
+ ),
411
+ team_id: Optional[str] = typer.Option(
412
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
413
+ ),
414
+ jira_server: Optional[str] = typer.Option(
415
+ None,
416
+ "--jira-server",
417
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
418
+ ),
419
+ jira_email: Optional[str] = typer.Option(
420
+ None, "--jira-email", help="JIRA user email for authentication"
421
+ ),
422
+ jira_project: Optional[str] = typer.Option(
423
+ None, "--jira-project", help="Default JIRA project key"
424
+ ),
425
+ github_owner: Optional[str] = typer.Option(
426
+ None, "--github-owner", help="GitHub repository owner"
427
+ ),
428
+ github_repo: Optional[str] = typer.Option(
429
+ None, "--github-repo", help="GitHub repository name"
430
+ ),
431
+ github_token: Optional[str] = typer.Option(
432
+ None, "--github-token", help="GitHub Personal Access Token"
433
+ ),
434
+ ) -> None:
435
+ """Interactive setup wizard for MCP Ticketer (alias for init).
436
+
437
+ This command provides a user-friendly setup experience with prompts
438
+ to guide you through configuring MCP Ticketer for your preferred
439
+ ticket management system. It's identical to 'init' and 'install'.
440
+
441
+ Examples:
442
+ # Run interactive setup
443
+ mcp-ticketer setup
444
+
445
+ # Setup with specific adapter
446
+ mcp-ticketer setup --adapter linear
447
+
448
+ # Setup for different project
449
+ mcp-ticketer setup --path /path/to/project
450
+ """
451
+ # Call init with all parameters
452
+ init(
453
+ adapter=adapter,
454
+ project_path=project_path,
455
+ global_config=global_config,
456
+ base_path=base_path,
457
+ api_key=api_key,
458
+ team_id=team_id,
459
+ jira_server=jira_server,
460
+ jira_email=jira_email,
461
+ jira_project=jira_project,
462
+ github_owner=github_owner,
463
+ github_repo=github_repo,
464
+ github_token=github_token,
465
+ )
466
+
467
+
320
468
  @app.command()
321
469
  def init(
322
470
  adapter: Optional[str] = typer.Option(
323
471
  None,
324
472
  "--adapter",
325
473
  "-a",
326
- help="Adapter type to use (auto-detected from .env if not specified)",
474
+ help="Adapter type to use (interactive prompt if not specified)",
327
475
  ),
328
476
  project_path: Optional[str] = typer.Option(
329
477
  None, "--path", help="Project path (default: current directory)"
@@ -369,11 +517,17 @@ def init(
369
517
  ) -> None:
370
518
  """Initialize mcp-ticketer for the current project.
371
519
 
520
+ This command sets up MCP Ticketer configuration with interactive prompts
521
+ to guide you through the process. It auto-detects adapter configuration
522
+ from .env files or prompts for interactive setup if no configuration is found.
523
+
372
524
  Creates .mcp-ticketer/config.json in the current directory with
373
525
  auto-detected or specified adapter configuration.
374
526
 
527
+ Note: 'setup' and 'install' are synonyms for this command.
528
+
375
529
  Examples:
376
- # Auto-detect from .env.local
530
+ # Interactive setup (same as 'setup' and 'install')
377
531
  mcp-ticketer init
378
532
 
379
533
  # Force specific adapter
@@ -429,16 +583,21 @@ def init(
429
583
  f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
430
584
  )
431
585
  console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
586
+
587
+ # Ask user to confirm auto-detected adapter
588
+ if not typer.confirm(
589
+ f"Use detected {adapter_type} adapter?",
590
+ default=True,
591
+ ):
592
+ adapter_type = None # Will trigger interactive selection
432
593
  else:
433
- adapter_type = "aitrackdown" # Fallback
434
- console.print(
435
- "[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]"
436
- )
594
+ adapter_type = None # Will trigger interactive selection
437
595
  else:
438
- adapter_type = "aitrackdown" # Fallback
439
- console.print(
440
- "[yellow]⚠ No .env files found, defaulting to aitrackdown[/yellow]"
441
- )
596
+ adapter_type = None # Will trigger interactive selection
597
+
598
+ # If no adapter determined, show interactive selection
599
+ if not adapter_type:
600
+ adapter_type = _prompt_for_adapter_selection(console)
442
601
 
443
602
  # 2. Create configuration based on adapter type
444
603
  config = {"default_adapter": adapter_type, "adapters": {}}
@@ -462,59 +621,99 @@ def init(
462
621
  }
463
622
 
464
623
  elif adapter_type == "linear":
465
- # If not auto-discovered, build from CLI params
624
+ # If not auto-discovered, build from CLI params or prompt
466
625
  if adapter_type not in config["adapters"]:
467
626
  linear_config = {}
468
627
 
469
- # Team ID
470
- if team_id:
471
- linear_config["team_id"] = team_id
472
-
473
628
  # API Key
474
629
  linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
630
+ if not linear_api_key and not discovered:
631
+ console.print("\n[bold]Linear Configuration[/bold]")
632
+ console.print("You need a Linear API key to connect to Linear.")
633
+ console.print("[dim]Get your API key at: https://linear.app/settings/api[/dim]\n")
634
+
635
+ linear_api_key = typer.prompt(
636
+ "Enter your Linear API key",
637
+ hide_input=True
638
+ )
639
+
475
640
  if linear_api_key:
476
641
  linear_config["api_key"] = linear_api_key
477
- elif not discovered:
478
- console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
479
- console.print(
480
- "Set LINEAR_API_KEY environment variable or use --api-key option"
481
- )
482
642
 
483
- if linear_config:
484
- linear_config["type"] = "linear"
485
- config["adapters"]["linear"] = linear_config
643
+ # Team ID
644
+ linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
645
+ if not linear_team_id and not discovered:
646
+ console.print("\nYou need your Linear team ID.")
647
+ console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
648
+
649
+ linear_team_id = typer.prompt("Enter your Linear team ID")
650
+
651
+ if linear_team_id:
652
+ linear_config["team_id"] = linear_team_id
653
+
654
+ if not linear_config.get("api_key") or not linear_config.get("team_id"):
655
+ console.print("[red]Error:[/red] Linear requires both API key and team ID")
656
+ console.print("Run 'mcp-ticketer init --adapter linear' with proper credentials")
657
+ raise typer.Exit(1)
658
+
659
+ linear_config["type"] = "linear"
660
+ config["adapters"]["linear"] = linear_config
486
661
 
487
662
  elif adapter_type == "jira":
488
- # If not auto-discovered, build from CLI params
663
+ # If not auto-discovered, build from CLI params or prompt
489
664
  if adapter_type not in config["adapters"]:
490
665
  server = jira_server or os.getenv("JIRA_SERVER")
491
666
  email = jira_email or os.getenv("JIRA_EMAIL")
492
667
  token = api_key or os.getenv("JIRA_API_TOKEN")
493
668
  project = jira_project or os.getenv("JIRA_PROJECT_KEY")
494
669
 
670
+ # Interactive prompts for missing values
671
+ if not server and not discovered:
672
+ console.print("\n[bold]JIRA Configuration[/bold]")
673
+ console.print("Enter your JIRA server details.\n")
674
+
675
+ server = typer.prompt(
676
+ "JIRA server URL (e.g., https://company.atlassian.net)"
677
+ )
678
+
679
+ if not email and not discovered:
680
+ email = typer.prompt("Your JIRA email address")
681
+
682
+ if not token and not discovered:
683
+ console.print("\nYou need a JIRA API token.")
684
+ console.print("[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n")
685
+
686
+ token = typer.prompt(
687
+ "Enter your JIRA API token",
688
+ hide_input=True
689
+ )
690
+
691
+ if not project and not discovered:
692
+ project = typer.prompt(
693
+ "Default JIRA project key (optional, press Enter to skip)",
694
+ default="",
695
+ show_default=False
696
+ )
697
+
698
+ # Validate required fields
495
699
  if not server:
496
700
  console.print("[red]Error:[/red] JIRA server URL is required")
497
- console.print(
498
- "Use --jira-server or set JIRA_SERVER environment variable"
499
- )
500
701
  raise typer.Exit(1)
501
702
 
502
703
  if not email:
503
704
  console.print("[red]Error:[/red] JIRA email is required")
504
- console.print("Use --jira-email or set JIRA_EMAIL environment variable")
505
705
  raise typer.Exit(1)
506
706
 
507
707
  if not token:
508
708
  console.print("[red]Error:[/red] JIRA API token is required")
509
- console.print(
510
- "Use --api-key or set JIRA_API_TOKEN environment variable"
511
- )
512
- console.print(
513
- "[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
514
- )
515
709
  raise typer.Exit(1)
516
710
 
517
- jira_config = {"server": server, "email": email, "api_token": token}
711
+ jira_config = {
712
+ "server": server,
713
+ "email": email,
714
+ "api_token": token,
715
+ "type": "jira"
716
+ }
518
717
 
519
718
  if project:
520
719
  jira_config["project_key"] = project
@@ -522,45 +721,50 @@ def init(
522
721
  config["adapters"]["jira"] = jira_config
523
722
 
524
723
  elif adapter_type == "github":
525
- # If not auto-discovered, build from CLI params
724
+ # If not auto-discovered, build from CLI params or prompt
526
725
  if adapter_type not in config["adapters"]:
527
726
  owner = github_owner or os.getenv("GITHUB_OWNER")
528
727
  repo = github_repo or os.getenv("GITHUB_REPO")
529
728
  token = github_token or os.getenv("GITHUB_TOKEN")
530
729
 
730
+ # Interactive prompts for missing values
731
+ if not owner and not discovered:
732
+ console.print("\n[bold]GitHub Configuration[/bold]")
733
+ console.print("Enter your GitHub repository details.\n")
734
+
735
+ owner = typer.prompt("GitHub repository owner (username or organization)")
736
+
737
+ if not repo and not discovered:
738
+ repo = typer.prompt("GitHub repository name")
739
+
740
+ if not token and not discovered:
741
+ console.print("\nYou need a GitHub Personal Access Token.")
742
+ console.print("[dim]Create one at: https://github.com/settings/tokens/new[/dim]")
743
+ console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n")
744
+
745
+ token = typer.prompt(
746
+ "Enter your GitHub Personal Access Token",
747
+ hide_input=True
748
+ )
749
+
750
+ # Validate required fields
531
751
  if not owner:
532
752
  console.print("[red]Error:[/red] GitHub repository owner is required")
533
- console.print(
534
- "Use --github-owner or set GITHUB_OWNER environment variable"
535
- )
536
753
  raise typer.Exit(1)
537
754
 
538
755
  if not repo:
539
756
  console.print("[red]Error:[/red] GitHub repository name is required")
540
- console.print(
541
- "Use --github-repo or set GITHUB_REPO environment variable"
542
- )
543
757
  raise typer.Exit(1)
544
758
 
545
759
  if not token:
546
- console.print(
547
- "[red]Error:[/red] GitHub Personal Access Token is required"
548
- )
549
- console.print(
550
- "Use --github-token or set GITHUB_TOKEN environment variable"
551
- )
552
- console.print(
553
- "[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
554
- )
555
- console.print(
556
- "[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]"
557
- )
760
+ console.print("[red]Error:[/red] GitHub Personal Access Token is required")
558
761
  raise typer.Exit(1)
559
762
 
560
763
  config["adapters"]["github"] = {
561
764
  "owner": owner,
562
765
  "repo": repo,
563
766
  "token": token,
767
+ "type": "github"
564
768
  }
565
769
 
566
770
  # 5. Save to appropriate location
@@ -600,6 +804,48 @@ def init(
600
804
  f.write("# MCP Ticketer\n.mcp-ticketer/\n")
601
805
  console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
602
806
 
807
+ # Show next steps
808
+ _show_next_steps(console, adapter_type, config_file_path)
809
+
810
+
811
+ def _show_next_steps(console: Console, adapter_type: str, config_file_path: Path) -> None:
812
+ """Show helpful next steps after initialization.
813
+
814
+ Args:
815
+ console: Rich console for output
816
+ adapter_type: Type of adapter that was configured
817
+ config_file_path: Path to the configuration file
818
+ """
819
+ console.print("\n[bold green]🎉 Setup Complete![/bold green]")
820
+ console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
821
+
822
+ console.print("[bold]Next Steps:[/bold]")
823
+ console.print("1. [cyan]Test your configuration:[/cyan]")
824
+ console.print(" mcp-ticketer diagnose")
825
+ console.print("\n2. [cyan]Create a test ticket:[/cyan]")
826
+ console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
827
+
828
+ if adapter_type != "aitrackdown":
829
+ console.print(f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]")
830
+
831
+ if adapter_type == "linear":
832
+ console.print(" Check your Linear workspace for the new ticket")
833
+ elif adapter_type == "github":
834
+ console.print(" Check your GitHub repository's Issues tab")
835
+ elif adapter_type == "jira":
836
+ console.print(" Check your JIRA project for the new ticket")
837
+ else:
838
+ console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
839
+ console.print(" ls .aitrackdown/")
840
+
841
+ console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
842
+ console.print(" mcp-ticketer mcp claude # For Claude Code")
843
+ console.print(" mcp-ticketer mcp auggie # For Auggie")
844
+ console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
845
+
846
+ console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
847
+ console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
848
+
603
849
 
604
850
  @app.command()
605
851
  def install(
@@ -653,12 +899,12 @@ def install(
653
899
  ) -> None:
654
900
  """Initialize mcp-ticketer for the current project (alias for init).
655
901
 
656
- This command is synonymous with 'init' and provides the same functionality.
657
- Creates .mcp-ticketer/config.json in the current directory with
658
- auto-detected or specified adapter configuration.
902
+ This command is synonymous with 'init' and 'setup' - all three provide
903
+ identical functionality with interactive prompts to guide you through
904
+ configuring MCP Ticketer for your preferred ticket management system.
659
905
 
660
906
  Examples:
661
- # Auto-detect from .env.local
907
+ # Interactive setup (same as 'init' and 'setup')
662
908
  mcp-ticketer install
663
909
 
664
910
  # Force specific adapter
@@ -1548,6 +1794,9 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
1548
1794
  console.print(f"\nRetry Count: {item.retry_count}")
1549
1795
 
1550
1796
 
1797
+
1798
+
1799
+
1551
1800
  @app.command()
1552
1801
  def serve(
1553
1802
  adapter: Optional[AdapterType] = typer.Option(
@@ -1575,16 +1824,27 @@ def serve(
1575
1824
  # Load configuration (respects project-specific config in cwd)
1576
1825
  config = load_config()
1577
1826
 
1578
- # Determine adapter type
1579
- adapter_type = (
1580
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1581
- )
1582
-
1583
- # Get adapter configuration
1584
- adapters_config = config.get("adapters", {})
1585
- adapter_config = adapters_config.get(adapter_type, {})
1827
+ # Determine adapter type with priority: CLI arg > .env files > config > default
1828
+ if adapter:
1829
+ # Priority 1: Command line argument
1830
+ adapter_type = adapter.value
1831
+ # Get base config from config file
1832
+ adapters_config = config.get("adapters", {})
1833
+ adapter_config = adapters_config.get(adapter_type, {})
1834
+ else:
1835
+ # Priority 2: .env files
1836
+ from ..mcp.server import _load_env_configuration
1837
+ env_config = _load_env_configuration()
1838
+ if env_config:
1839
+ adapter_type = env_config["adapter_type"]
1840
+ adapter_config = env_config["adapter_config"]
1841
+ else:
1842
+ # Priority 3: Configuration file
1843
+ adapter_type = config.get("default_adapter", "aitrackdown")
1844
+ adapters_config = config.get("adapters", {})
1845
+ adapter_config = adapters_config.get(adapter_type, {})
1586
1846
 
1587
- # Override with command line options if provided
1847
+ # Override with command line options if provided (highest priority)
1588
1848
  if base_path and adapter_type == "aitrackdown":
1589
1849
  adapter_config["base_path"] = base_path
1590
1850
 
@@ -0,0 +1,152 @@
1
+ """Exception classes for MCP Ticketer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from .models import TicketState
8
+
9
+
10
+ class MCPTicketerError(Exception):
11
+ """Base exception for MCP Ticketer."""
12
+ pass
13
+
14
+
15
+ class AdapterError(MCPTicketerError):
16
+ """Base adapter error."""
17
+
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ adapter_name: str,
22
+ original_error: Optional[Exception] = None
23
+ ):
24
+ """Initialize adapter error.
25
+
26
+ Args:
27
+ message: Error message
28
+ adapter_name: Name of the adapter that raised the error
29
+ original_error: Original exception that caused this error
30
+ """
31
+ super().__init__(message)
32
+ self.adapter_name = adapter_name
33
+ self.original_error = original_error
34
+
35
+ def __str__(self) -> str:
36
+ """String representation of the error."""
37
+ base_msg = f"[{self.adapter_name}] {super().__str__()}"
38
+ if self.original_error:
39
+ base_msg += f" (caused by: {self.original_error})"
40
+ return base_msg
41
+
42
+
43
+ class AuthenticationError(AdapterError):
44
+ """Authentication failed with external service."""
45
+ pass
46
+
47
+
48
+ class RateLimitError(AdapterError):
49
+ """Rate limit exceeded."""
50
+
51
+ def __init__(
52
+ self,
53
+ message: str,
54
+ adapter_name: str,
55
+ retry_after: Optional[int] = None,
56
+ original_error: Optional[Exception] = None
57
+ ):
58
+ """Initialize rate limit error.
59
+
60
+ Args:
61
+ message: Error message
62
+ adapter_name: Name of the adapter
63
+ retry_after: Seconds to wait before retrying
64
+ original_error: Original exception
65
+ """
66
+ super().__init__(message, adapter_name, original_error)
67
+ self.retry_after = retry_after
68
+
69
+
70
+ class ValidationError(MCPTicketerError):
71
+ """Data validation error."""
72
+
73
+ def __init__(
74
+ self,
75
+ message: str,
76
+ field: Optional[str] = None,
77
+ value: Any = None
78
+ ):
79
+ """Initialize validation error.
80
+
81
+ Args:
82
+ message: Error message
83
+ field: Field that failed validation
84
+ value: Value that failed validation
85
+ """
86
+ super().__init__(message)
87
+ self.field = field
88
+ self.value = value
89
+
90
+ def __str__(self) -> str:
91
+ """String representation of the error."""
92
+ base_msg = super().__str__()
93
+ if self.field:
94
+ base_msg += f" (field: {self.field})"
95
+ if self.value is not None:
96
+ base_msg += f" (value: {self.value})"
97
+ return base_msg
98
+
99
+
100
+ class ConfigurationError(MCPTicketerError):
101
+ """Configuration error."""
102
+ pass
103
+
104
+
105
+ class CacheError(MCPTicketerError):
106
+ """Cache operation error."""
107
+ pass
108
+
109
+
110
+ class StateTransitionError(MCPTicketerError):
111
+ """Invalid state transition."""
112
+
113
+ def __init__(
114
+ self,
115
+ message: str,
116
+ from_state: TicketState,
117
+ to_state: TicketState
118
+ ):
119
+ """Initialize state transition error.
120
+
121
+ Args:
122
+ message: Error message
123
+ from_state: Current state
124
+ to_state: Target state
125
+ """
126
+ super().__init__(message)
127
+ self.from_state = from_state
128
+ self.to_state = to_state
129
+
130
+ def __str__(self) -> str:
131
+ """String representation of the error."""
132
+ return f"{super().__str__()} ({self.from_state} -> {self.to_state})"
133
+
134
+
135
+ class NetworkError(AdapterError):
136
+ """Network-related error."""
137
+ pass
138
+
139
+
140
+ class TimeoutError(AdapterError):
141
+ """Request timeout error."""
142
+ pass
143
+
144
+
145
+ class NotFoundError(AdapterError):
146
+ """Resource not found error."""
147
+ pass
148
+
149
+
150
+ class PermissionError(AdapterError):
151
+ """Permission denied error."""
152
+ pass