mcp-ticketer 0.3.1__py3-none-any.whl → 0.3.2__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 (37) hide show
  1. mcp_ticketer/__version__.py +1 -1
  2. mcp_ticketer/adapters/aitrackdown.py +12 -15
  3. mcp_ticketer/adapters/github.py +7 -4
  4. mcp_ticketer/adapters/jira.py +23 -22
  5. mcp_ticketer/adapters/linear/__init__.py +1 -1
  6. mcp_ticketer/adapters/linear/adapter.py +88 -89
  7. mcp_ticketer/adapters/linear/client.py +71 -52
  8. mcp_ticketer/adapters/linear/mappers.py +88 -68
  9. mcp_ticketer/adapters/linear/queries.py +28 -7
  10. mcp_ticketer/adapters/linear/types.py +57 -50
  11. mcp_ticketer/adapters/linear.py +2 -2
  12. mcp_ticketer/cli/adapter_diagnostics.py +86 -51
  13. mcp_ticketer/cli/diagnostics.py +165 -72
  14. mcp_ticketer/cli/linear_commands.py +156 -113
  15. mcp_ticketer/cli/main.py +153 -82
  16. mcp_ticketer/cli/simple_health.py +73 -45
  17. mcp_ticketer/cli/utils.py +15 -10
  18. mcp_ticketer/core/config.py +23 -19
  19. mcp_ticketer/core/env_discovery.py +5 -4
  20. mcp_ticketer/core/env_loader.py +109 -86
  21. mcp_ticketer/core/exceptions.py +20 -18
  22. mcp_ticketer/core/models.py +9 -0
  23. mcp_ticketer/core/project_config.py +1 -1
  24. mcp_ticketer/mcp/server.py +294 -139
  25. mcp_ticketer/queue/health_monitor.py +152 -121
  26. mcp_ticketer/queue/manager.py +11 -4
  27. mcp_ticketer/queue/queue.py +15 -3
  28. mcp_ticketer/queue/run_worker.py +1 -1
  29. mcp_ticketer/queue/ticket_registry.py +190 -132
  30. mcp_ticketer/queue/worker.py +54 -25
  31. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/METADATA +1 -1
  32. mcp_ticketer-0.3.2.dist-info/RECORD +59 -0
  33. mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
  34. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/WHEEL +0 -0
  35. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/entry_points.txt +0 -0
  36. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/licenses/LICENSE +0 -0
  37. {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- Unified Environment Loading System for MCP Ticketer.
2
+ """Unified Environment Loading System for MCP Ticketer.
4
3
 
5
4
  This module provides a resilient environment loading system that:
6
5
  1. Supports multiple naming conventions for each configuration key
@@ -9,11 +8,11 @@ This module provides a resilient environment loading system that:
9
8
  4. Provides fallback mechanisms for different key aliases
10
9
  """
11
10
 
12
- import os
13
11
  import logging
14
- from pathlib import Path
15
- from typing import Dict, Any, List, Optional, Union
12
+ import os
16
13
  from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
@@ -21,6 +20,7 @@ logger = logging.getLogger(__name__)
21
20
  @dataclass
22
21
  class EnvKeyConfig:
23
22
  """Configuration for environment variable key aliases."""
23
+
24
24
  primary_key: str
25
25
  aliases: List[str]
26
26
  description: str
@@ -29,11 +29,10 @@ class EnvKeyConfig:
29
29
 
30
30
 
31
31
  class UnifiedEnvLoader:
32
- """
33
- Unified environment loader that handles multiple naming conventions
32
+ """Unified environment loader that handles multiple naming conventions
34
33
  and provides consistent environment loading across all contexts.
35
34
  """
36
-
35
+
37
36
  # Define key aliases for all adapters
38
37
  KEY_MAPPINGS = {
39
38
  # Linear adapter keys
@@ -41,93 +40,97 @@ class UnifiedEnvLoader:
41
40
  primary_key="LINEAR_API_KEY",
42
41
  aliases=["LINEAR_TOKEN", "LINEAR_ACCESS_TOKEN", "LINEAR_AUTH_TOKEN"],
43
42
  description="Linear API key",
44
- required=True
43
+ required=True,
45
44
  ),
46
45
  "linear_team_id": EnvKeyConfig(
47
46
  primary_key="LINEAR_TEAM_ID",
48
47
  aliases=["LINEAR_TEAM_UUID", "LINEAR_TEAM_IDENTIFIER"],
49
48
  description="Linear team ID (UUID)",
50
- required=False
49
+ required=False,
51
50
  ),
52
51
  "linear_team_key": EnvKeyConfig(
53
52
  primary_key="LINEAR_TEAM_KEY",
54
53
  aliases=["LINEAR_TEAM_IDENTIFIER", "LINEAR_TEAM_NAME"],
55
54
  description="Linear team key (short name)",
56
- required=False
55
+ required=False,
57
56
  ),
58
-
59
57
  # JIRA adapter keys
60
58
  "jira_server": EnvKeyConfig(
61
59
  primary_key="JIRA_SERVER",
62
60
  aliases=["JIRA_URL", "JIRA_HOST", "JIRA_BASE_URL"],
63
61
  description="JIRA server URL",
64
- required=True
62
+ required=True,
65
63
  ),
66
64
  "jira_email": EnvKeyConfig(
67
65
  primary_key="JIRA_EMAIL",
68
66
  aliases=["JIRA_USER", "JIRA_USERNAME", "JIRA_ACCESS_USER"],
69
67
  description="JIRA user email",
70
- required=True
68
+ required=True,
71
69
  ),
72
70
  "jira_api_token": EnvKeyConfig(
73
71
  primary_key="JIRA_API_TOKEN",
74
- aliases=["JIRA_TOKEN", "JIRA_ACCESS_TOKEN", "JIRA_AUTH_TOKEN", "JIRA_PASSWORD"],
72
+ aliases=[
73
+ "JIRA_TOKEN",
74
+ "JIRA_ACCESS_TOKEN",
75
+ "JIRA_AUTH_TOKEN",
76
+ "JIRA_PASSWORD",
77
+ ],
75
78
  description="JIRA API token",
76
- required=True
79
+ required=True,
77
80
  ),
78
81
  "jira_project_key": EnvKeyConfig(
79
82
  primary_key="JIRA_PROJECT_KEY",
80
83
  aliases=["JIRA_PROJECT", "JIRA_PROJECT_ID"],
81
84
  description="JIRA project key",
82
- required=False
85
+ required=False,
83
86
  ),
84
-
85
87
  # GitHub adapter keys
86
88
  "github_token": EnvKeyConfig(
87
89
  primary_key="GITHUB_TOKEN",
88
90
  aliases=["GITHUB_ACCESS_TOKEN", "GITHUB_API_TOKEN", "GITHUB_AUTH_TOKEN"],
89
91
  description="GitHub access token",
90
- required=True
92
+ required=True,
91
93
  ),
92
94
  "github_owner": EnvKeyConfig(
93
95
  primary_key="GITHUB_OWNER",
94
96
  aliases=["GITHUB_USER", "GITHUB_USERNAME", "GITHUB_ORG"],
95
97
  description="GitHub repository owner",
96
- required=True
98
+ required=True,
97
99
  ),
98
100
  "github_repo": EnvKeyConfig(
99
101
  primary_key="GITHUB_REPO",
100
102
  aliases=["GITHUB_REPOSITORY", "GITHUB_REPO_NAME"],
101
103
  description="GitHub repository name",
102
- required=True
104
+ required=True,
103
105
  ),
104
106
  }
105
-
107
+
106
108
  def __init__(self, project_root: Optional[Path] = None):
107
109
  """Initialize the environment loader.
108
-
110
+
109
111
  Args:
110
112
  project_root: Project root directory. If None, will auto-detect.
113
+
111
114
  """
112
115
  self.project_root = project_root or self._find_project_root()
113
116
  self._env_cache: Dict[str, str] = {}
114
117
  self._load_env_files()
115
-
118
+
116
119
  def _find_project_root(self) -> Path:
117
120
  """Find the project root directory."""
118
121
  current = Path.cwd()
119
-
122
+
120
123
  # Look for common project indicators
121
124
  indicators = [".mcp-ticketer", ".git", "pyproject.toml", "setup.py"]
122
-
125
+
123
126
  while current != current.parent:
124
127
  if any((current / indicator).exists() for indicator in indicators):
125
128
  return current
126
129
  current = current.parent
127
-
130
+
128
131
  # Fallback to current directory
129
132
  return Path.cwd()
130
-
133
+
131
134
  def _load_env_files(self):
132
135
  """Load environment variables from .env files."""
133
136
  env_files = [
@@ -135,142 +138,160 @@ class UnifiedEnvLoader:
135
138
  self.project_root / ".env",
136
139
  Path.home() / ".mcp-ticketer" / ".env",
137
140
  ]
138
-
141
+
139
142
  for env_file in env_files:
140
143
  if env_file.exists():
141
144
  logger.debug(f"Loading environment from: {env_file}")
142
145
  self._load_env_file(env_file)
143
-
146
+
144
147
  def _load_env_file(self, env_file: Path):
145
148
  """Load variables from a single .env file."""
146
149
  try:
147
- with open(env_file, 'r') as f:
150
+ with open(env_file) as f:
148
151
  for line_num, line in enumerate(f, 1):
149
152
  line = line.strip()
150
-
153
+
151
154
  # Skip empty lines and comments
152
- if not line or line.startswith('#'):
155
+ if not line or line.startswith("#"):
153
156
  continue
154
-
157
+
155
158
  # Parse KEY=VALUE format
156
- if '=' in line:
157
- key, value = line.split('=', 1)
159
+ if "=" in line:
160
+ key, value = line.split("=", 1)
158
161
  key = key.strip()
159
162
  value = value.strip()
160
-
163
+
161
164
  # Remove quotes if present
162
165
  if value.startswith('"') and value.endswith('"'):
163
166
  value = value[1:-1]
164
167
  elif value.startswith("'") and value.endswith("'"):
165
168
  value = value[1:-1]
166
-
169
+
167
170
  # Only set if not already in environment
168
171
  if key not in os.environ:
169
172
  os.environ[key] = value
170
173
  self._env_cache[key] = value
171
174
  logger.debug(f"Loaded {key} from {env_file}")
172
-
175
+
173
176
  except Exception as e:
174
177
  logger.warning(f"Failed to load {env_file}: {e}")
175
-
176
- def get_value(self, config_key: str, config: Optional[Dict[str, Any]] = None) -> Optional[str]:
177
- """
178
- Get a configuration value using the key alias system.
179
-
178
+
179
+ def get_value(
180
+ self, config_key: str, config: Optional[Dict[str, Any]] = None
181
+ ) -> Optional[str]:
182
+ """Get a configuration value using the key alias system.
183
+
180
184
  Args:
181
185
  config_key: The configuration key (e.g., 'linear_api_key')
182
186
  config: Optional configuration dictionary to check first
183
-
187
+
184
188
  Returns:
185
189
  The value if found, None otherwise
190
+
186
191
  """
187
192
  if config_key not in self.KEY_MAPPINGS:
188
193
  logger.warning(f"Unknown configuration key: {config_key}")
189
194
  return None
190
-
195
+
191
196
  key_config = self.KEY_MAPPINGS[config_key]
192
-
197
+
193
198
  # 1. Check provided config dictionary first
194
199
  if config:
195
200
  # Check for the config key itself (without adapter prefix)
196
- simple_key = config_key.split('_', 1)[1] if '_' in config_key else config_key
201
+ simple_key = (
202
+ config_key.split("_", 1)[1] if "_" in config_key else config_key
203
+ )
197
204
  if simple_key in config:
198
205
  value = config[simple_key]
199
206
  if value:
200
207
  logger.debug(f"Found {config_key} in config as {simple_key}")
201
208
  return str(value)
202
-
209
+
203
210
  # 2. Check environment variables (primary key first, then aliases)
204
211
  all_keys = [key_config.primary_key] + key_config.aliases
205
-
212
+
206
213
  for env_key in all_keys:
207
214
  value = os.getenv(env_key)
208
215
  if value:
209
216
  logger.debug(f"Found {config_key} as {env_key}")
210
217
  return value
211
-
218
+
212
219
  # 3. Return default if available
213
220
  if key_config.default:
214
221
  logger.debug(f"Using default for {config_key}")
215
222
  return key_config.default
216
-
223
+
217
224
  # 4. Log if required key is missing
218
225
  if key_config.required:
219
- logger.warning(f"Required configuration key {config_key} not found. Tried: {all_keys}")
220
-
226
+ logger.warning(
227
+ f"Required configuration key {config_key} not found. Tried: {all_keys}"
228
+ )
229
+
221
230
  return None
222
-
223
- def get_adapter_config(self, adapter_name: str, base_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
224
- """
225
- Get complete configuration for an adapter with environment variable resolution.
226
-
231
+
232
+ def get_adapter_config(
233
+ self, adapter_name: str, base_config: Optional[Dict[str, Any]] = None
234
+ ) -> Dict[str, Any]:
235
+ """Get complete configuration for an adapter with environment variable resolution.
236
+
227
237
  Args:
228
238
  adapter_name: Name of the adapter ('linear', 'jira', 'github')
229
239
  base_config: Base configuration dictionary
230
-
240
+
231
241
  Returns:
232
242
  Complete configuration with environment variables resolved
243
+
233
244
  """
234
245
  config = base_config.copy() if base_config else {}
235
-
246
+
236
247
  # Get adapter-specific keys
237
- adapter_keys = [key for key in self.KEY_MAPPINGS.keys() if key.startswith(f"{adapter_name}_")]
238
-
248
+ adapter_keys = [
249
+ key
250
+ for key in self.KEY_MAPPINGS.keys()
251
+ if key.startswith(f"{adapter_name}_")
252
+ ]
253
+
239
254
  for config_key in adapter_keys:
240
255
  # Remove adapter prefix for the config key
241
- simple_key = config_key.split('_', 1)[1]
242
-
256
+ simple_key = config_key.split("_", 1)[1]
257
+
243
258
  # Only set if not already in config or if config value is empty
244
259
  if simple_key not in config or not config[simple_key]:
245
260
  value = self.get_value(config_key, config)
246
261
  if value:
247
262
  config[simple_key] = value
248
-
263
+
249
264
  return config
250
-
251
- def validate_adapter_config(self, adapter_name: str, config: Dict[str, Any]) -> List[str]:
252
- """
253
- Validate that all required configuration is present for an adapter.
254
-
265
+
266
+ def validate_adapter_config(
267
+ self, adapter_name: str, config: Dict[str, Any]
268
+ ) -> List[str]:
269
+ """Validate that all required configuration is present for an adapter.
270
+
255
271
  Args:
256
272
  adapter_name: Name of the adapter
257
273
  config: Configuration dictionary
258
-
274
+
259
275
  Returns:
260
276
  List of missing required keys (empty if all required keys are present)
277
+
261
278
  """
262
279
  missing_keys = []
263
- adapter_keys = [key for key in self.KEY_MAPPINGS.keys() if key.startswith(f"{adapter_name}_")]
264
-
280
+ adapter_keys = [
281
+ key
282
+ for key in self.KEY_MAPPINGS.keys()
283
+ if key.startswith(f"{adapter_name}_")
284
+ ]
285
+
265
286
  for config_key in adapter_keys:
266
287
  key_config = self.KEY_MAPPINGS[config_key]
267
288
  if key_config.required:
268
- simple_key = config_key.split('_', 1)[1]
289
+ simple_key = config_key.split("_", 1)[1]
269
290
  if simple_key not in config or not config[simple_key]:
270
291
  missing_keys.append(f"{simple_key} ({key_config.description})")
271
-
292
+
272
293
  return missing_keys
273
-
294
+
274
295
  def get_debug_info(self) -> Dict[str, Any]:
275
296
  """Get debug information about environment loading."""
276
297
  return {
@@ -297,29 +318,31 @@ def get_env_loader() -> UnifiedEnvLoader:
297
318
  return _env_loader
298
319
 
299
320
 
300
- def load_adapter_config(adapter_name: str, base_config: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
301
- """
302
- Convenience function to load adapter configuration with environment variables.
303
-
321
+ def load_adapter_config(
322
+ adapter_name: str, base_config: Optional[Dict[str, Any]] = None
323
+ ) -> Dict[str, Any]:
324
+ """Convenience function to load adapter configuration with environment variables.
325
+
304
326
  Args:
305
327
  adapter_name: Name of the adapter ('linear', 'jira', 'github')
306
328
  base_config: Base configuration dictionary
307
-
329
+
308
330
  Returns:
309
331
  Complete configuration with environment variables resolved
332
+
310
333
  """
311
334
  return get_env_loader().get_adapter_config(adapter_name, base_config)
312
335
 
313
336
 
314
337
  def validate_adapter_config(adapter_name: str, config: Dict[str, Any]) -> List[str]:
315
- """
316
- Convenience function to validate adapter configuration.
317
-
338
+ """Convenience function to validate adapter configuration.
339
+
318
340
  Args:
319
341
  adapter_name: Name of the adapter
320
342
  config: Configuration dictionary
321
-
343
+
322
344
  Returns:
323
345
  List of missing required keys (empty if all required keys are present)
346
+
324
347
  """
325
348
  return get_env_loader().validate_adapter_config(adapter_name, config)
@@ -9,6 +9,7 @@ from .models import TicketState
9
9
 
10
10
  class MCPTicketerError(Exception):
11
11
  """Base exception for MCP Ticketer."""
12
+
12
13
  pass
13
14
 
14
15
 
@@ -19,14 +20,15 @@ class AdapterError(MCPTicketerError):
19
20
  self,
20
21
  message: str,
21
22
  adapter_name: str,
22
- original_error: Optional[Exception] = None
23
+ original_error: Optional[Exception] = None,
23
24
  ):
24
25
  """Initialize adapter error.
25
-
26
+
26
27
  Args:
27
28
  message: Error message
28
29
  adapter_name: Name of the adapter that raised the error
29
30
  original_error: Original exception that caused this error
31
+
30
32
  """
31
33
  super().__init__(message)
32
34
  self.adapter_name = adapter_name
@@ -42,6 +44,7 @@ class AdapterError(MCPTicketerError):
42
44
 
43
45
  class AuthenticationError(AdapterError):
44
46
  """Authentication failed with external service."""
47
+
45
48
  pass
46
49
 
47
50
 
@@ -53,15 +56,16 @@ class RateLimitError(AdapterError):
53
56
  message: str,
54
57
  adapter_name: str,
55
58
  retry_after: Optional[int] = None,
56
- original_error: Optional[Exception] = None
59
+ original_error: Optional[Exception] = None,
57
60
  ):
58
61
  """Initialize rate limit error.
59
-
62
+
60
63
  Args:
61
64
  message: Error message
62
65
  adapter_name: Name of the adapter
63
66
  retry_after: Seconds to wait before retrying
64
67
  original_error: Original exception
68
+
65
69
  """
66
70
  super().__init__(message, adapter_name, original_error)
67
71
  self.retry_after = retry_after
@@ -70,18 +74,14 @@ class RateLimitError(AdapterError):
70
74
  class ValidationError(MCPTicketerError):
71
75
  """Data validation error."""
72
76
 
73
- def __init__(
74
- self,
75
- message: str,
76
- field: Optional[str] = None,
77
- value: Any = None
78
- ):
77
+ def __init__(self, message: str, field: Optional[str] = None, value: Any = None):
79
78
  """Initialize validation error.
80
-
79
+
81
80
  Args:
82
81
  message: Error message
83
82
  field: Field that failed validation
84
83
  value: Value that failed validation
84
+
85
85
  """
86
86
  super().__init__(message)
87
87
  self.field = field
@@ -99,29 +99,27 @@ class ValidationError(MCPTicketerError):
99
99
 
100
100
  class ConfigurationError(MCPTicketerError):
101
101
  """Configuration error."""
102
+
102
103
  pass
103
104
 
104
105
 
105
106
  class CacheError(MCPTicketerError):
106
107
  """Cache operation error."""
108
+
107
109
  pass
108
110
 
109
111
 
110
112
  class StateTransitionError(MCPTicketerError):
111
113
  """Invalid state transition."""
112
114
 
113
- def __init__(
114
- self,
115
- message: str,
116
- from_state: TicketState,
117
- to_state: TicketState
118
- ):
115
+ def __init__(self, message: str, from_state: TicketState, to_state: TicketState):
119
116
  """Initialize state transition error.
120
-
117
+
121
118
  Args:
122
119
  message: Error message
123
120
  from_state: Current state
124
121
  to_state: Target state
122
+
125
123
  """
126
124
  super().__init__(message)
127
125
  self.from_state = from_state
@@ -134,19 +132,23 @@ class StateTransitionError(MCPTicketerError):
134
132
 
135
133
  class NetworkError(AdapterError):
136
134
  """Network-related error."""
135
+
137
136
  pass
138
137
 
139
138
 
140
139
  class TimeoutError(AdapterError):
141
140
  """Request timeout error."""
141
+
142
142
  pass
143
143
 
144
144
 
145
145
  class NotFoundError(AdapterError):
146
146
  """Resource not found error."""
147
+
147
148
  pass
148
149
 
149
150
 
150
151
  class PermissionError(AdapterError):
151
152
  """Permission denied error."""
153
+
152
154
  pass
@@ -20,6 +20,7 @@ Example:
20
20
  ... state=TicketState.IN_PROGRESS
21
21
  ... )
22
22
  >>> print(task.model_dump_json())
23
+
23
24
  """
24
25
 
25
26
  from datetime import datetime
@@ -42,6 +43,7 @@ class Priority(str, Enum):
42
43
  MEDIUM: Standard priority, default for most work
43
44
  HIGH: High priority, should be addressed soon
44
45
  CRITICAL: Critical priority, urgent work requiring immediate attention
46
+
45
47
  """
46
48
 
47
49
  LOW = "low"
@@ -66,6 +68,7 @@ class TicketType(str, Enum):
66
68
  ISSUE: Standard work items, the primary unit of work
67
69
  TASK: Sub-work items, smaller pieces of an issue
68
70
  SUBTASK: Alias for TASK for backward compatibility
71
+
69
72
  """
70
73
 
71
74
  EPIC = "epic" # Strategic level (Projects in Linear, Milestones in GitHub)
@@ -101,6 +104,7 @@ class TicketState(str, Enum):
101
104
  WAITING: Work is paused waiting for external dependency
102
105
  BLOCKED: Work is blocked by an impediment
103
106
  CLOSED: Final state, work is closed/archived
107
+
104
108
  """
105
109
 
106
110
  OPEN = "open"
@@ -121,6 +125,7 @@ class TicketState(str, Enum):
121
125
 
122
126
  Note:
123
127
  CLOSED is a terminal state with no valid transitions
128
+
124
129
  """
125
130
  return {
126
131
  cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
@@ -151,6 +156,7 @@ class TicketState(str, Enum):
151
156
  True
152
157
  >>> state.can_transition_to(TicketState.DONE)
153
158
  False
159
+
154
160
  """
155
161
  return target.value in self.valid_transitions().get(self, [])
156
162
 
@@ -183,6 +189,7 @@ class BaseTicket(BaseModel):
183
189
  ... tags=["bug", "authentication"]
184
190
  ... )
185
191
  >>> ticket.state = TicketState.IN_PROGRESS
192
+
186
193
  """
187
194
 
188
195
  model_config = ConfigDict(use_enum_values=True)
@@ -228,6 +235,7 @@ class Epic(BaseTicket):
228
235
  ... priority=Priority.HIGH
229
236
  ... )
230
237
  >>> epic.child_issues = ["ISSUE-123", "ISSUE-124"]
238
+
231
239
  """
232
240
 
233
241
  ticket_type: TicketType = Field(
@@ -245,6 +253,7 @@ class Epic(BaseTicket):
245
253
 
246
254
  Returns:
247
255
  Empty list (epics have no hierarchy constraints)
256
+
248
257
  """
249
258
  # Epics don't have parents in our hierarchy
250
259
  return []
@@ -14,7 +14,7 @@ import os
14
14
  from dataclasses import asdict, dataclass, field
15
15
  from enum import Enum
16
16
  from pathlib import Path
17
- from typing import Any, Optional, TYPE_CHECKING
17
+ from typing import TYPE_CHECKING, Any, Optional
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from .env_discovery import DiscoveryResult