mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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 (42) hide show
  1. mcp_ticketer/__init__.py +7 -7
  2. mcp_ticketer/__version__.py +4 -2
  3. mcp_ticketer/adapters/__init__.py +4 -4
  4. mcp_ticketer/adapters/aitrackdown.py +66 -49
  5. mcp_ticketer/adapters/github.py +192 -125
  6. mcp_ticketer/adapters/hybrid.py +99 -53
  7. mcp_ticketer/adapters/jira.py +161 -151
  8. mcp_ticketer/adapters/linear.py +396 -246
  9. mcp_ticketer/cache/__init__.py +1 -1
  10. mcp_ticketer/cache/memory.py +15 -16
  11. mcp_ticketer/cli/__init__.py +1 -1
  12. mcp_ticketer/cli/configure.py +69 -93
  13. mcp_ticketer/cli/discover.py +43 -35
  14. mcp_ticketer/cli/main.py +283 -298
  15. mcp_ticketer/cli/mcp_configure.py +39 -15
  16. mcp_ticketer/cli/migrate_config.py +11 -13
  17. mcp_ticketer/cli/queue_commands.py +21 -58
  18. mcp_ticketer/cli/utils.py +121 -66
  19. mcp_ticketer/core/__init__.py +2 -2
  20. mcp_ticketer/core/adapter.py +46 -39
  21. mcp_ticketer/core/config.py +128 -92
  22. mcp_ticketer/core/env_discovery.py +69 -37
  23. mcp_ticketer/core/http_client.py +57 -40
  24. mcp_ticketer/core/mappers.py +98 -54
  25. mcp_ticketer/core/models.py +38 -24
  26. mcp_ticketer/core/project_config.py +145 -80
  27. mcp_ticketer/core/registry.py +16 -16
  28. mcp_ticketer/mcp/__init__.py +1 -1
  29. mcp_ticketer/mcp/server.py +199 -145
  30. mcp_ticketer/queue/__init__.py +2 -2
  31. mcp_ticketer/queue/__main__.py +1 -1
  32. mcp_ticketer/queue/manager.py +30 -26
  33. mcp_ticketer/queue/queue.py +147 -85
  34. mcp_ticketer/queue/run_worker.py +2 -3
  35. mcp_ticketer/queue/worker.py +55 -40
  36. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
  37. mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
  38. mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
  39. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
  40. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
  41. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
  42. {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
@@ -8,14 +8,14 @@ environment files, including:
8
8
  - Security validation
9
9
  """
10
10
 
11
- import os
12
11
  import logging
13
- from pathlib import Path
14
- from typing import Dict, Any, Optional, List, Tuple
15
12
  from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Optional
15
+
16
16
  from dotenv import dotenv_values
17
17
 
18
- from .project_config import AdapterType, AdapterConfig
18
+ from .project_config import AdapterType
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -105,9 +105,9 @@ class DiscoveredAdapter:
105
105
  """Information about a discovered adapter configuration."""
106
106
 
107
107
  adapter_type: str
108
- config: Dict[str, Any]
108
+ config: dict[str, Any]
109
109
  confidence: float # 0.0-1.0 how complete the configuration is
110
- missing_fields: List[str] = field(default_factory=list)
110
+ missing_fields: list[str] = field(default_factory=list)
111
111
  found_in: str = ".env" # Which file it was found in
112
112
 
113
113
  def is_complete(self) -> bool:
@@ -119,9 +119,9 @@ class DiscoveredAdapter:
119
119
  class DiscoveryResult:
120
120
  """Result of environment file discovery."""
121
121
 
122
- adapters: List[DiscoveredAdapter] = field(default_factory=list)
123
- warnings: List[str] = field(default_factory=list)
124
- env_files_found: List[str] = field(default_factory=list)
122
+ adapters: list[DiscoveredAdapter] = field(default_factory=list)
123
+ warnings: list[str] = field(default_factory=list)
124
+ env_files_found: list[str] = field(default_factory=list)
125
125
 
126
126
  def get_primary_adapter(self) -> Optional[DiscoveredAdapter]:
127
127
  """Get the adapter with highest confidence and completeness."""
@@ -130,9 +130,7 @@ class DiscoveryResult:
130
130
 
131
131
  # Sort by: complete configs first, then by confidence
132
132
  sorted_adapters = sorted(
133
- self.adapters,
134
- key=lambda a: (a.is_complete(), a.confidence),
135
- reverse=True
133
+ self.adapters, key=lambda a: (a.is_complete(), a.confidence), reverse=True
136
134
  )
137
135
  return sorted_adapters[0]
138
136
 
@@ -160,6 +158,7 @@ class EnvDiscovery:
160
158
 
161
159
  Args:
162
160
  project_path: Path to project root (defaults to cwd)
161
+
163
162
  """
164
163
  self.project_path = project_path or Path.cwd()
165
164
 
@@ -168,6 +167,7 @@ class EnvDiscovery:
168
167
 
169
168
  Returns:
170
169
  DiscoveryResult with found adapters and warnings
170
+
171
171
  """
172
172
  result = DiscoveryResult()
173
173
 
@@ -179,19 +179,27 @@ class EnvDiscovery:
179
179
  return result
180
180
 
181
181
  # Detect adapters
182
- linear_adapter = self._detect_linear(env_vars, result.env_files_found[0] if result.env_files_found else ".env")
182
+ linear_adapter = self._detect_linear(
183
+ env_vars, result.env_files_found[0] if result.env_files_found else ".env"
184
+ )
183
185
  if linear_adapter:
184
186
  result.adapters.append(linear_adapter)
185
187
 
186
- github_adapter = self._detect_github(env_vars, result.env_files_found[0] if result.env_files_found else ".env")
188
+ github_adapter = self._detect_github(
189
+ env_vars, result.env_files_found[0] if result.env_files_found else ".env"
190
+ )
187
191
  if github_adapter:
188
192
  result.adapters.append(github_adapter)
189
193
 
190
- jira_adapter = self._detect_jira(env_vars, result.env_files_found[0] if result.env_files_found else ".env")
194
+ jira_adapter = self._detect_jira(
195
+ env_vars, result.env_files_found[0] if result.env_files_found else ".env"
196
+ )
191
197
  if jira_adapter:
192
198
  result.adapters.append(jira_adapter)
193
199
 
194
- aitrackdown_adapter = self._detect_aitrackdown(env_vars, result.env_files_found[0] if result.env_files_found else ".env")
200
+ aitrackdown_adapter = self._detect_aitrackdown(
201
+ env_vars, result.env_files_found[0] if result.env_files_found else ".env"
202
+ )
195
203
  if aitrackdown_adapter:
196
204
  result.adapters.append(aitrackdown_adapter)
197
205
 
@@ -201,7 +209,7 @@ class EnvDiscovery:
201
209
 
202
210
  return result
203
211
 
204
- def _load_env_files(self, result: DiscoveryResult) -> Dict[str, str]:
212
+ def _load_env_files(self, result: DiscoveryResult) -> dict[str, str]:
205
213
  """Load environment variables from files.
206
214
 
207
215
  Args:
@@ -209,8 +217,9 @@ class EnvDiscovery:
209
217
 
210
218
  Returns:
211
219
  Merged dictionary of environment variables
220
+
212
221
  """
213
- merged_env: Dict[str, str] = {}
222
+ merged_env: dict[str, str] = {}
214
223
 
215
224
  # Load files in reverse order (lowest priority first)
216
225
  for env_file in reversed(self.ENV_FILE_ORDER):
@@ -229,7 +238,9 @@ class EnvDiscovery:
229
238
 
230
239
  return merged_env
231
240
 
232
- def _find_key_value(self, env_vars: Dict[str, str], patterns: List[str]) -> Optional[str]:
241
+ def _find_key_value(
242
+ self, env_vars: dict[str, str], patterns: list[str]
243
+ ) -> Optional[str]:
233
244
  """Find first matching key value from patterns.
234
245
 
235
246
  Args:
@@ -238,13 +249,16 @@ class EnvDiscovery:
238
249
 
239
250
  Returns:
240
251
  Value if found, None otherwise
252
+
241
253
  """
242
254
  for pattern in patterns:
243
255
  if pattern in env_vars and env_vars[pattern]:
244
256
  return env_vars[pattern]
245
257
  return None
246
258
 
247
- def _detect_linear(self, env_vars: Dict[str, str], found_in: str) -> Optional[DiscoveredAdapter]:
259
+ def _detect_linear(
260
+ self, env_vars: dict[str, str], found_in: str
261
+ ) -> Optional[DiscoveredAdapter]:
248
262
  """Detect Linear adapter configuration.
249
263
 
250
264
  Args:
@@ -253,18 +267,19 @@ class EnvDiscovery:
253
267
 
254
268
  Returns:
255
269
  DiscoveredAdapter if Linear config detected, None otherwise
270
+
256
271
  """
257
272
  api_key = self._find_key_value(env_vars, LINEAR_KEY_PATTERNS)
258
273
 
259
274
  if not api_key:
260
275
  return None
261
276
 
262
- config: Dict[str, Any] = {
277
+ config: dict[str, Any] = {
263
278
  "api_key": api_key,
264
279
  "adapter": AdapterType.LINEAR.value,
265
280
  }
266
281
 
267
- missing_fields: List[str] = []
282
+ missing_fields: list[str] = []
268
283
  confidence = 0.6 # Has API key
269
284
 
270
285
  # Extract team ID (recommended but not required)
@@ -289,7 +304,9 @@ class EnvDiscovery:
289
304
  found_in=found_in,
290
305
  )
291
306
 
292
- def _detect_github(self, env_vars: Dict[str, str], found_in: str) -> Optional[DiscoveredAdapter]:
307
+ def _detect_github(
308
+ self, env_vars: dict[str, str], found_in: str
309
+ ) -> Optional[DiscoveredAdapter]:
293
310
  """Detect GitHub adapter configuration.
294
311
 
295
312
  Args:
@@ -298,18 +315,19 @@ class EnvDiscovery:
298
315
 
299
316
  Returns:
300
317
  DiscoveredAdapter if GitHub config detected, None otherwise
318
+
301
319
  """
302
320
  token = self._find_key_value(env_vars, GITHUB_TOKEN_PATTERNS)
303
321
 
304
322
  if not token:
305
323
  return None
306
324
 
307
- config: Dict[str, Any] = {
325
+ config: dict[str, Any] = {
308
326
  "token": token,
309
327
  "adapter": AdapterType.GITHUB.value,
310
328
  }
311
329
 
312
- missing_fields: List[str] = []
330
+ missing_fields: list[str] = []
313
331
  confidence = 0.4 # Has token
314
332
 
315
333
  # Try to extract owner/repo from combined field
@@ -345,7 +363,9 @@ class EnvDiscovery:
345
363
  found_in=found_in,
346
364
  )
347
365
 
348
- def _detect_jira(self, env_vars: Dict[str, str], found_in: str) -> Optional[DiscoveredAdapter]:
366
+ def _detect_jira(
367
+ self, env_vars: dict[str, str], found_in: str
368
+ ) -> Optional[DiscoveredAdapter]:
349
369
  """Detect JIRA adapter configuration.
350
370
 
351
371
  Args:
@@ -354,18 +374,19 @@ class EnvDiscovery:
354
374
 
355
375
  Returns:
356
376
  DiscoveredAdapter if JIRA config detected, None otherwise
377
+
357
378
  """
358
379
  api_token = self._find_key_value(env_vars, JIRA_TOKEN_PATTERNS)
359
380
 
360
381
  if not api_token:
361
382
  return None
362
383
 
363
- config: Dict[str, Any] = {
384
+ config: dict[str, Any] = {
364
385
  "api_token": api_token,
365
386
  "adapter": AdapterType.JIRA.value,
366
387
  }
367
388
 
368
- missing_fields: List[str] = []
389
+ missing_fields: list[str] = []
369
390
  confidence = 0.3 # Has token
370
391
 
371
392
  # Extract server (required)
@@ -398,7 +419,9 @@ class EnvDiscovery:
398
419
  found_in=found_in,
399
420
  )
400
421
 
401
- def _detect_aitrackdown(self, env_vars: Dict[str, str], found_in: str) -> Optional[DiscoveredAdapter]:
422
+ def _detect_aitrackdown(
423
+ self, env_vars: dict[str, str], found_in: str
424
+ ) -> Optional[DiscoveredAdapter]:
402
425
  """Detect AITrackdown adapter configuration.
403
426
 
404
427
  Args:
@@ -407,6 +430,7 @@ class EnvDiscovery:
407
430
 
408
431
  Returns:
409
432
  DiscoveredAdapter if AITrackdown config detected, None otherwise
433
+
410
434
  """
411
435
  base_path = self._find_key_value(env_vars, AITRACKDOWN_PATH_PATTERNS)
412
436
 
@@ -415,7 +439,7 @@ class EnvDiscovery:
415
439
  if not base_path and not aitrackdown_dir.exists():
416
440
  return None
417
441
 
418
- config: Dict[str, Any] = {
442
+ config: dict[str, Any] = {
419
443
  "adapter": AdapterType.AITRACKDOWN.value,
420
444
  }
421
445
 
@@ -435,13 +459,14 @@ class EnvDiscovery:
435
459
  found_in=found_in,
436
460
  )
437
461
 
438
- def _validate_security(self) -> List[str]:
462
+ def _validate_security(self) -> list[str]:
439
463
  """Validate security of environment files.
440
464
 
441
465
  Returns:
442
466
  List of security warnings
467
+
443
468
  """
444
- warnings: List[str] = []
469
+ warnings: list[str] = []
445
470
 
446
471
  # Check if .env files are tracked in git
447
472
  gitignore_path = self.project_path / ".gitignore"
@@ -460,7 +485,7 @@ class EnvDiscovery:
460
485
  # Check if .gitignore exists and has .env patterns
461
486
  if gitignore_path.exists():
462
487
  try:
463
- with open(gitignore_path, 'r') as f:
488
+ with open(gitignore_path) as f:
464
489
  gitignore_content = f.read()
465
490
  if ".env" not in gitignore_content:
466
491
  warnings.append(
@@ -479,6 +504,7 @@ class EnvDiscovery:
479
504
 
480
505
  Returns:
481
506
  True if file is tracked in git, False otherwise
507
+
482
508
  """
483
509
  import subprocess
484
510
 
@@ -498,7 +524,7 @@ class EnvDiscovery:
498
524
  logger.debug(f"Git check failed: {e}")
499
525
  return False
500
526
 
501
- def validate_discovered_config(self, adapter: DiscoveredAdapter) -> List[str]:
527
+ def validate_discovered_config(self, adapter: DiscoveredAdapter) -> list[str]:
502
528
  """Validate a discovered adapter configuration.
503
529
 
504
530
  Args:
@@ -506,8 +532,9 @@ class EnvDiscovery:
506
532
 
507
533
  Returns:
508
534
  List of validation warnings
535
+
509
536
  """
510
- warnings: List[str] = []
537
+ warnings: list[str] = []
511
538
 
512
539
  # Check API key/token length (basic sanity check)
513
540
  if adapter.adapter_type == AdapterType.LINEAR.value:
@@ -522,12 +549,16 @@ class EnvDiscovery:
522
549
 
523
550
  # Validate token prefix
524
551
  if token and not token.startswith(("ghp_", "gho_", "ghu_", "ghs_", "ghr_")):
525
- warnings.append("⚠️ GitHub token doesn't match expected format (should start with ghp_, gho_, etc.)")
552
+ warnings.append(
553
+ "⚠️ GitHub token doesn't match expected format (should start with ghp_, gho_, etc.)"
554
+ )
526
555
 
527
556
  elif adapter.adapter_type == AdapterType.JIRA.value:
528
557
  server = adapter.config.get("server", "")
529
558
  if server and not server.startswith(("http://", "https://")):
530
- warnings.append("⚠️ JIRA server URL should start with http:// or https://")
559
+ warnings.append(
560
+ "⚠️ JIRA server URL should start with http:// or https://"
561
+ )
531
562
 
532
563
  email = adapter.config.get("email", "")
533
564
  if email and "@" not in email:
@@ -550,6 +581,7 @@ def discover_config(project_path: Optional[Path] = None) -> DiscoveryResult:
550
581
 
551
582
  Returns:
552
583
  DiscoveryResult with found adapters and warnings
584
+
553
585
  """
554
586
  discovery = EnvDiscovery(project_path)
555
587
  return discovery.discover()
@@ -2,19 +2,19 @@
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from typing import Dict, Any, Optional, List, Union, Callable
6
- from datetime import datetime, timedelta
7
- from enum import Enum
8
5
  import time
6
+ from enum import Enum
7
+ from typing import Any, Optional, Union
9
8
 
10
9
  import httpx
11
- from httpx import AsyncClient, HTTPStatusError, TimeoutException
10
+ from httpx import AsyncClient, TimeoutException
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
14
 
16
15
  class HTTPMethod(str, Enum):
17
16
  """HTTP methods."""
17
+
18
18
  GET = "GET"
19
19
  POST = "POST"
20
20
  PUT = "PUT"
@@ -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: Optional[list[int]] = None,
36
+ retry_on_exceptions: Optional[list[type]] = None,
37
37
  ):
38
38
  self.max_retries = max_retries
39
39
  self.initial_delay = initial_delay
@@ -41,7 +41,11 @@ class RetryConfig:
41
41
  self.exponential_base = exponential_base
42
42
  self.jitter = jitter
43
43
  self.retry_on_status = retry_on_status or [429, 502, 503, 504]
44
- self.retry_on_exceptions = retry_on_exceptions or [TimeoutException, httpx.ConnectTimeout, httpx.ReadTimeout]
44
+ self.retry_on_exceptions = retry_on_exceptions or [
45
+ TimeoutException,
46
+ httpx.ConnectTimeout,
47
+ httpx.ReadTimeout,
48
+ ]
45
49
 
46
50
 
47
51
  class RateLimiter:
@@ -53,6 +57,7 @@ class RateLimiter:
53
57
  Args:
54
58
  max_requests: Maximum number of requests allowed
55
59
  time_window: Time window in seconds
60
+
56
61
  """
57
62
  self.max_requests = max_requests
58
63
  self.time_window = time_window
@@ -69,7 +74,7 @@ class RateLimiter:
69
74
  time_passed = now - self.last_update
70
75
  self.tokens = min(
71
76
  self.max_requests,
72
- self.tokens + (time_passed / self.time_window) * self.max_requests
77
+ self.tokens + (time_passed / self.time_window) * self.max_requests,
73
78
  )
74
79
  self.last_update = now
75
80
 
@@ -89,13 +94,13 @@ class BaseHTTPClient:
89
94
  def __init__(
90
95
  self,
91
96
  base_url: str,
92
- headers: Optional[Dict[str, str]] = None,
97
+ headers: Optional[dict[str, str]] = None,
93
98
  auth: Optional[Union[httpx.Auth, tuple]] = None,
94
99
  timeout: float = 30.0,
95
100
  retry_config: Optional[RetryConfig] = None,
96
101
  rate_limiter: Optional[RateLimiter] = None,
97
102
  verify_ssl: bool = True,
98
- follow_redirects: bool = True
103
+ follow_redirects: bool = True,
99
104
  ):
100
105
  """Initialize HTTP client.
101
106
 
@@ -108,6 +113,7 @@ class BaseHTTPClient:
108
113
  rate_limiter: Rate limiter instance
109
114
  verify_ssl: Whether to verify SSL certificates
110
115
  follow_redirects: Whether to follow redirects
116
+
111
117
  """
112
118
  self.base_url = base_url.rstrip("/")
113
119
  self.default_headers = headers or {}
@@ -137,11 +143,13 @@ class BaseHTTPClient:
137
143
  auth=self.auth,
138
144
  timeout=self.timeout,
139
145
  verify=self.verify_ssl,
140
- follow_redirects=self.follow_redirects
146
+ follow_redirects=self.follow_redirects,
141
147
  )
142
148
  return self._client
143
149
 
144
- async def _calculate_delay(self, attempt: int, response: Optional[httpx.Response] = None) -> float:
150
+ async def _calculate_delay(
151
+ self, attempt: int, response: Optional[httpx.Response] = None
152
+ ) -> float:
145
153
  """Calculate delay for retry attempt."""
146
154
  if response and response.status_code == 429:
147
155
  # Use Retry-After header if available
@@ -162,7 +170,8 @@ class BaseHTTPClient:
162
170
  # Add jitter to prevent thundering herd
163
171
  if self.retry_config.jitter:
164
172
  import random
165
- delay *= (0.5 + random.random() * 0.5)
173
+
174
+ delay *= 0.5 + random.random() * 0.5
166
175
 
167
176
  return delay
168
177
 
@@ -170,7 +179,7 @@ class BaseHTTPClient:
170
179
  self,
171
180
  exception: Exception,
172
181
  response: Optional[httpx.Response] = None,
173
- attempt: int = 1
182
+ attempt: int = 1,
174
183
  ) -> bool:
175
184
  """Determine if request should be retried."""
176
185
  if attempt >= self.retry_config.max_retries:
@@ -191,13 +200,13 @@ class BaseHTTPClient:
191
200
  self,
192
201
  method: Union[HTTPMethod, str],
193
202
  endpoint: str,
194
- data: Optional[Dict[str, Any]] = None,
195
- json: Optional[Dict[str, Any]] = None,
196
- params: Optional[Dict[str, Any]] = None,
197
- headers: Optional[Dict[str, str]] = None,
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,
198
207
  timeout: Optional[float] = None,
199
208
  retry_count: int = 0,
200
- **kwargs
209
+ **kwargs,
201
210
  ) -> httpx.Response:
202
211
  """Make HTTP request with retry and rate limiting.
203
212
 
@@ -218,6 +227,7 @@ class BaseHTTPClient:
218
227
  Raises:
219
228
  HTTPStatusError: On HTTP errors
220
229
  TimeoutException: On timeout
230
+
221
231
  """
222
232
  # Rate limiting
223
233
  if self.rate_limiter:
@@ -243,7 +253,7 @@ class BaseHTTPClient:
243
253
  params=params,
244
254
  headers=request_headers,
245
255
  timeout=timeout or self.timeout,
246
- **kwargs
256
+ **kwargs,
247
257
  )
248
258
 
249
259
  # Update stats
@@ -258,7 +268,7 @@ class BaseHTTPClient:
258
268
  self.stats["errors"] += 1
259
269
 
260
270
  # Check if we should retry
261
- response = getattr(e, 'response', None)
271
+ response = getattr(e, "response", None)
262
272
  if self._should_retry(e, response, retry_count + 1):
263
273
  delay = await self._calculate_delay(retry_count + 1, response)
264
274
 
@@ -269,7 +279,15 @@ class BaseHTTPClient:
269
279
 
270
280
  await asyncio.sleep(delay)
271
281
  return await self.request(
272
- method, endpoint, data, json, params, headers, timeout, retry_count + 1, **kwargs
282
+ method,
283
+ endpoint,
284
+ data,
285
+ json,
286
+ params,
287
+ headers,
288
+ timeout,
289
+ retry_count + 1,
290
+ **kwargs,
273
291
  )
274
292
 
275
293
  # No more retries, re-raise the exception
@@ -295,7 +313,7 @@ class BaseHTTPClient:
295
313
  """Make DELETE request."""
296
314
  return await self.request(HTTPMethod.DELETE, endpoint, **kwargs)
297
315
 
298
- async def get_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
316
+ async def get_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
299
317
  """Make GET request and return JSON response."""
300
318
  response = await self.get(endpoint, **kwargs)
301
319
 
@@ -305,7 +323,7 @@ class BaseHTTPClient:
305
323
 
306
324
  return response.json()
307
325
 
308
- async def post_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
326
+ async def post_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
309
327
  """Make POST request and return JSON response."""
310
328
  response = await self.post(endpoint, **kwargs)
311
329
 
@@ -315,7 +333,7 @@ class BaseHTTPClient:
315
333
 
316
334
  return response.json()
317
335
 
318
- async def put_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
336
+ async def put_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
319
337
  """Make PUT request and return JSON response."""
320
338
  response = await self.put(endpoint, **kwargs)
321
339
 
@@ -325,7 +343,7 @@ class BaseHTTPClient:
325
343
 
326
344
  return response.json()
327
345
 
328
- async def patch_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
346
+ async def patch_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
329
347
  """Make PATCH request and return JSON response."""
330
348
  response = await self.patch(endpoint, **kwargs)
331
349
 
@@ -335,7 +353,7 @@ class BaseHTTPClient:
335
353
 
336
354
  return response.json()
337
355
 
338
- def get_stats(self) -> Dict[str, Any]:
356
+ def get_stats(self) -> dict[str, Any]:
339
357
  """Get client statistics."""
340
358
  return self.stats.copy()
341
359
 
@@ -364,6 +382,7 @@ class GitHubHTTPClient(BaseHTTPClient):
364
382
  Args:
365
383
  token: GitHub API token
366
384
  api_url: GitHub API URL
385
+
367
386
  """
368
387
  headers = {
369
388
  "Authorization": f"Bearer {token}",
@@ -379,9 +398,8 @@ class GitHubHTTPClient(BaseHTTPClient):
379
398
  headers=headers,
380
399
  rate_limiter=rate_limiter,
381
400
  retry_config=RetryConfig(
382
- max_retries=3,
383
- retry_on_status=[429, 502, 503, 504, 522, 524]
384
- )
401
+ max_retries=3, retry_on_status=[429, 502, 503, 504, 522, 524]
402
+ ),
385
403
  )
386
404
 
387
405
 
@@ -394,7 +412,7 @@ class JiraHTTPClient(BaseHTTPClient):
394
412
  api_token: str,
395
413
  server_url: str,
396
414
  is_cloud: bool = True,
397
- verify_ssl: bool = True
415
+ verify_ssl: bool = True,
398
416
  ):
399
417
  """Initialize JIRA HTTP client.
400
418
 
@@ -404,13 +422,13 @@ class JiraHTTPClient(BaseHTTPClient):
404
422
  server_url: JIRA server URL
405
423
  is_cloud: Whether this is JIRA Cloud
406
424
  verify_ssl: Whether to verify SSL certificates
425
+
407
426
  """
408
- api_base = f"{server_url}/rest/api/3" if is_cloud else f"{server_url}/rest/api/2"
427
+ api_base = (
428
+ f"{server_url}/rest/api/3" if is_cloud else f"{server_url}/rest/api/2"
429
+ )
409
430
 
410
- headers = {
411
- "Accept": "application/json",
412
- "Content-Type": "application/json"
413
- }
431
+ headers = {"Accept": "application/json", "Content-Type": "application/json"}
414
432
 
415
433
  auth = httpx.BasicAuth(email, api_token)
416
434
 
@@ -424,7 +442,6 @@ class JiraHTTPClient(BaseHTTPClient):
424
442
  rate_limiter=rate_limiter,
425
443
  verify_ssl=verify_ssl,
426
444
  retry_config=RetryConfig(
427
- max_retries=3,
428
- retry_on_status=[429, 502, 503, 504]
429
- )
430
- )
445
+ max_retries=3, retry_on_status=[429, 502, 503, 504]
446
+ ),
447
+ )