mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +91 -54
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1544
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -2030
  155. mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
  157. mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,379 @@
1
+ """1Password CLI integration for secure secret management.
2
+
3
+ This module provides automatic secret loading from 1Password using the op CLI,
4
+ supporting:
5
+ - Detection of op:// secret references in .env files
6
+ - Automatic resolution using `op run` or `op inject`
7
+ - Fallback to regular .env values if 1Password CLI is not available
8
+ - Support for .env.1password template files
9
+ """
10
+
11
+ import logging
12
+ import shutil
13
+ import subprocess
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class OnePasswordConfig:
23
+ """Configuration for 1Password integration."""
24
+
25
+ enabled: bool = True
26
+ vault: str | None = None # Default vault for secret references
27
+ service_account_token: str | None = None # For CI/CD environments
28
+ fallback_to_env: bool = True # Fall back to regular .env if op CLI unavailable
29
+
30
+
31
+ class OnePasswordSecretsLoader:
32
+ """Load secrets from 1Password using the op CLI.
33
+
34
+ This class provides methods to:
35
+ 1. Check if 1Password CLI is installed and authenticated
36
+ 2. Resolve op:// secret references in .env files
37
+ 3. Load secrets into environment variables
38
+ 4. Create .env templates with op:// references
39
+ """
40
+
41
+ def __init__(self, config: OnePasswordConfig | None = None) -> None:
42
+ """Initialize the 1Password secrets loader.
43
+
44
+ Args:
45
+ config: Configuration for 1Password integration
46
+
47
+ """
48
+ self.config = config or OnePasswordConfig()
49
+ self._op_available: bool | None = None
50
+ self._op_authenticated: bool | None = None
51
+
52
+ def is_op_available(self) -> bool:
53
+ """Check if 1Password CLI is installed.
54
+
55
+ Returns:
56
+ True if op CLI is available, False otherwise
57
+
58
+ """
59
+ if self._op_available is None:
60
+ self._op_available = shutil.which("op") is not None
61
+ if not self._op_available:
62
+ logger.debug("1Password CLI (op) not found in PATH")
63
+ return self._op_available
64
+
65
+ def is_authenticated(self) -> bool:
66
+ """Check if user is authenticated with 1Password.
67
+
68
+ Returns:
69
+ True if authenticated, False otherwise
70
+
71
+ """
72
+ if not self.is_op_available():
73
+ return False
74
+
75
+ if self._op_authenticated is None:
76
+ try:
77
+ # Try to list accounts to check authentication
78
+ result = subprocess.run(
79
+ ["op", "account", "list"],
80
+ capture_output=True,
81
+ text=True,
82
+ timeout=5,
83
+ check=False,
84
+ )
85
+ self._op_authenticated = result.returncode == 0
86
+ if not self._op_authenticated:
87
+ logger.debug(
88
+ "1Password CLI not authenticated. Run 'op signin' first."
89
+ )
90
+ except (subprocess.TimeoutExpired, FileNotFoundError) as e:
91
+ logger.debug(f"Error checking 1Password authentication: {e}")
92
+ self._op_authenticated = False
93
+
94
+ return self._op_authenticated
95
+
96
+ def load_secrets_from_env_file(
97
+ self, env_file: Path, output_dict: dict[str, str] | None = None
98
+ ) -> dict[str, str]:
99
+ """Load secrets from .env file, resolving 1Password references.
100
+
101
+ This method:
102
+ 1. Checks if the .env file contains op:// references
103
+ 2. If yes and op CLI is available, uses op inject to resolve them
104
+ 3. If no op references or CLI unavailable, returns regular dotenv values
105
+
106
+ Args:
107
+ env_file: Path to .env file (may contain op:// references)
108
+ output_dict: Optional dict to update with loaded secrets
109
+
110
+ Returns:
111
+ Dictionary of environment variables with secrets resolved
112
+
113
+ """
114
+ if not env_file.exists():
115
+ logger.warning(f"Environment file not found: {env_file}")
116
+ return output_dict or {}
117
+
118
+ # Read the file to check for op:// references
119
+ content = env_file.read_text(encoding="utf-8")
120
+ has_op_references = "op://" in content
121
+
122
+ if has_op_references and self.is_authenticated():
123
+ # Use op inject to resolve references
124
+ return self._inject_secrets(env_file, output_dict)
125
+ else:
126
+ # Fall back to regular dotenv parsing
127
+ if has_op_references and not self.is_authenticated():
128
+ logger.warning(
129
+ f"File {env_file} contains 1Password references but op CLI "
130
+ "is not authenticated. Using fallback values."
131
+ )
132
+ return self._load_regular_env(env_file, output_dict)
133
+
134
+ def _inject_secrets(
135
+ self, env_file: Path, output_dict: dict[str, str] | None = None
136
+ ) -> dict[str, str]:
137
+ """Use op inject to resolve secret references in .env file.
138
+
139
+ Args:
140
+ env_file: Path to .env file with op:// references
141
+ output_dict: Optional dict to update
142
+
143
+ Returns:
144
+ Dictionary with resolved secrets
145
+
146
+ """
147
+ try:
148
+ # Use op inject to resolve references
149
+ cmd = ["op", "inject", "--in-file", str(env_file)]
150
+
151
+ # Add service account token if provided
152
+ env = None
153
+ if self.config.service_account_token:
154
+ env = {"OP_SERVICE_ACCOUNT_TOKEN": self.config.service_account_token}
155
+
156
+ result = subprocess.run(
157
+ cmd,
158
+ capture_output=True,
159
+ text=True,
160
+ timeout=30,
161
+ check=True,
162
+ env=env,
163
+ )
164
+
165
+ # Parse the injected output
166
+ secrets = self._parse_env_output(result.stdout)
167
+
168
+ if output_dict is not None:
169
+ output_dict.update(secrets)
170
+ return output_dict
171
+ return secrets
172
+
173
+ except subprocess.CalledProcessError as e:
174
+ logger.error(f"Error injecting 1Password secrets: {e.stderr}")
175
+ if self.config.fallback_to_env:
176
+ logger.info("Falling back to regular .env parsing")
177
+ return self._load_regular_env(env_file, output_dict)
178
+ raise
179
+ except subprocess.TimeoutExpired:
180
+ logger.error("Timeout while injecting 1Password secrets")
181
+ if self.config.fallback_to_env:
182
+ return self._load_regular_env(env_file, output_dict)
183
+ raise
184
+
185
+ def _load_regular_env(
186
+ self, env_file: Path, output_dict: dict[str, str] | None = None
187
+ ) -> dict[str, str]:
188
+ """Load environment variables without 1Password resolution.
189
+
190
+ Args:
191
+ env_file: Path to .env file
192
+ output_dict: Optional dict to update
193
+
194
+ Returns:
195
+ Dictionary of environment variables
196
+
197
+ """
198
+ from dotenv import dotenv_values
199
+
200
+ values = dotenv_values(env_file)
201
+
202
+ if output_dict is not None:
203
+ output_dict.update(values)
204
+ return output_dict
205
+ return dict(values)
206
+
207
+ def _parse_env_output(self, output: str) -> dict[str, str]:
208
+ """Parse environment variable output from op inject.
209
+
210
+ Args:
211
+ output: String output from op inject
212
+
213
+ Returns:
214
+ Dictionary of parsed environment variables
215
+
216
+ """
217
+ env_vars = {}
218
+ for line in output.splitlines():
219
+ line = line.strip()
220
+ if not line or line.startswith("#"):
221
+ continue
222
+
223
+ # Split on first = only
224
+ if "=" in line:
225
+ key, value = line.split("=", 1)
226
+ key = key.strip()
227
+ value = value.strip()
228
+
229
+ # Remove quotes if present
230
+ if value.startswith('"') and value.endswith('"'):
231
+ value = value[1:-1]
232
+ elif value.startswith("'") and value.endswith("'"):
233
+ value = value[1:-1]
234
+
235
+ env_vars[key] = value
236
+
237
+ return env_vars
238
+
239
+ def create_template_file(
240
+ self,
241
+ output_path: Path,
242
+ adapter_type: str,
243
+ vault_name: str = "Development",
244
+ item_name: str | None = None,
245
+ ) -> None:
246
+ """Create a .env template file with 1Password secret references.
247
+
248
+ Args:
249
+ output_path: Path where to create the template file
250
+ adapter_type: Type of adapter (linear, github, jira, aitrackdown)
251
+ vault_name: Name of 1Password vault to use
252
+ item_name: Name of 1Password item (defaults to adapter name)
253
+
254
+ """
255
+ if item_name is None:
256
+ item_name = adapter_type.upper()
257
+
258
+ templates = {
259
+ "linear": f"""# Linear Configuration with 1Password
260
+ # This file contains secret references that will be resolved by 1Password CLI
261
+ # Run: op run --env-file=.env.1password -- mcp-ticketer discover
262
+
263
+ LINEAR_API_KEY="op://{vault_name}/{item_name}/api_key"
264
+ LINEAR_TEAM_ID="op://{vault_name}/{item_name}/team_id"
265
+ LINEAR_TEAM_KEY="op://{vault_name}/{item_name}/team_key"
266
+ LINEAR_PROJECT_ID="op://{vault_name}/{item_name}/project_id"
267
+ """,
268
+ "github": f"""# GitHub Configuration with 1Password
269
+ # This file contains secret references that will be resolved by 1Password CLI
270
+ # Run: op run --env-file=.env.1password -- mcp-ticketer discover
271
+
272
+ GITHUB_TOKEN="op://{vault_name}/{item_name}/token"
273
+ GITHUB_OWNER="op://{vault_name}/{item_name}/owner"
274
+ GITHUB_REPO="op://{vault_name}/{item_name}/repo"
275
+ """,
276
+ "jira": f"""# JIRA Configuration with 1Password
277
+ # This file contains secret references that will be resolved by 1Password CLI
278
+ # Run: op run --env-file=.env.1password -- mcp-ticketer discover
279
+
280
+ JIRA_SERVER="op://{vault_name}/{item_name}/server"
281
+ JIRA_EMAIL="op://{vault_name}/{item_name}/email"
282
+ JIRA_API_TOKEN="op://{vault_name}/{item_name}/api_token"
283
+ JIRA_PROJECT_KEY="op://{vault_name}/{item_name}/project_key"
284
+ """,
285
+ "aitrackdown": """# AITrackdown Configuration
286
+ # AITrackdown doesn't use API keys, but you can store the base path
287
+
288
+ AITRACKDOWN_PATH=".aitrackdown"
289
+ """,
290
+ }
291
+
292
+ template = templates.get(adapter_type.lower(), "")
293
+ if template:
294
+ output_path.write_text(template, encoding="utf-8")
295
+ logger.info(f"Created 1Password template file: {output_path}")
296
+ else:
297
+ logger.error(f"Unknown adapter type: {adapter_type}")
298
+
299
+ def run_with_secrets(
300
+ self, command: list[str], env_file: Path | None = None
301
+ ) -> subprocess.CompletedProcess[str]:
302
+ """Run a command with secrets loaded from 1Password.
303
+
304
+ Args:
305
+ command: Command and arguments to run
306
+ env_file: Optional .env file with secret references
307
+
308
+ Returns:
309
+ CompletedProcess result
310
+
311
+ """
312
+ if not self.is_authenticated():
313
+ raise RuntimeError(
314
+ "1Password CLI not authenticated. Run 'op signin' first."
315
+ )
316
+
317
+ cmd = ["op", "run"]
318
+
319
+ if env_file:
320
+ cmd.extend(["--env-file", str(env_file)])
321
+
322
+ cmd.append("--")
323
+ cmd.extend(command)
324
+
325
+ return subprocess.run(cmd, capture_output=True, text=True, check=True)
326
+
327
+
328
+ def check_op_cli_status() -> dict[str, Any]:
329
+ """Check the status of 1Password CLI installation and authentication.
330
+
331
+ Returns:
332
+ Dictionary with status information
333
+
334
+ """
335
+ loader = OnePasswordSecretsLoader()
336
+
337
+ status: dict[str, Any] = {
338
+ "installed": loader.is_op_available(),
339
+ "authenticated": False,
340
+ "version": None,
341
+ "accounts": [],
342
+ }
343
+
344
+ if status["installed"]:
345
+ # Get version
346
+ try:
347
+ result = subprocess.run(
348
+ ["op", "--version"],
349
+ capture_output=True,
350
+ text=True,
351
+ timeout=5,
352
+ check=False,
353
+ )
354
+ if result.returncode == 0:
355
+ status["version"] = result.stdout.strip()
356
+ except (subprocess.TimeoutExpired, FileNotFoundError):
357
+ pass
358
+
359
+ # Check authentication
360
+ status["authenticated"] = loader.is_authenticated()
361
+
362
+ # Get accounts if authenticated
363
+ if status["authenticated"]:
364
+ try:
365
+ result = subprocess.run(
366
+ ["op", "account", "list", "--format=json"],
367
+ capture_output=True,
368
+ text=True,
369
+ timeout=5,
370
+ check=False,
371
+ )
372
+ if result.returncode == 0:
373
+ import json
374
+
375
+ status["accounts"] = json.loads(result.stdout)
376
+ except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
377
+ pass
378
+
379
+ return status