mcp-ticketer 0.4.1__py3-none-any.whl → 0.4.3__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 (56) hide show
  1. mcp_ticketer/__init__.py +3 -12
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +243 -11
  4. mcp_ticketer/adapters/github.py +15 -14
  5. mcp_ticketer/adapters/hybrid.py +11 -11
  6. mcp_ticketer/adapters/jira.py +22 -25
  7. mcp_ticketer/adapters/linear/adapter.py +9 -21
  8. mcp_ticketer/adapters/linear/client.py +2 -1
  9. mcp_ticketer/adapters/linear/mappers.py +2 -1
  10. mcp_ticketer/cache/memory.py +6 -5
  11. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  12. mcp_ticketer/cli/auggie_configure.py +66 -0
  13. mcp_ticketer/cli/codex_configure.py +70 -2
  14. mcp_ticketer/cli/configure.py +7 -14
  15. mcp_ticketer/cli/diagnostics.py +2 -2
  16. mcp_ticketer/cli/discover.py +6 -11
  17. mcp_ticketer/cli/gemini_configure.py +68 -2
  18. mcp_ticketer/cli/linear_commands.py +6 -7
  19. mcp_ticketer/cli/main.py +341 -203
  20. mcp_ticketer/cli/mcp_configure.py +61 -2
  21. mcp_ticketer/cli/ticket_commands.py +27 -30
  22. mcp_ticketer/cli/utils.py +23 -22
  23. mcp_ticketer/core/__init__.py +3 -1
  24. mcp_ticketer/core/adapter.py +82 -13
  25. mcp_ticketer/core/config.py +27 -29
  26. mcp_ticketer/core/env_discovery.py +10 -10
  27. mcp_ticketer/core/env_loader.py +8 -8
  28. mcp_ticketer/core/http_client.py +16 -16
  29. mcp_ticketer/core/mappers.py +10 -10
  30. mcp_ticketer/core/models.py +50 -20
  31. mcp_ticketer/core/project_config.py +40 -34
  32. mcp_ticketer/core/registry.py +2 -2
  33. mcp_ticketer/mcp/dto.py +32 -32
  34. mcp_ticketer/mcp/response_builder.py +2 -2
  35. mcp_ticketer/mcp/server.py +17 -37
  36. mcp_ticketer/mcp/server_sdk.py +93 -0
  37. mcp_ticketer/mcp/tools/__init__.py +36 -0
  38. mcp_ticketer/mcp/tools/attachment_tools.py +179 -0
  39. mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
  40. mcp_ticketer/mcp/tools/comment_tools.py +90 -0
  41. mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
  42. mcp_ticketer/mcp/tools/pr_tools.py +154 -0
  43. mcp_ticketer/mcp/tools/search_tools.py +206 -0
  44. mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
  45. mcp_ticketer/queue/health_monitor.py +4 -4
  46. mcp_ticketer/queue/manager.py +2 -2
  47. mcp_ticketer/queue/queue.py +16 -16
  48. mcp_ticketer/queue/ticket_registry.py +7 -7
  49. mcp_ticketer/queue/worker.py +2 -2
  50. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/METADATA +90 -17
  51. mcp_ticketer-0.4.3.dist-info/RECORD +73 -0
  52. mcp_ticketer-0.4.1.dist-info/RECORD +0 -64
  53. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/WHEEL +0 -0
  54. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/entry_points.txt +0 -0
  55. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/licenses/LICENSE +0 -0
  56. {mcp_ticketer-0.4.1.dist-info → mcp_ticketer-0.4.3.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ import os
6
6
  from enum import Enum
7
7
  from functools import lru_cache
8
8
  from pathlib import Path
9
- from typing import Any, Optional, Union
9
+ from typing import Any, Optional
10
10
 
11
11
  import yaml
12
12
  from pydantic import BaseModel, Field, field_validator, model_validator
@@ -27,23 +27,23 @@ class BaseAdapterConfig(BaseModel):
27
27
  """Base configuration for all adapters."""
28
28
 
29
29
  type: AdapterType
30
- name: Optional[str] = None
30
+ name: str | None = None
31
31
  enabled: bool = True
32
32
  timeout: float = 30.0
33
33
  max_retries: int = 3
34
- rate_limit: Optional[dict[str, Any]] = None
34
+ rate_limit: dict[str, Any] | None = None
35
35
 
36
36
 
37
37
  class GitHubConfig(BaseAdapterConfig):
38
38
  """GitHub adapter configuration."""
39
39
 
40
40
  type: AdapterType = AdapterType.GITHUB
41
- token: Optional[str] = Field(None, env="GITHUB_TOKEN")
42
- owner: Optional[str] = Field(None, env="GITHUB_OWNER")
43
- repo: Optional[str] = Field(None, env="GITHUB_REPO")
41
+ token: str | None = Field(None, env="GITHUB_TOKEN")
42
+ owner: str | None = Field(None, env="GITHUB_OWNER")
43
+ repo: str | None = Field(None, env="GITHUB_REPO")
44
44
  api_url: str = "https://api.github.com"
45
45
  use_projects_v2: bool = False
46
- custom_priority_scheme: Optional[dict[str, list[str]]] = None
46
+ custom_priority_scheme: dict[str, list[str]] | None = None
47
47
 
48
48
  @field_validator("token", mode="before")
49
49
  @classmethod
@@ -77,10 +77,10 @@ class JiraConfig(BaseAdapterConfig):
77
77
  """JIRA adapter configuration."""
78
78
 
79
79
  type: AdapterType = AdapterType.JIRA
80
- server: Optional[str] = Field(None, env="JIRA_SERVER")
81
- email: Optional[str] = Field(None, env="JIRA_EMAIL")
82
- api_token: Optional[str] = Field(None, env="JIRA_API_TOKEN")
83
- project_key: Optional[str] = Field(None, env="JIRA_PROJECT_KEY")
80
+ server: str | None = Field(None, env="JIRA_SERVER")
81
+ email: str | None = Field(None, env="JIRA_EMAIL")
82
+ api_token: str | None = Field(None, env="JIRA_API_TOKEN")
83
+ project_key: str | None = Field(None, env="JIRA_PROJECT_KEY")
84
84
  cloud: bool = True
85
85
  verify_ssl: bool = True
86
86
 
@@ -116,10 +116,10 @@ class LinearConfig(BaseAdapterConfig):
116
116
  """Linear adapter configuration."""
117
117
 
118
118
  type: AdapterType = AdapterType.LINEAR
119
- api_key: Optional[str] = Field(None, env="LINEAR_API_KEY")
120
- workspace: Optional[str] = None
121
- team_key: Optional[str] = None # Short team key like "BTA"
122
- team_id: Optional[str] = None # UUID team identifier
119
+ api_key: str | None = Field(None, env="LINEAR_API_KEY")
120
+ workspace: str | None = None
121
+ team_key: str | None = None # Short team key like "BTA"
122
+ team_id: str | None = None # UUID team identifier
123
123
  api_url: str = "https://api.linear.app/graphql"
124
124
 
125
125
  @model_validator(mode="after")
@@ -150,7 +150,7 @@ class QueueConfig(BaseModel):
150
150
  """Queue configuration."""
151
151
 
152
152
  provider: str = "sqlite"
153
- connection_string: Optional[str] = None
153
+ connection_string: str | None = None
154
154
  batch_size: int = 10
155
155
  max_concurrent: int = 5
156
156
  retry_attempts: int = 3
@@ -162,7 +162,7 @@ class LoggingConfig(BaseModel):
162
162
 
163
163
  level: str = "INFO"
164
164
  format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
165
- file: Optional[str] = None
165
+ file: str | None = None
166
166
  max_size: str = "10MB"
167
167
  backup_count: int = 5
168
168
 
@@ -171,12 +171,12 @@ class AppConfig(BaseModel):
171
171
  """Main application configuration."""
172
172
 
173
173
  adapters: dict[
174
- str, Union[GitHubConfig, JiraConfig, LinearConfig, AITrackdownConfig]
174
+ str, GitHubConfig | JiraConfig | LinearConfig | AITrackdownConfig
175
175
  ] = {}
176
176
  queue: QueueConfig = QueueConfig()
177
177
  logging: LoggingConfig = LoggingConfig()
178
178
  cache_ttl: int = 300 # Cache TTL in seconds
179
- default_adapter: Optional[str] = None
179
+ default_adapter: str | None = None
180
180
 
181
181
  @model_validator(mode="after")
182
182
  def validate_adapters(self):
@@ -196,7 +196,7 @@ class AppConfig(BaseModel):
196
196
 
197
197
  return self
198
198
 
199
- def get_adapter_config(self, adapter_name: str) -> Optional[BaseAdapterConfig]:
199
+ def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
200
200
  """Get configuration for a specific adapter."""
201
201
  return self.adapters.get(adapter_name)
202
202
 
@@ -211,7 +211,7 @@ class ConfigurationManager:
211
211
  """Centralized configuration management with caching and validation."""
212
212
 
213
213
  _instance: Optional["ConfigurationManager"] = None
214
- _config: Optional[AppConfig] = None
214
+ _config: AppConfig | None = None
215
215
  _config_file_paths: list[Path] = []
216
216
 
217
217
  def __new__(cls) -> "ConfigurationManager":
@@ -266,7 +266,7 @@ class ConfigurationManager:
266
266
  logger.debug("No project-local config files found, will use defaults")
267
267
 
268
268
  @lru_cache(maxsize=1)
269
- def load_config(self, config_file: Optional[Union[str, Path]] = None) -> AppConfig:
269
+ def load_config(self, config_file: str | Path | None = None) -> AppConfig:
270
270
  """Load and validate configuration from file and environment.
271
271
 
272
272
  Args:
@@ -437,7 +437,7 @@ class ConfigurationManager:
437
437
  return self.load_config()
438
438
  return self._config
439
439
 
440
- def get_adapter_config(self, adapter_name: str) -> Optional[BaseAdapterConfig]:
440
+ def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
441
441
  """Get configuration for a specific adapter."""
442
442
  config = self.get_config()
443
443
  return config.get_adapter_config(adapter_name)
@@ -457,9 +457,7 @@ class ConfigurationManager:
457
457
  config = self.get_config()
458
458
  return config.logging
459
459
 
460
- def reload_config(
461
- self, config_file: Optional[Union[str, Path]] = None
462
- ) -> AppConfig:
460
+ def reload_config(self, config_file: str | Path | None = None) -> AppConfig:
463
461
  """Reload configuration from file."""
464
462
  # Clear cache
465
463
  self.load_config.cache_clear()
@@ -492,7 +490,7 @@ class ConfigurationManager:
492
490
  self._config_cache[key] = value
493
491
  return value
494
492
 
495
- def create_sample_config(self, output_path: Union[str, Path]) -> None:
493
+ def create_sample_config(self, output_path: str | Path) -> None:
496
494
  """Create a sample configuration file."""
497
495
  sample_config = {
498
496
  "adapters": {
@@ -539,11 +537,11 @@ def get_config() -> AppConfig:
539
537
  return config_manager.get_config()
540
538
 
541
539
 
542
- def get_adapter_config(adapter_name: str) -> Optional[BaseAdapterConfig]:
540
+ def get_adapter_config(adapter_name: str) -> BaseAdapterConfig | None:
543
541
  """Get configuration for a specific adapter."""
544
542
  return config_manager.get_adapter_config(adapter_name)
545
543
 
546
544
 
547
- def reload_config(config_file: Optional[Union[str, Path]] = None) -> AppConfig:
545
+ def reload_config(config_file: str | Path | None = None) -> AppConfig:
548
546
  """Reload the global configuration."""
549
547
  return config_manager.reload_config(config_file)
@@ -11,7 +11,7 @@ environment files, including:
11
11
  import logging
12
12
  from dataclasses import dataclass, field
13
13
  from pathlib import Path
14
- from typing import Any, Optional
14
+ from typing import Any
15
15
 
16
16
  from dotenv import dotenv_values
17
17
 
@@ -125,7 +125,7 @@ class DiscoveryResult:
125
125
  warnings: list[str] = field(default_factory=list)
126
126
  env_files_found: list[str] = field(default_factory=list)
127
127
 
128
- def get_primary_adapter(self) -> Optional[DiscoveredAdapter]:
128
+ def get_primary_adapter(self) -> DiscoveredAdapter | None:
129
129
  """Get the adapter with highest confidence and completeness."""
130
130
  if not self.adapters:
131
131
  return None
@@ -136,7 +136,7 @@ class DiscoveryResult:
136
136
  )
137
137
  return sorted_adapters[0]
138
138
 
139
- def get_adapter_by_type(self, adapter_type: str) -> Optional[DiscoveredAdapter]:
139
+ def get_adapter_by_type(self, adapter_type: str) -> DiscoveredAdapter | None:
140
140
  """Get discovered adapter by type."""
141
141
  for adapter in self.adapters:
142
142
  if adapter.adapter_type == adapter_type:
@@ -155,7 +155,7 @@ class EnvDiscovery:
155
155
  ".env.development",
156
156
  ]
157
157
 
158
- def __init__(self, project_path: Optional[Path] = None):
158
+ def __init__(self, project_path: Path | None = None):
159
159
  """Initialize discovery.
160
160
 
161
161
  Args:
@@ -255,7 +255,7 @@ class EnvDiscovery:
255
255
 
256
256
  def _find_key_value(
257
257
  self, env_vars: dict[str, str], patterns: list[str]
258
- ) -> Optional[str]:
258
+ ) -> str | None:
259
259
  """Find first matching key value from patterns.
260
260
 
261
261
  Args:
@@ -273,7 +273,7 @@ class EnvDiscovery:
273
273
 
274
274
  def _detect_linear(
275
275
  self, env_vars: dict[str, str], found_in: str
276
- ) -> Optional[DiscoveredAdapter]:
276
+ ) -> DiscoveredAdapter | None:
277
277
  """Detect Linear adapter configuration.
278
278
 
279
279
  Args:
@@ -327,7 +327,7 @@ class EnvDiscovery:
327
327
 
328
328
  def _detect_github(
329
329
  self, env_vars: dict[str, str], found_in: str
330
- ) -> Optional[DiscoveredAdapter]:
330
+ ) -> DiscoveredAdapter | None:
331
331
  """Detect GitHub adapter configuration.
332
332
 
333
333
  Args:
@@ -386,7 +386,7 @@ class EnvDiscovery:
386
386
 
387
387
  def _detect_jira(
388
388
  self, env_vars: dict[str, str], found_in: str
389
- ) -> Optional[DiscoveredAdapter]:
389
+ ) -> DiscoveredAdapter | None:
390
390
  """Detect JIRA adapter configuration.
391
391
 
392
392
  Args:
@@ -442,7 +442,7 @@ class EnvDiscovery:
442
442
 
443
443
  def _detect_aitrackdown(
444
444
  self, env_vars: dict[str, str], found_in: str
445
- ) -> Optional[DiscoveredAdapter]:
445
+ ) -> DiscoveredAdapter | None:
446
446
  """Detect AITrackdown adapter configuration.
447
447
 
448
448
  Args:
@@ -622,7 +622,7 @@ class EnvDiscovery:
622
622
  return warnings
623
623
 
624
624
 
625
- def discover_config(project_path: Optional[Path] = None) -> DiscoveryResult:
625
+ def discover_config(project_path: Path | None = None) -> DiscoveryResult:
626
626
  """Convenience function to discover configuration.
627
627
 
628
628
  Args:
@@ -12,7 +12,7 @@ import logging
12
12
  import os
13
13
  from dataclasses import dataclass
14
14
  from pathlib import Path
15
- from typing import Any, Optional
15
+ from typing import Any
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
@@ -25,7 +25,7 @@ class EnvKeyConfig:
25
25
  aliases: list[str]
26
26
  description: str
27
27
  required: bool = False
28
- default: Optional[str] = None
28
+ default: str | None = None
29
29
 
30
30
 
31
31
  class UnifiedEnvLoader:
@@ -105,7 +105,7 @@ class UnifiedEnvLoader:
105
105
  ),
106
106
  }
107
107
 
108
- def __init__(self, project_root: Optional[Path] = None):
108
+ def __init__(self, project_root: Path | None = None):
109
109
  """Initialize the environment loader.
110
110
 
111
111
  Args:
@@ -177,8 +177,8 @@ class UnifiedEnvLoader:
177
177
  logger.warning(f"Failed to load {env_file}: {e}")
178
178
 
179
179
  def get_value(
180
- self, config_key: str, config: Optional[dict[str, Any]] = None
181
- ) -> Optional[str]:
180
+ self, config_key: str, config: dict[str, Any] | None = None
181
+ ) -> str | None:
182
182
  """Get a configuration value using the key alias system.
183
183
 
184
184
  Args:
@@ -230,7 +230,7 @@ class UnifiedEnvLoader:
230
230
  return None
231
231
 
232
232
  def get_adapter_config(
233
- self, adapter_name: str, base_config: Optional[dict[str, Any]] = None
233
+ self, adapter_name: str, base_config: dict[str, Any] | None = None
234
234
  ) -> dict[str, Any]:
235
235
  """Get complete configuration for an adapter with environment variable resolution.
236
236
 
@@ -307,7 +307,7 @@ class UnifiedEnvLoader:
307
307
 
308
308
 
309
309
  # Global instance
310
- _env_loader: Optional[UnifiedEnvLoader] = None
310
+ _env_loader: UnifiedEnvLoader | None = None
311
311
 
312
312
 
313
313
  def get_env_loader() -> UnifiedEnvLoader:
@@ -319,7 +319,7 @@ def get_env_loader() -> UnifiedEnvLoader:
319
319
 
320
320
 
321
321
  def load_adapter_config(
322
- adapter_name: str, base_config: Optional[dict[str, Any]] = None
322
+ adapter_name: str, base_config: dict[str, Any] | None = None
323
323
  ) -> dict[str, Any]:
324
324
  """Convenience function to load adapter configuration with environment variables.
325
325
 
@@ -4,7 +4,7 @@ import asyncio
4
4
  import logging
5
5
  import time
6
6
  from enum import Enum
7
- from typing import Any, Optional, Union
7
+ from typing import Any
8
8
 
9
9
  import httpx
10
10
  from httpx import AsyncClient, TimeoutException
@@ -32,8 +32,8 @@ class RetryConfig:
32
32
  max_delay: float = 60.0,
33
33
  exponential_base: float = 2.0,
34
34
  jitter: bool = True,
35
- retry_on_status: Optional[list[int]] = None,
36
- retry_on_exceptions: Optional[list[type]] = None,
35
+ retry_on_status: list[int] | None = None,
36
+ retry_on_exceptions: list[type] | None = None,
37
37
  ):
38
38
  self.max_retries = max_retries
39
39
  self.initial_delay = initial_delay
@@ -94,11 +94,11 @@ class BaseHTTPClient:
94
94
  def __init__(
95
95
  self,
96
96
  base_url: str,
97
- headers: Optional[dict[str, str]] = None,
98
- auth: Optional[Union[httpx.Auth, tuple]] = None,
97
+ headers: dict[str, str] | None = None,
98
+ auth: httpx.Auth | tuple | None = None,
99
99
  timeout: float = 30.0,
100
- retry_config: Optional[RetryConfig] = None,
101
- rate_limiter: Optional[RateLimiter] = None,
100
+ retry_config: RetryConfig | None = None,
101
+ rate_limiter: RateLimiter | None = None,
102
102
  verify_ssl: bool = True,
103
103
  follow_redirects: bool = True,
104
104
  ):
@@ -132,7 +132,7 @@ class BaseHTTPClient:
132
132
  "errors": 0,
133
133
  }
134
134
 
135
- self._client: Optional[AsyncClient] = None
135
+ self._client: AsyncClient | None = None
136
136
 
137
137
  async def _get_client(self) -> AsyncClient:
138
138
  """Get or create HTTP client instance."""
@@ -148,7 +148,7 @@ class BaseHTTPClient:
148
148
  return self._client
149
149
 
150
150
  async def _calculate_delay(
151
- self, attempt: int, response: Optional[httpx.Response] = None
151
+ self, attempt: int, response: httpx.Response | None = None
152
152
  ) -> float:
153
153
  """Calculate delay for retry attempt."""
154
154
  if response and response.status_code == 429:
@@ -178,7 +178,7 @@ class BaseHTTPClient:
178
178
  def _should_retry(
179
179
  self,
180
180
  exception: Exception,
181
- response: Optional[httpx.Response] = None,
181
+ response: httpx.Response | None = None,
182
182
  attempt: int = 1,
183
183
  ) -> bool:
184
184
  """Determine if request should be retried."""
@@ -198,13 +198,13 @@ class BaseHTTPClient:
198
198
 
199
199
  async def request(
200
200
  self,
201
- method: Union[HTTPMethod, str],
201
+ method: HTTPMethod | str,
202
202
  endpoint: str,
203
- data: Optional[dict[str, Any]] = None,
204
- json: Optional[dict[str, Any]] = None,
205
- params: Optional[dict[str, Any]] = None,
206
- headers: Optional[dict[str, str]] = None,
207
- timeout: Optional[float] = None,
203
+ data: dict[str, Any] | None = None,
204
+ json: dict[str, Any] | None = None,
205
+ params: dict[str, Any] | None = None,
206
+ headers: dict[str, str] | None = None,
207
+ timeout: float | None = None,
208
208
  retry_count: int = 0,
209
209
  **kwargs,
210
210
  ) -> httpx.Response:
@@ -3,7 +3,7 @@
3
3
  import logging
4
4
  from abc import ABC, abstractmethod
5
5
  from functools import lru_cache
6
- from typing import Any, Generic, Optional, TypeVar
6
+ from typing import Any, Generic, TypeVar
7
7
 
8
8
  from .models import Priority, TicketState
9
9
 
@@ -27,11 +27,11 @@ class BiDirectionalDict(Generic[T, U]):
27
27
  self._reverse: dict[U, T] = {v: k for k, v in mapping.items()}
28
28
  self._cache: dict[str, Any] = {}
29
29
 
30
- def get_forward(self, key: T, default: Optional[U] = None) -> Optional[U]:
30
+ def get_forward(self, key: T, default: U | None = None) -> U | None:
31
31
  """Get value by forward key."""
32
32
  return self._forward.get(key, default)
33
33
 
34
- def get_reverse(self, key: U, default: Optional[T] = None) -> Optional[T]:
34
+ def get_reverse(self, key: U, default: T | None = None) -> T | None:
35
35
  """Get value by reverse key."""
36
36
  return self._reverse.get(key, default)
37
37
 
@@ -83,7 +83,7 @@ class StateMapper(BaseMapper):
83
83
  """Universal state mapping utility."""
84
84
 
85
85
  def __init__(
86
- self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
86
+ self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
87
87
  ):
88
88
  """Initialize state mapper.
89
89
 
@@ -95,7 +95,7 @@ class StateMapper(BaseMapper):
95
95
  super().__init__()
96
96
  self.adapter_type = adapter_type
97
97
  self.custom_mappings = custom_mappings or {}
98
- self._mapping: Optional[BiDirectionalDict] = None
98
+ self._mapping: BiDirectionalDict | None = None
99
99
 
100
100
  @lru_cache(maxsize=1)
101
101
  def get_mapping(self) -> BiDirectionalDict:
@@ -229,7 +229,7 @@ class StateMapper(BaseMapper):
229
229
  """Check if adapter uses labels for extended states."""
230
230
  return self.adapter_type in ["github", "linear"]
231
231
 
232
- def get_state_label(self, state: TicketState) -> Optional[str]:
232
+ def get_state_label(self, state: TicketState) -> str | None:
233
233
  """Get label name for extended states that require labels.
234
234
 
235
235
  Args:
@@ -258,7 +258,7 @@ class PriorityMapper(BaseMapper):
258
258
  """Universal priority mapping utility."""
259
259
 
260
260
  def __init__(
261
- self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
261
+ self, adapter_type: str, custom_mappings: dict[str, Any] | None = None
262
262
  ):
263
263
  """Initialize priority mapper.
264
264
 
@@ -270,7 +270,7 @@ class PriorityMapper(BaseMapper):
270
270
  super().__init__()
271
271
  self.adapter_type = adapter_type
272
272
  self.custom_mappings = custom_mappings or {}
273
- self._mapping: Optional[BiDirectionalDict] = None
273
+ self._mapping: BiDirectionalDict | None = None
274
274
 
275
275
  @lru_cache(maxsize=1)
276
276
  def get_mapping(self) -> BiDirectionalDict:
@@ -483,7 +483,7 @@ class MapperRegistry:
483
483
 
484
484
  @classmethod
485
485
  def get_state_mapper(
486
- cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
486
+ cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
487
487
  ) -> StateMapper:
488
488
  """Get or create state mapper for adapter type.
489
489
 
@@ -502,7 +502,7 @@ class MapperRegistry:
502
502
 
503
503
  @classmethod
504
504
  def get_priority_mapper(
505
- cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
505
+ cls, adapter_type: str, custom_mappings: dict[str, Any] | None = None
506
506
  ) -> PriorityMapper:
507
507
  """Get or create priority mapper for adapter type.
508
508
 
@@ -25,7 +25,7 @@ Example:
25
25
 
26
26
  from datetime import datetime
27
27
  from enum import Enum
28
- from typing import Any, Optional
28
+ from typing import Any
29
29
 
30
30
  from pydantic import BaseModel, ConfigDict, Field
31
31
 
@@ -194,14 +194,14 @@ class BaseTicket(BaseModel):
194
194
 
195
195
  model_config = ConfigDict(use_enum_values=True)
196
196
 
197
- id: Optional[str] = Field(None, description="Unique identifier")
197
+ id: str | None = Field(None, description="Unique identifier")
198
198
  title: str = Field(..., min_length=1, description="Ticket title")
199
- description: Optional[str] = Field(None, description="Detailed description")
199
+ description: str | None = Field(None, description="Detailed description")
200
200
  state: TicketState = Field(TicketState.OPEN, description="Current state")
201
201
  priority: Priority = Field(Priority.MEDIUM, description="Priority level")
202
202
  tags: list[str] = Field(default_factory=list, description="Tags/labels")
203
- created_at: Optional[datetime] = Field(None, description="Creation timestamp")
204
- updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
203
+ created_at: datetime | None = Field(None, description="Creation timestamp")
204
+ updated_at: datetime | None = Field(None, description="Last update timestamp")
205
205
 
206
206
  # Metadata for field mapping to different systems
207
207
  metadata: dict[str, Any] = Field(
@@ -270,20 +270,20 @@ class Task(BaseTicket):
270
270
  ticket_type: TicketType = Field(
271
271
  default=TicketType.ISSUE, description="Ticket type in hierarchy"
272
272
  )
273
- parent_issue: Optional[str] = Field(None, description="Parent issue ID (for tasks)")
274
- parent_epic: Optional[str] = Field(
273
+ parent_issue: str | None = Field(None, description="Parent issue ID (for tasks)")
274
+ parent_epic: str | None = Field(
275
275
  None,
276
276
  description="Parent epic/project ID (for issues). Synonym: 'project'",
277
277
  )
278
- assignee: Optional[str] = Field(None, description="Assigned user")
278
+ assignee: str | None = Field(None, description="Assigned user")
279
279
  children: list[str] = Field(default_factory=list, description="Child task IDs")
280
280
 
281
281
  # Additional fields common across systems
282
- estimated_hours: Optional[float] = Field(None, description="Time estimate")
283
- actual_hours: Optional[float] = Field(None, description="Actual time spent")
282
+ estimated_hours: float | None = Field(None, description="Time estimate")
283
+ actual_hours: float | None = Field(None, description="Actual time spent")
284
284
 
285
285
  @property
286
- def project(self) -> Optional[str]:
286
+ def project(self) -> str | None:
287
287
  """Synonym for parent_epic.
288
288
 
289
289
  Returns:
@@ -293,7 +293,7 @@ class Task(BaseTicket):
293
293
  return self.parent_epic
294
294
 
295
295
  @project.setter
296
- def project(self, value: Optional[str]) -> None:
296
+ def project(self, value: str | None) -> None:
297
297
  """Set parent_epic via project synonym.
298
298
 
299
299
  Args:
@@ -345,23 +345,53 @@ class Comment(BaseModel):
345
345
 
346
346
  model_config = ConfigDict(use_enum_values=True)
347
347
 
348
- id: Optional[str] = Field(None, description="Comment ID")
348
+ id: str | None = Field(None, description="Comment ID")
349
349
  ticket_id: str = Field(..., description="Parent ticket ID")
350
- author: Optional[str] = Field(None, description="Comment author")
350
+ author: str | None = Field(None, description="Comment author")
351
351
  content: str = Field(..., min_length=1, description="Comment text")
352
- created_at: Optional[datetime] = Field(None, description="Creation timestamp")
352
+ created_at: datetime | None = Field(None, description="Creation timestamp")
353
353
  metadata: dict[str, Any] = Field(
354
354
  default_factory=dict, description="System-specific metadata"
355
355
  )
356
356
 
357
357
 
358
+ class Attachment(BaseModel):
359
+ """File attachment metadata for tickets.
360
+
361
+ Represents a file attached to a ticket across all adapters.
362
+ Each adapter maps its native attachment format to this model.
363
+ """
364
+
365
+ model_config = ConfigDict(use_enum_values=True)
366
+
367
+ id: str | None = Field(None, description="Attachment unique identifier")
368
+ ticket_id: str = Field(..., description="Parent ticket identifier")
369
+ filename: str = Field(..., description="Original filename")
370
+ url: str | None = Field(None, description="Download URL or file path")
371
+ content_type: str | None = Field(
372
+ None, description="MIME type (e.g., 'application/pdf', 'image/png')"
373
+ )
374
+ size_bytes: int | None = Field(None, description="File size in bytes")
375
+ created_at: datetime | None = Field(None, description="Upload timestamp")
376
+ created_by: str | None = Field(None, description="User who uploaded the attachment")
377
+ description: str | None = Field(None, description="Attachment description or notes")
378
+ metadata: dict[str, Any] = Field(
379
+ default_factory=dict, description="Adapter-specific attachment metadata"
380
+ )
381
+
382
+ def __str__(self) -> str:
383
+ """String representation showing filename and size."""
384
+ size_str = f" ({self.size_bytes} bytes)" if self.size_bytes else ""
385
+ return f"Attachment({self.filename}{size_str})"
386
+
387
+
358
388
  class SearchQuery(BaseModel):
359
389
  """Search query parameters."""
360
390
 
361
- query: Optional[str] = Field(None, description="Text search query")
362
- state: Optional[TicketState] = Field(None, description="Filter by state")
363
- priority: Optional[Priority] = Field(None, description="Filter by priority")
364
- tags: Optional[list[str]] = Field(None, description="Filter by tags")
365
- assignee: Optional[str] = Field(None, description="Filter by assignee")
391
+ query: str | None = Field(None, description="Text search query")
392
+ state: TicketState | None = Field(None, description="Filter by state")
393
+ priority: Priority | None = Field(None, description="Filter by priority")
394
+ tags: list[str] | None = Field(None, description="Filter by tags")
395
+ assignee: str | None = Field(None, description="Filter by assignee")
366
396
  limit: int = Field(10, gt=0, le=100, description="Maximum results")
367
397
  offset: int = Field(0, ge=0, description="Result offset for pagination")