mcp-ticketer 2.0.1__py3-none-any.whl → 2.2.13__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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (73) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/_version_scm.py +1 -0
  3. mcp_ticketer/adapters/aitrackdown.py +122 -0
  4. mcp_ticketer/adapters/asana/adapter.py +121 -0
  5. mcp_ticketer/adapters/github/__init__.py +26 -0
  6. mcp_ticketer/adapters/{github.py → github/adapter.py} +1506 -365
  7. mcp_ticketer/adapters/github/client.py +335 -0
  8. mcp_ticketer/adapters/github/mappers.py +797 -0
  9. mcp_ticketer/adapters/github/queries.py +692 -0
  10. mcp_ticketer/adapters/github/types.py +460 -0
  11. mcp_ticketer/adapters/jira/__init__.py +35 -0
  12. mcp_ticketer/adapters/{jira.py → jira/adapter.py} +250 -678
  13. mcp_ticketer/adapters/jira/client.py +271 -0
  14. mcp_ticketer/adapters/jira/mappers.py +246 -0
  15. mcp_ticketer/adapters/jira/queries.py +216 -0
  16. mcp_ticketer/adapters/jira/types.py +304 -0
  17. mcp_ticketer/adapters/linear/adapter.py +1000 -92
  18. mcp_ticketer/adapters/linear/client.py +91 -1
  19. mcp_ticketer/adapters/linear/mappers.py +107 -0
  20. mcp_ticketer/adapters/linear/queries.py +112 -2
  21. mcp_ticketer/adapters/linear/types.py +50 -10
  22. mcp_ticketer/cli/configure.py +524 -89
  23. mcp_ticketer/cli/install_mcp_server.py +418 -0
  24. mcp_ticketer/cli/main.py +10 -0
  25. mcp_ticketer/cli/mcp_configure.py +177 -49
  26. mcp_ticketer/cli/platform_installer.py +9 -0
  27. mcp_ticketer/cli/setup_command.py +157 -1
  28. mcp_ticketer/cli/ticket_commands.py +443 -81
  29. mcp_ticketer/cli/utils.py +113 -0
  30. mcp_ticketer/core/__init__.py +28 -0
  31. mcp_ticketer/core/adapter.py +367 -1
  32. mcp_ticketer/core/milestone_manager.py +252 -0
  33. mcp_ticketer/core/models.py +345 -0
  34. mcp_ticketer/core/project_utils.py +281 -0
  35. mcp_ticketer/core/project_validator.py +376 -0
  36. mcp_ticketer/core/session_state.py +6 -1
  37. mcp_ticketer/core/state_matcher.py +36 -3
  38. mcp_ticketer/mcp/server/__main__.py +2 -1
  39. mcp_ticketer/mcp/server/routing.py +68 -0
  40. mcp_ticketer/mcp/server/tools/__init__.py +7 -4
  41. mcp_ticketer/mcp/server/tools/attachment_tools.py +3 -1
  42. mcp_ticketer/mcp/server/tools/config_tools.py +233 -35
  43. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  44. mcp_ticketer/mcp/server/tools/search_tools.py +30 -1
  45. mcp_ticketer/mcp/server/tools/ticket_tools.py +37 -1
  46. mcp_ticketer/queue/queue.py +68 -0
  47. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/METADATA +33 -3
  48. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/RECORD +72 -36
  49. mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
  50. py_mcp_installer/examples/phase3_demo.py +178 -0
  51. py_mcp_installer/scripts/manage_version.py +54 -0
  52. py_mcp_installer/setup.py +6 -0
  53. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  54. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  55. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  56. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  57. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  58. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  59. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  60. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  61. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  62. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  63. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  64. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  65. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  66. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  67. py_mcp_installer/tests/__init__.py +0 -0
  68. py_mcp_installer/tests/platforms/__init__.py +0 -0
  69. py_mcp_installer/tests/test_platform_detector.py +17 -0
  70. mcp_ticketer-2.0.1.dist-info/top_level.txt +0 -1
  71. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
  72. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
  73. {mcp_ticketer-2.0.1.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,252 @@
1
+ """Local milestone storage manager.
2
+
3
+ This module provides local persistent storage for milestones in the
4
+ .mcp-ticketer/milestones.json file. It handles CRUD operations for
5
+ milestones with automatic timestamp management and filtering support.
6
+
7
+ The storage format is JSON with the following structure:
8
+ {
9
+ "version": "1.0",
10
+ "milestones": {
11
+ "milestone-id-1": {...},
12
+ "milestone-id-2": {...}
13
+ }
14
+ }
15
+
16
+ Example:
17
+ >>> from pathlib import Path
18
+ >>> from mcp_ticketer.core.milestone_manager import MilestoneManager
19
+ >>> from mcp_ticketer.core.models import Milestone
20
+ >>>
21
+ >>> config_dir = Path.home() / ".mcp-ticketer"
22
+ >>> manager = MilestoneManager(config_dir)
23
+ >>>
24
+ >>> milestone = Milestone(
25
+ ... id="mile-001",
26
+ ... name="v2.1.0 Release",
27
+ ... labels=["v2.1", "release"]
28
+ ... )
29
+ >>> saved = manager.save_milestone(milestone)
30
+ >>> retrieved = manager.get_milestone("mile-001")
31
+
32
+ Note:
33
+ Related to ticket 1M-607: Add milestone support (Phase 1 - Core Infrastructure)
34
+
35
+ """
36
+
37
+ import json
38
+ import logging
39
+ from datetime import datetime
40
+ from pathlib import Path
41
+
42
+ from .models import Milestone
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class MilestoneManager:
48
+ """Manages local milestone storage in .mcp-ticketer/milestones.json.
49
+
50
+ This class provides a simple file-based storage mechanism for milestones,
51
+ with automatic timestamp management and filtering capabilities. It is
52
+ designed to work alongside adapter-specific milestone implementations.
53
+
54
+ Attributes:
55
+ config_dir: Path to .mcp-ticketer configuration directory
56
+ milestones_file: Path to milestones.json storage file
57
+
58
+ """
59
+
60
+ def __init__(self, config_dir: Path):
61
+ """Initialize milestone manager.
62
+
63
+ Creates the storage file if it doesn't exist. Ensures the config
64
+ directory exists before attempting to create the storage file.
65
+
66
+ Args:
67
+ config_dir: Path to .mcp-ticketer directory
68
+
69
+ """
70
+ self.config_dir = config_dir
71
+ self.milestones_file = config_dir / "milestones.json"
72
+ self._ensure_storage()
73
+
74
+ def _ensure_storage(self) -> None:
75
+ """Ensure milestone storage file exists.
76
+
77
+ Creates the config directory and initializes an empty storage file
78
+ if it doesn't exist. Uses atomic write to prevent corruption.
79
+
80
+ """
81
+ if not self.milestones_file.exists():
82
+ self.config_dir.mkdir(parents=True, exist_ok=True)
83
+ self._save_data({"milestones": {}, "version": "1.0"})
84
+ logger.info(f"Initialized milestone storage at {self.milestones_file}")
85
+
86
+ def _load_data(self) -> dict:
87
+ """Load milestone data from file.
88
+
89
+ Returns:
90
+ Dictionary containing milestones and version info
91
+
92
+ Note:
93
+ Returns empty structure if file doesn't exist or is corrupted
94
+
95
+ """
96
+ try:
97
+ with open(self.milestones_file, encoding="utf-8") as f:
98
+ return json.load(f)
99
+ except (json.JSONDecodeError, FileNotFoundError) as e:
100
+ logger.warning(
101
+ f"Failed to load milestones from {self.milestones_file}: {e}"
102
+ )
103
+ return {"milestones": {}, "version": "1.0"}
104
+
105
+ def _save_data(self, data: dict) -> None:
106
+ """Save milestone data to file.
107
+
108
+ Uses atomic write pattern to prevent corruption. Serializes datetime
109
+ objects to ISO format strings automatically.
110
+
111
+ Args:
112
+ data: Dictionary containing milestones and version info
113
+
114
+ """
115
+ with open(self.milestones_file, "w", encoding="utf-8") as f:
116
+ json.dump(data, f, indent=2, default=self._json_serializer)
117
+
118
+ @staticmethod
119
+ def _json_serializer(obj):
120
+ """Custom JSON serializer for datetime objects.
121
+
122
+ Args:
123
+ obj: Object to serialize
124
+
125
+ Returns:
126
+ ISO format string for datetime, original object otherwise
127
+
128
+ """
129
+ if isinstance(obj, datetime):
130
+ return obj.isoformat()
131
+ raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
132
+
133
+ def save_milestone(self, milestone: Milestone) -> Milestone:
134
+ """Save or update milestone in local storage.
135
+
136
+ Automatically updates the updated_at timestamp before saving.
137
+ If the milestone doesn't have an ID, generates one based on name.
138
+
139
+ Args:
140
+ milestone: Milestone to save
141
+
142
+ Returns:
143
+ Saved milestone with updated timestamp
144
+
145
+ """
146
+ data = self._load_data()
147
+
148
+ # Generate ID if not present
149
+ if not milestone.id:
150
+ # Simple ID generation based on name
151
+ import uuid
152
+
153
+ milestone.id = str(uuid.uuid4())[:8]
154
+
155
+ # Update timestamp
156
+ milestone.updated_at = datetime.utcnow()
157
+ if not milestone.created_at:
158
+ milestone.created_at = datetime.utcnow()
159
+
160
+ # Convert to dict for storage
161
+ milestone_dict = milestone.model_dump(mode="json")
162
+ data["milestones"][milestone.id] = milestone_dict
163
+
164
+ self._save_data(data)
165
+ logger.debug(f"Saved milestone {milestone.id} ({milestone.name})")
166
+ return milestone
167
+
168
+ def get_milestone(self, milestone_id: str) -> Milestone | None:
169
+ """Get milestone by ID.
170
+
171
+ Args:
172
+ milestone_id: Milestone identifier
173
+
174
+ Returns:
175
+ Milestone object or None if not found
176
+
177
+ """
178
+ data = self._load_data()
179
+ milestone_data = data["milestones"].get(milestone_id)
180
+
181
+ if not milestone_data:
182
+ logger.debug(f"Milestone {milestone_id} not found")
183
+ return None
184
+
185
+ return Milestone(**milestone_data)
186
+
187
+ def list_milestones(
188
+ self,
189
+ project_id: str | None = None,
190
+ state: str | None = None,
191
+ ) -> list[Milestone]:
192
+ """List milestones with optional filters.
193
+
194
+ Filters are applied in sequence: project_id, then state.
195
+ Results are sorted by target_date (None values appear last).
196
+
197
+ Args:
198
+ project_id: Filter by project ID
199
+ state: Filter by state (open, active, completed, closed)
200
+
201
+ Returns:
202
+ List of milestones matching filters, sorted by target_date
203
+
204
+ """
205
+ data = self._load_data()
206
+ milestones = []
207
+
208
+ for milestone_data in data["milestones"].values():
209
+ milestone = Milestone(**milestone_data)
210
+
211
+ # Apply filters
212
+ if project_id and milestone.project_id != project_id:
213
+ continue
214
+ if state and milestone.state != state:
215
+ continue
216
+
217
+ milestones.append(milestone)
218
+
219
+ # Sort by target_date (None values last)
220
+ milestones.sort(
221
+ key=lambda m: (
222
+ m.target_date is None,
223
+ m.target_date if m.target_date else datetime.max,
224
+ )
225
+ )
226
+
227
+ logger.debug(
228
+ f"Listed {len(milestones)} milestones "
229
+ f"(project_id={project_id}, state={state})"
230
+ )
231
+ return milestones
232
+
233
+ def delete_milestone(self, milestone_id: str) -> bool:
234
+ """Delete milestone from storage.
235
+
236
+ Args:
237
+ milestone_id: Milestone identifier
238
+
239
+ Returns:
240
+ True if deleted, False if not found
241
+
242
+ """
243
+ data = self._load_data()
244
+
245
+ if milestone_id not in data["milestones"]:
246
+ logger.warning(f"Cannot delete milestone {milestone_id}: not found")
247
+ return False
248
+
249
+ del data["milestones"][milestone_id]
250
+ self._save_data(data)
251
+ logger.info(f"Deleted milestone {milestone_id}")
252
+ return True
@@ -519,6 +519,351 @@ class ProjectUpdate(BaseModel):
519
519
  )
520
520
 
521
521
 
522
+ class Milestone(BaseModel):
523
+ """Universal milestone model for cross-platform support.
524
+
525
+ A milestone is a collection of issues grouped by labels with a target date.
526
+ Progress is calculated by counting closed vs total issues matching the labels.
527
+
528
+ Platform Mappings:
529
+ - Linear: Milestones (with labels and target dates)
530
+ - GitHub: Milestones (native support with due dates)
531
+ - JIRA: Versions/Releases (with target dates)
532
+ - Asana: Projects with dates (workaround via filtering)
533
+
534
+ The model follows the user's definition: "A milestone is a list of labels
535
+ with target dates, into which issues can be grouped."
536
+
537
+ Attributes:
538
+ id: Unique milestone identifier
539
+ name: Milestone name
540
+ target_date: Target completion date (ISO format: YYYY-MM-DD)
541
+ state: Milestone state (open, active, completed, closed)
542
+ description: Milestone description
543
+ labels: Labels that define this milestone's scope
544
+ total_issues: Total issues in milestone (calculated)
545
+ closed_issues: Closed issues in milestone (calculated)
546
+ progress_pct: Progress percentage 0-100 (calculated)
547
+ project_id: Associated project/epic ID
548
+ created_at: Creation timestamp
549
+ updated_at: Last update timestamp
550
+ platform_data: Platform-specific metadata
551
+
552
+ Example:
553
+ >>> milestone = Milestone(
554
+ ... name="v2.1.0 Release",
555
+ ... target_date=date(2025, 12, 31),
556
+ ... labels=["v2.1", "release"],
557
+ ... project_id="proj-123"
558
+ ... )
559
+ >>> milestone.total_issues = 15
560
+ >>> milestone.closed_issues = 8
561
+ >>> milestone.progress_pct = 53.3
562
+
563
+ Note:
564
+ Related to ticket 1M-607: Add milestone support (Phase 1 - Core Infrastructure)
565
+
566
+ """
567
+
568
+ model_config = ConfigDict(use_enum_values=True)
569
+
570
+ id: str | None = Field(None, description="Unique milestone identifier")
571
+ name: str = Field(..., min_length=1, description="Milestone name")
572
+ target_date: datetime | None = Field(
573
+ None, description="Target completion date (ISO format: YYYY-MM-DD)"
574
+ )
575
+ state: str = Field(
576
+ "open", description="Milestone state: open, active, completed, closed"
577
+ )
578
+ description: str = Field("", description="Milestone description")
579
+
580
+ # Label-based grouping (user's definition)
581
+ labels: list[str] = Field(
582
+ default_factory=list, description="Labels that define this milestone"
583
+ )
584
+
585
+ # Progress tracking (calculated fields)
586
+ total_issues: int = Field(0, ge=0, description="Total issues in milestone")
587
+ closed_issues: int = Field(0, ge=0, description="Closed issues in milestone")
588
+ progress_pct: float = Field(
589
+ 0.0, ge=0.0, le=100.0, description="Progress percentage (0-100)"
590
+ )
591
+
592
+ # Metadata
593
+ project_id: str | None = Field(None, description="Associated project ID")
594
+ created_at: datetime | None = Field(None, description="Creation timestamp")
595
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
596
+
597
+ # Platform-specific data
598
+ platform_data: dict[str, Any] = Field(
599
+ default_factory=dict, description="Platform-specific metadata"
600
+ )
601
+
602
+
603
+ class ProjectState(str, Enum):
604
+ """Project state across platforms.
605
+
606
+ Maps to different platform concepts:
607
+ - Linear: planned, started, completed, paused, canceled
608
+ - GitHub V2: OPEN, CLOSED (with status field for more granular states)
609
+ - JIRA: Not directly supported (use project status or custom fields)
610
+
611
+ Attributes:
612
+ PLANNED: Project is planned but not yet started
613
+ ACTIVE: Project is actively being worked on
614
+ COMPLETED: Project is finished successfully
615
+ ARCHIVED: Project is archived (no longer active)
616
+ CANCELLED: Project was cancelled before completion
617
+
618
+ """
619
+
620
+ PLANNED = "planned"
621
+ ACTIVE = "active"
622
+ COMPLETED = "completed"
623
+ ARCHIVED = "archived"
624
+ CANCELLED = "cancelled"
625
+
626
+
627
+ class ProjectVisibility(str, Enum):
628
+ """Project visibility setting.
629
+
630
+ Controls who can view the project across platforms.
631
+
632
+ Attributes:
633
+ PUBLIC: Visible to everyone
634
+ PRIVATE: Visible only to members
635
+ TEAM: Visible to team members
636
+
637
+ """
638
+
639
+ PUBLIC = "public"
640
+ PRIVATE = "private"
641
+ TEAM = "team"
642
+
643
+
644
+ class ProjectScope(str, Enum):
645
+ """Project organizational scope.
646
+
647
+ Defines the level at which a project exists in the organization hierarchy.
648
+
649
+ Platform Mappings:
650
+ - Linear: TEAM (projects belong to teams) or ORGANIZATION
651
+ - GitHub: REPOSITORY, USER, or ORGANIZATION
652
+ - JIRA: PROJECT (inherent) or ORGANIZATION (via project hierarchy)
653
+
654
+ Attributes:
655
+ USER: User-level project (GitHub Projects V2)
656
+ TEAM: Team-level project (Linear, GitHub org teams)
657
+ ORGANIZATION: Organization-level project (cross-team)
658
+ REPOSITORY: Repository-scoped project (GitHub)
659
+
660
+ """
661
+
662
+ USER = "user"
663
+ TEAM = "team"
664
+ ORGANIZATION = "organization"
665
+ REPOSITORY = "repository"
666
+
667
+
668
+ class Project(BaseModel):
669
+ """Unified project model across platforms.
670
+
671
+ Projects represent strategic-level containers for issues, superseding the
672
+ Epic model with a more comprehensive structure that maps cleanly to:
673
+ - Linear Projects
674
+ - GitHub Projects V2
675
+ - JIRA Projects/Epics
676
+
677
+ This model provides backward compatibility through conversion utilities
678
+ (see project_utils.py) while enabling richer project management features.
679
+
680
+ Attributes:
681
+ id: Unique identifier in MCP Ticketer namespace
682
+ platform: Platform identifier ("linear", "github", "jira")
683
+ platform_id: Original platform-specific identifier
684
+ scope: Organizational scope of the project
685
+ name: Project name (required)
686
+ description: Detailed project description
687
+ state: Current project state
688
+ visibility: Who can view the project
689
+ url: Direct URL to project in platform
690
+ created_at: When project was created
691
+ updated_at: When project was last modified
692
+ start_date: Planned or actual start date
693
+ target_date: Target completion date
694
+ completed_at: Actual completion date
695
+ owner_id: Project owner/lead user ID
696
+ owner_name: Project owner/lead display name
697
+ team_id: Team this project belongs to
698
+ team_name: Team display name
699
+ child_issues: List of issue IDs in this project
700
+ issue_count: Total number of issues
701
+ completed_count: Number of completed issues
702
+ in_progress_count: Number of in-progress issues
703
+ progress_percentage: Overall completion percentage
704
+ extra_data: Platform-specific additional data
705
+
706
+ Example:
707
+ >>> project = Project(
708
+ ... id="proj-123",
709
+ ... platform="linear",
710
+ ... platform_id="eac28953c267",
711
+ ... scope=ProjectScope.TEAM,
712
+ ... name="MCP Ticketer v2.0",
713
+ ... state=ProjectState.ACTIVE,
714
+ ... visibility=ProjectVisibility.TEAM
715
+ ... )
716
+
717
+ """
718
+
719
+ model_config = ConfigDict(use_enum_values=True)
720
+
721
+ # Core identification
722
+ id: str = Field(..., description="Unique identifier")
723
+ platform: str = Field(..., description="Platform name (linear, github, jira)")
724
+ platform_id: str = Field(..., description="Original platform ID")
725
+ scope: ProjectScope = Field(..., description="Organizational scope")
726
+
727
+ # Basic information
728
+ name: str = Field(..., min_length=1, description="Project name")
729
+ description: str | None = Field(None, description="Project description")
730
+ state: ProjectState = Field(ProjectState.PLANNED, description="Current state")
731
+ visibility: ProjectVisibility = Field(
732
+ ProjectVisibility.TEAM, description="Visibility"
733
+ )
734
+
735
+ # URLs and references
736
+ url: str | None = Field(None, description="Direct URL to project")
737
+
738
+ # Dates
739
+ created_at: datetime | None = Field(None, description="Creation timestamp")
740
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
741
+ start_date: datetime | None = Field(None, description="Start date")
742
+ target_date: datetime | None = Field(None, description="Target completion date")
743
+ completed_at: datetime | None = Field(None, description="Completion timestamp")
744
+
745
+ # Ownership
746
+ owner_id: str | None = Field(None, description="Owner user ID")
747
+ owner_name: str | None = Field(None, description="Owner display name")
748
+ team_id: str | None = Field(None, description="Team ID")
749
+ team_name: str | None = Field(None, description="Team display name")
750
+
751
+ # Issue relationships
752
+ child_issues: list[str] = Field(default_factory=list, description="Child issue IDs")
753
+ issue_count: int | None = Field(None, ge=0, description="Total issue count")
754
+ completed_count: int | None = Field(None, ge=0, description="Completed issues")
755
+ in_progress_count: int | None = Field(None, ge=0, description="In-progress issues")
756
+ progress_percentage: float | None = Field(
757
+ None, ge=0.0, le=100.0, description="Completion percentage"
758
+ )
759
+
760
+ # Platform-specific data
761
+ extra_data: dict[str, Any] = Field(
762
+ default_factory=dict, description="Platform-specific metadata"
763
+ )
764
+
765
+ def calculate_progress(self) -> float:
766
+ """Calculate progress percentage from issue counts.
767
+
768
+ Returns:
769
+ Progress percentage (0-100), or 0 if no issues
770
+
771
+ """
772
+ if not self.issue_count or self.issue_count == 0:
773
+ return 0.0
774
+
775
+ completed = self.completed_count or 0
776
+ return (completed / self.issue_count) * 100.0
777
+
778
+
779
+ class ProjectStatistics(BaseModel):
780
+ """Statistics and metrics for a project.
781
+
782
+ Provides calculated metrics for project health and progress tracking.
783
+ These statistics are typically computed from current project state
784
+ rather than stored directly.
785
+
786
+ Attributes:
787
+ project_id: ID of the project these stats belong to (optional for compatibility)
788
+ total_issues: Total number of issues (legacy field, use total_count)
789
+ completed_issues: Count of completed issues (legacy field, use completed_count)
790
+ in_progress_issues: Count of in-progress issues (legacy field, use in_progress_count)
791
+ open_issues: Count of open/backlog issues (legacy field, use open_count)
792
+ blocked_issues: Count of blocked issues (legacy field, use blocked_count)
793
+ total_count: Total number of issues (preferred)
794
+ open_count: Count of open issues (preferred)
795
+ in_progress_count: Count of in-progress issues (preferred)
796
+ completed_count: Count of completed issues (preferred)
797
+ blocked_count: Count of blocked issues (preferred)
798
+ priority_low_count: Count of low priority issues
799
+ priority_medium_count: Count of medium priority issues
800
+ priority_high_count: Count of high priority issues
801
+ priority_critical_count: Count of critical priority issues
802
+ health: Project health status (on_track, at_risk, off_track)
803
+ progress_percentage: Overall completion percentage
804
+ velocity: Issues completed per week (if available)
805
+ estimated_completion: Projected completion date
806
+
807
+ Example:
808
+ >>> stats = ProjectStatistics(
809
+ ... total_count=50,
810
+ ... completed_count=30,
811
+ ... in_progress_count=15,
812
+ ... open_count=5,
813
+ ... blocked_count=0,
814
+ ... priority_high_count=10,
815
+ ... health="on_track",
816
+ ... progress_percentage=60.0
817
+ ... )
818
+
819
+ """
820
+
821
+ model_config = ConfigDict(use_enum_values=True)
822
+
823
+ # Legacy fields for backward compatibility (optional)
824
+ project_id: str | None = Field(None, description="Project identifier (legacy)")
825
+ total_issues: int | None = Field(
826
+ None, ge=0, description="Total issue count (legacy)"
827
+ )
828
+ completed_issues: int | None = Field(
829
+ None, ge=0, description="Completed issues (legacy)"
830
+ )
831
+ in_progress_issues: int | None = Field(
832
+ None, ge=0, description="In-progress issues (legacy)"
833
+ )
834
+ open_issues: int | None = Field(
835
+ None, ge=0, description="Open/backlog issues (legacy)"
836
+ )
837
+ blocked_issues: int | None = Field(
838
+ None, ge=0, description="Blocked issues (legacy)"
839
+ )
840
+
841
+ # New preferred fields
842
+ total_count: int = Field(0, ge=0, description="Total issue count")
843
+ open_count: int = Field(0, ge=0, description="Open issues")
844
+ in_progress_count: int = Field(0, ge=0, description="In-progress issues")
845
+ completed_count: int = Field(0, ge=0, description="Completed issues")
846
+ blocked_count: int = Field(0, ge=0, description="Blocked issues")
847
+
848
+ # Priority distribution
849
+ priority_low_count: int = Field(0, ge=0, description="Low priority issues")
850
+ priority_medium_count: int = Field(0, ge=0, description="Medium priority issues")
851
+ priority_high_count: int = Field(0, ge=0, description="High priority issues")
852
+ priority_critical_count: int = Field(
853
+ 0, ge=0, description="Critical priority issues"
854
+ )
855
+
856
+ # Health and progress
857
+ health: str = Field(
858
+ "on_track", description="Health status: on_track, at_risk, off_track"
859
+ )
860
+ progress_percentage: float = Field(0.0, ge=0.0, le=100.0, description="Progress %")
861
+ velocity: float | None = Field(None, description="Issues/week completion rate")
862
+ estimated_completion: datetime | None = Field(
863
+ None, description="Projected completion date"
864
+ )
865
+
866
+
522
867
  class SearchQuery(BaseModel):
523
868
  """Search query parameters."""
524
869