youtrack-cli 0.3.0__tar.gz → 0.3.2__tar.gz

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 (95) hide show
  1. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.claude/settings.local.json +2 -1
  2. youtrack_cli-0.3.2/CLAUDE.md +35 -0
  3. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/PKG-INFO +1 -1
  4. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/justfile +2 -2
  5. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/pyproject.toml +2 -2
  6. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_admin.py +22 -64
  7. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_articles.py +15 -45
  8. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_auth.py +7 -20
  9. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_boards.py +7 -21
  10. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_issues.py +31 -93
  11. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_logging.py +2 -10
  12. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_main.py +51 -28
  13. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_projects.py +16 -48
  14. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_reports.py +6 -18
  15. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_security.py +3 -9
  16. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_time.py +5 -15
  17. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_users.py +32 -96
  18. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/uv.lock +1 -1
  19. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/admin.py +15 -31
  20. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/articles.py +6 -17
  21. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/auth.py +4 -11
  22. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/boards.py +5 -16
  23. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/cache.py +5 -15
  24. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/cli_utils/aliases.py +4 -4
  25. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/client.py +8 -25
  26. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/articles.py +9 -25
  27. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/issues.py +16 -47
  28. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/projects.py +4 -13
  29. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/time_tracking.py +2 -6
  30. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/users.py +3 -9
  31. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/common.py +2 -6
  32. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/exceptions.py +4 -12
  33. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/issues.py +21 -69
  34. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/logging.py +3 -9
  35. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/main.py +19 -60
  36. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/performance.py +3 -11
  37. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/progress.py +1 -3
  38. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/projects.py +6 -19
  39. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/reports.py +29 -79
  40. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/security.py +9 -27
  41. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/time.py +4 -14
  42. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/users.py +6 -20
  43. youtrack_cli-0.3.0/CLAUDE.md +0 -204
  44. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.github/dependabot.yml +0 -0
  45. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.github/workflows/ci.yml +0 -0
  46. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.github/workflows/release.yml +0 -0
  47. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.gitignore +0 -0
  48. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.pre-commit-config.yaml +0 -0
  49. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.readthedocs.yaml +0 -0
  50. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/PUBLISHING.md +0 -0
  51. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/README.md +0 -0
  52. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/Makefile +0 -0
  53. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/api/index.rst +0 -0
  54. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/changelog.rst +0 -0
  55. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/command-aliases.rst +0 -0
  56. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/admin.rst +0 -0
  57. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/articles.rst +0 -0
  58. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/auth.rst +0 -0
  59. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/boards.rst +0 -0
  60. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/config.rst +0 -0
  61. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/index.rst +0 -0
  62. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/issues.rst +0 -0
  63. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/projects.rst +0 -0
  64. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/reports.rst +0 -0
  65. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/time.rst +0 -0
  66. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/users.rst +0 -0
  67. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/conf.py +0 -0
  68. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/configuration.rst +0 -0
  69. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/development.rst +0 -0
  70. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/index.rst +0 -0
  71. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/installation.rst +0 -0
  72. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/learning-path.rst +0 -0
  73. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/logging.rst +0 -0
  74. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/performance.md +0 -0
  75. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/progress-indicators.md +0 -0
  76. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/quickstart.rst +0 -0
  77. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/requirements.txt +0 -0
  78. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/security.rst +0 -0
  79. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/troubleshooting.rst +0 -0
  80. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/workflows.rst +0 -0
  81. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/youtrack-concepts.rst +0 -0
  82. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/package-lock.json +0 -0
  83. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/package.json +0 -0
  84. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/__init__.py +0 -0
  85. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/conftest.py +0 -0
  86. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_config.py +0 -0
  87. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tox.ini +0 -0
  88. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/__init__.py +0 -0
  89. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/cli_utils/__init__.py +0 -0
  90. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/__init__.py +0 -0
  91. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/boards.py +0 -0
  92. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/common.py +0 -0
  93. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/config.py +0 -0
  94. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/py.typed +0 -0
  95. {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/utils.py +0 -0
@@ -40,7 +40,8 @@
40
40
  "mcp__github__create_issue",
41
41
  "mcp__claude-code__Read",
42
42
  "mcp__claude-code__Grep",
43
- "WebFetch(domain:github.com)"
43
+ "WebFetch(domain:github.com)",
44
+ "Bash(git remote get-url:*)"
44
45
  ],
45
46
  "deny": []
46
47
  }
@@ -0,0 +1,35 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Plan
6
+
7
+ This is a YouTrack CLI application for interacting with JetBrains YouTrack issue tracking system via command line interface. This cli will offer an ergonomic, best practice cli and will leverage
8
+
9
+ - rich
10
+ - textual
11
+ - pydantic
12
+
13
+ We use `uv` for managing dependencies.
14
+
15
+ ## Create
16
+
17
+ Each new feature must have a corresponding github issue. When working on a new issue a new feature branch must be created with the name of the branch matching the name of the issue with the issue number in it.
18
+
19
+ For every change that is implemented, the README.md file MUST be updated to reflect that change.
20
+
21
+ ## Test
22
+
23
+ All tests must pass. We use `pytest` for testing, `ruff` for linting, `ty` for type checking, `tox` for running on various versions of Python. We'll utilize `zizmor` for reviewing our GitHub Actions. Pre-commit hooks ensure code quality before commits. All commands should be run with uv.
24
+
25
+ ## Documentation
26
+
27
+ Documentation is available in the docs/ folder. Any new functionality should have documentation written for it there. The README.md file shoudl not be used for comprehensive documentation.
28
+
29
+ ## Deploy
30
+
31
+ Deployment will always be done to a feature branch. When a feature is significant enough, we'll bump the version of the tool and tag it with that version. We will have a github action that deploys this to Test PyPI and PyPI using a `release.yml` GitHub Action.
32
+
33
+ ## Current Configuration
34
+
35
+ - Claude Code permissions are configured in `.claude/settings.local.json`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: youtrack-cli
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: YouTrack CLI - Command line interface for JetBrains YouTrack issue tracking system
5
5
  Project-URL: Homepage, https://github.com/ryan-murphy/yt-cli
6
6
  Project-URL: Documentation, https://yt-cli.readthedocs.io/
@@ -52,8 +52,8 @@ format-check:
52
52
  [group('quality')]
53
53
  typecheck:
54
54
  #!/usr/bin/env bash
55
- echo "Running mypy type checker..."
56
- uv run mypy youtrack_cli
55
+ echo "Running ty type checker..."
56
+ uv run ty youtrack_cli
57
57
  echo "✅ Type checking complete"
58
58
 
59
59
  [group('quality')]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "youtrack-cli"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "YouTrack CLI - Command line interface for JetBrains YouTrack issue tracking system"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9, <3.14"
@@ -62,7 +62,7 @@ packages = ["youtrack_cli"]
62
62
 
63
63
  [tool.ruff]
64
64
  target-version = "py39"
65
- line-length = 88
65
+ line-length = 120
66
66
 
67
67
  [tool.ruff.lint]
68
68
  select = [
@@ -51,9 +51,7 @@ class TestAdminManager:
51
51
  mock_response.json.return_value = mock_settings
52
52
  mock_response.raise_for_status.return_value = None
53
53
 
54
- mock_client.return_value.__aenter__.return_value.get.return_value = (
55
- mock_response
56
- )
54
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
57
55
 
58
56
  result = await admin_manager.get_global_settings()
59
57
 
@@ -71,20 +69,14 @@ class TestAdminManager:
71
69
  assert "Not authenticated" in result["message"]
72
70
 
73
71
  @pytest.mark.asyncio
74
- async def test_get_global_settings_insufficient_permissions(
75
- self, admin_manager, auth_manager
76
- ):
72
+ async def test_get_global_settings_insufficient_permissions(self, admin_manager, auth_manager):
77
73
  """Test global settings retrieval with insufficient permissions."""
78
74
  with patch("httpx.AsyncClient") as mock_client:
79
75
  mock_response = Mock()
80
76
  mock_response.status_code = 403
81
77
  mock_request = Mock()
82
- http_error = httpx.HTTPStatusError(
83
- "Forbidden", request=mock_request, response=mock_response
84
- )
85
- mock_client.return_value.__aenter__.return_value.get.side_effect = (
86
- http_error
87
- )
78
+ http_error = httpx.HTTPStatusError("Forbidden", request=mock_request, response=mock_response)
79
+ mock_client.return_value.__aenter__.return_value.get.side_effect = http_error
88
80
 
89
81
  result = await admin_manager.get_global_settings()
90
82
 
@@ -98,9 +90,7 @@ class TestAdminManager:
98
90
  mock_response = Mock()
99
91
  mock_response.raise_for_status.return_value = None
100
92
 
101
- mock_client.return_value.__aenter__.return_value.post.return_value = (
102
- mock_response
103
- )
93
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
104
94
 
105
95
  result = await admin_manager.set_global_setting("server.name", "New Name")
106
96
 
@@ -114,12 +104,8 @@ class TestAdminManager:
114
104
  mock_response = Mock()
115
105
  mock_response.status_code = 400
116
106
  mock_request = Mock()
117
- http_error = httpx.HTTPStatusError(
118
- "Bad Request", request=mock_request, response=mock_response
119
- )
120
- mock_client.return_value.__aenter__.return_value.post.side_effect = (
121
- http_error
122
- )
107
+ http_error = httpx.HTTPStatusError("Bad Request", request=mock_request, response=mock_response)
108
+ mock_client.return_value.__aenter__.return_value.post.side_effect = http_error
123
109
 
124
110
  result = await admin_manager.set_global_setting("invalid.key", "value")
125
111
 
@@ -142,9 +128,7 @@ class TestAdminManager:
142
128
  mock_response.json.return_value = mock_license
143
129
  mock_response.raise_for_status.return_value = None
144
130
 
145
- mock_client.return_value.__aenter__.return_value.get.return_value = (
146
- mock_response
147
- )
131
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
148
132
 
149
133
  result = await admin_manager.get_license_info()
150
134
 
@@ -161,9 +145,7 @@ class TestAdminManager:
161
145
  mock_response.json.return_value = mock_usage
162
146
  mock_response.raise_for_status.return_value = None
163
147
 
164
- mock_client.return_value.__aenter__.return_value.get.return_value = (
165
- mock_response
166
- )
148
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
167
149
 
168
150
  result = await admin_manager.get_license_usage()
169
151
 
@@ -186,9 +168,7 @@ class TestAdminManager:
186
168
  mock_response.json.return_value = mock_health
187
169
  mock_response.raise_for_status.return_value = None
188
170
 
189
- mock_client.return_value.__aenter__.return_value.get.return_value = (
190
- mock_response
191
- )
171
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
192
172
 
193
173
  result = await admin_manager.get_system_health()
194
174
 
@@ -202,9 +182,7 @@ class TestAdminManager:
202
182
  mock_response = Mock()
203
183
  mock_response.raise_for_status.return_value = None
204
184
 
205
- mock_client.return_value.__aenter__.return_value.post.return_value = (
206
- mock_response
207
- )
185
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
208
186
 
209
187
  result = await admin_manager.clear_caches()
210
188
 
@@ -231,12 +209,10 @@ class TestAdminManager:
231
209
 
232
210
  with patch("httpx.AsyncClient") as mock_client:
233
211
  mock_response = Mock()
234
- mock_response.json.return_value = mock_groups
212
+ mock_response.json.return_value = {"usergroups": mock_groups}
235
213
  mock_response.raise_for_status.return_value = None
236
214
 
237
- mock_client.return_value.__aenter__.return_value.get.return_value = (
238
- mock_response
239
- )
215
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
240
216
 
241
217
  result = await admin_manager.list_user_groups()
242
218
 
@@ -257,9 +233,7 @@ class TestAdminManager:
257
233
  mock_response.json.return_value = mock_created_group
258
234
  mock_response.raise_for_status.return_value = None
259
235
 
260
- mock_client.return_value.__aenter__.return_value.post.return_value = (
261
- mock_response
262
- )
236
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
263
237
 
264
238
  result = await admin_manager.create_user_group("New Group", "A new group")
265
239
 
@@ -274,12 +248,8 @@ class TestAdminManager:
274
248
  mock_response = Mock()
275
249
  mock_response.status_code = 400
276
250
  mock_request = Mock()
277
- http_error = httpx.HTTPStatusError(
278
- "Bad Request", request=mock_request, response=mock_response
279
- )
280
- mock_client.return_value.__aenter__.return_value.post.side_effect = (
281
- http_error
282
- )
251
+ http_error = httpx.HTTPStatusError("Bad Request", request=mock_request, response=mock_response)
252
+ mock_client.return_value.__aenter__.return_value.post.side_effect = http_error
283
253
 
284
254
  result = await admin_manager.create_user_group("Existing Group")
285
255
 
@@ -311,9 +281,7 @@ class TestAdminManager:
311
281
  mock_response.json.return_value = mock_fields
312
282
  mock_response.raise_for_status.return_value = None
313
283
 
314
- mock_client.return_value.__aenter__.return_value.get.return_value = (
315
- mock_response
316
- )
284
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
317
285
 
318
286
  result = await admin_manager.list_custom_fields()
319
287
 
@@ -387,9 +355,7 @@ class TestAdminManager:
387
355
  # Verify that it shows "No user groups found"
388
356
  mock_print.assert_called()
389
357
  call_args = [call[0][0] for call in mock_print.call_args_list]
390
- no_groups_found = any(
391
- "No user groups found" in str(arg) for arg in call_args
392
- )
358
+ no_groups_found = any("No user groups found" in str(arg) for arg in call_args)
393
359
  assert no_groups_found
394
360
 
395
361
  def test_display_user_groups_with_data(self, admin_manager):
@@ -417,9 +383,7 @@ class TestAdminManager:
417
383
  # Verify that it shows "No custom fields found"
418
384
  mock_print.assert_called()
419
385
  call_args = [call[0][0] for call in mock_print.call_args_list]
420
- no_fields_found = any(
421
- "No custom fields found" in str(arg) for arg in call_args
422
- )
386
+ no_fields_found = any("No custom fields found" in str(arg) for arg in call_args)
423
387
  assert no_fields_found
424
388
 
425
389
  def test_display_custom_fields_with_data(self, admin_manager):
@@ -478,9 +442,7 @@ class TestAdminCommands:
478
442
  }
479
443
 
480
444
  with patch("asyncio.run") as mock_asyncio:
481
- result = self.runner.invoke(
482
- main, ["admin", "global-settings", "set", "server.name", "New Name"]
483
- )
445
+ result = self.runner.invoke(main, ["admin", "global-settings", "set", "server.name", "New Name"])
484
446
 
485
447
  assert result.exit_code == 0
486
448
  mock_asyncio.assert_called_once()
@@ -525,9 +487,7 @@ class TestAdminCommands:
525
487
  }
526
488
 
527
489
  with patch("asyncio.run") as mock_asyncio:
528
- result = self.runner.invoke(
529
- main, ["admin", "maintenance", "clear-cache", "--confirm"]
530
- )
490
+ result = self.runner.invoke(main, ["admin", "maintenance", "clear-cache", "--confirm"])
531
491
 
532
492
  assert result.exit_code == 0
533
493
  mock_asyncio.assert_called_once()
@@ -572,9 +532,7 @@ class TestAdminCommands:
572
532
  }
573
533
 
574
534
  with patch("asyncio.run") as mock_asyncio:
575
- result = self.runner.invoke(
576
- main, ["admin", "user-groups", "create", "NewGroup"]
577
- )
535
+ result = self.runner.invoke(main, ["admin", "user-groups", "create", "NewGroup"])
578
536
 
579
537
  assert result.exit_code == 0
580
538
  mock_asyncio.assert_called_once()
@@ -52,9 +52,7 @@ class TestArticleManager:
52
52
  mock_resp.text = '{"id": "123", "summary": "Test Article"}'
53
53
  mock_resp.headers = {"content-type": "application/json"}
54
54
  mock_resp.raise_for_status.return_value = None
55
- mock_client.return_value.__aenter__.return_value.post.return_value = (
56
- mock_resp # noqa: E501
57
- )
55
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
58
56
 
59
57
  result = await article_manager.create_article(
60
58
  title="Test Article",
@@ -72,9 +70,7 @@ class TestArticleManager:
72
70
  mock_resp = Mock()
73
71
  mock_resp.status_code = 400
74
72
  mock_resp.text = "Bad Request"
75
- mock_client.return_value.__aenter__.return_value.post.return_value = (
76
- mock_resp # noqa: E501
77
- )
73
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
78
74
 
79
75
  result = await article_manager.create_article(
80
76
  title="Test Article",
@@ -107,9 +103,7 @@ class TestArticleManager:
107
103
  mock_resp.text = '[{"id": "123", "summary": "Article 1"}]'
108
104
  mock_resp.headers = {"content-type": "application/json"}
109
105
  mock_resp.raise_for_status.return_value = None
110
- mock_client.return_value.__aenter__.return_value.get.return_value = (
111
- mock_resp # noqa: E501
112
- )
106
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
113
107
 
114
108
  result = await article_manager.list_articles()
115
109
 
@@ -133,9 +127,7 @@ class TestArticleManager:
133
127
  mock_resp.text = '{"mock": "response"}'
134
128
  mock_resp.headers = {"content-type": "application/json"}
135
129
  mock_resp.raise_for_status.return_value = None
136
- mock_client.return_value.__aenter__.return_value.get.return_value = (
137
- mock_resp # noqa: E501
138
- )
130
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
139
131
 
140
132
  result = await article_manager.get_article("123")
141
133
 
@@ -158,9 +150,7 @@ class TestArticleManager:
158
150
  mock_resp.text = '{"mock": "response"}'
159
151
  mock_resp.headers = {"content-type": "application/json"}
160
152
  mock_resp.raise_for_status.return_value = None
161
- mock_client.return_value.__aenter__.return_value.post.return_value = (
162
- mock_resp # noqa: E501
163
- )
153
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
164
154
 
165
155
  result = await article_manager.update_article(
166
156
  article_id="123",
@@ -187,9 +177,7 @@ class TestArticleManager:
187
177
  mock_resp = Mock()
188
178
  mock_resp.status_code = 200
189
179
  mock_resp.raise_for_status.return_value = None
190
- mock_client.return_value.__aenter__.return_value.delete.return_value = (
191
- mock_resp # noqa: E501
192
- )
180
+ mock_client.return_value.__aenter__.return_value.delete.return_value = mock_resp # noqa: E501
193
181
 
194
182
  result = await article_manager.delete_article("123")
195
183
 
@@ -212,9 +200,7 @@ class TestArticleManager:
212
200
  mock_resp.text = '{"mock": "response"}'
213
201
  mock_resp.headers = {"content-type": "application/json"}
214
202
  mock_resp.raise_for_status.return_value = None
215
- mock_client.return_value.__aenter__.return_value.post.return_value = (
216
- mock_resp # noqa: E501
217
- )
203
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
218
204
 
219
205
  result = await article_manager.publish_article("123")
220
206
 
@@ -240,9 +226,7 @@ class TestArticleManager:
240
226
  mock_resp.text = '{"mock": "response"}'
241
227
  mock_resp.headers = {"content-type": "application/json"}
242
228
  mock_resp.raise_for_status.return_value = None
243
- mock_client.return_value.__aenter__.return_value.get.return_value = (
244
- mock_resp # noqa: E501
245
- )
229
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
246
230
 
247
231
  result = await article_manager.search_articles("search query")
248
232
 
@@ -268,9 +252,7 @@ class TestArticleManager:
268
252
  mock_resp.text = '{"mock": "response"}'
269
253
  mock_resp.headers = {"content-type": "application/json"}
270
254
  mock_resp.raise_for_status.return_value = None
271
- mock_client.return_value.__aenter__.return_value.get.return_value = (
272
- mock_resp # noqa: E501
273
- )
255
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
274
256
 
275
257
  result = await article_manager.get_article_comments("123")
276
258
 
@@ -293,9 +275,7 @@ class TestArticleManager:
293
275
  mock_resp.text = '{"mock": "response"}'
294
276
  mock_resp.headers = {"content-type": "application/json"}
295
277
  mock_resp.raise_for_status.return_value = None
296
- mock_client.return_value.__aenter__.return_value.post.return_value = (
297
- mock_resp # noqa: E501
298
- )
278
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
299
279
 
300
280
  result = await article_manager.add_comment("123", "Test comment")
301
281
 
@@ -322,9 +302,7 @@ class TestArticleManager:
322
302
  mock_resp.text = '{"mock": "response"}'
323
303
  mock_resp.headers = {"content-type": "application/json"}
324
304
  mock_resp.raise_for_status.return_value = None
325
- mock_client.return_value.__aenter__.return_value.get.return_value = (
326
- mock_resp # noqa: E501
327
- )
305
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
328
306
 
329
307
  result = await article_manager.get_article_attachments("123")
330
308
 
@@ -350,9 +328,7 @@ class TestArticleManager:
350
328
  article_manager.console = mock_console.return_value
351
329
  article_manager.display_articles_table([])
352
330
 
353
- mock_console.return_value.print.assert_called_with(
354
- "No articles found.", style="yellow"
355
- )
331
+ mock_console.return_value.print.assert_called_with("No articles found.", style="yellow")
356
332
 
357
333
  def test_display_articles_table_with_data(self, article_manager):
358
334
  """Test displaying articles table with data."""
@@ -383,9 +359,7 @@ class TestArticleManager:
383
359
  article_manager.console = mock_console.return_value
384
360
  article_manager.display_articles_tree([])
385
361
 
386
- mock_console.return_value.print.assert_called_with(
387
- "No articles found.", style="yellow"
388
- )
362
+ mock_console.return_value.print.assert_called_with("No articles found.", style="yellow")
389
363
 
390
364
  def test_display_articles_tree_with_data(self, article_manager):
391
365
  """Test displaying articles tree with data."""
@@ -455,9 +429,7 @@ class TestArticlesCLI:
455
429
  "data": {"id": "123"},
456
430
  }
457
431
 
458
- result = runner.invoke(
459
- main, ["articles", "create", "Test Title", "--content", "Test content"]
460
- )
432
+ result = runner.invoke(main, ["articles", "create", "Test Title", "--content", "Test content"])
461
433
 
462
434
  assert result.exit_code == 0
463
435
  assert "Creating article" in result.output
@@ -545,9 +517,7 @@ class TestArticlesCLI:
545
517
  "data": {"id": "comment-1"},
546
518
  }
547
519
 
548
- result = runner.invoke(
549
- main, ["articles", "comments", "add", "123", "Test comment"]
550
- )
520
+ result = runner.invoke(main, ["articles", "comments", "add", "123", "Test comment"])
551
521
 
552
522
  assert result.exit_code == 0
553
523
  assert "Adding comment" in result.output
@@ -27,9 +27,7 @@ class TestAuthConfig:
27
27
 
28
28
  def test_config_without_username(self):
29
29
  """Test creating config without username."""
30
- config = AuthConfig(
31
- base_url="https://example.youtrack.cloud", token="test-token-123"
32
- )
30
+ config = AuthConfig(base_url="https://example.youtrack.cloud", token="test-token-123")
33
31
  assert config.base_url == "https://example.youtrack.cloud"
34
32
  assert config.token == "test-token-123"
35
33
  assert config.username is None
@@ -51,8 +49,7 @@ class TestAuthManager:
51
49
 
52
50
  # Store original environment variables to restore later
53
51
  self.original_env = {
54
- key: os.environ.get(key)
55
- for key in ["YOUTRACK_BASE_URL", "YOUTRACK_TOKEN", "YOUTRACK_USERNAME"]
52
+ key: os.environ.get(key) for key in ["YOUTRACK_BASE_URL", "YOUTRACK_TOKEN", "YOUTRACK_USERNAME"]
56
53
  }
57
54
 
58
55
  def teardown_method(self):
@@ -93,9 +90,7 @@ class TestAuthManager:
93
90
  def test_save_credentials_without_username(self):
94
91
  """Test saving credentials without username."""
95
92
  # Force file storage instead of keyring for this test
96
- self.auth_manager.save_credentials(
97
- "https://example.youtrack.cloud", "test-token-123", use_keyring=False
98
- )
93
+ self.auth_manager.save_credentials("https://example.youtrack.cloud", "test-token-123", use_keyring=False)
99
94
 
100
95
  with open(self.config_path) as f:
101
96
  content = f.read()
@@ -183,13 +178,9 @@ class TestAuthManager:
183
178
  mock_response.raise_for_status.return_value = None
184
179
 
185
180
  with patch("httpx.AsyncClient") as mock_client:
186
- mock_client.return_value.__aenter__.return_value.get = AsyncMock(
187
- return_value=mock_response
188
- )
181
+ mock_client.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
189
182
 
190
- result = await self.auth_manager.verify_credentials(
191
- "https://example.youtrack.cloud", "test-token-123"
192
- )
183
+ result = await self.auth_manager.verify_credentials("https://example.youtrack.cloud", "test-token-123")
193
184
 
194
185
  assert result["status"] == "success"
195
186
  assert result["username"] == "testuser"
@@ -200,13 +191,9 @@ class TestAuthManager:
200
191
  async def test_verify_credentials_failure(self):
201
192
  """Test failed credential verification."""
202
193
  with patch("httpx.AsyncClient") as mock_client:
203
- mock_client.return_value.__aenter__.return_value.get = AsyncMock(
204
- side_effect=Exception("HTTP Error")
205
- )
194
+ mock_client.return_value.__aenter__.return_value.get = AsyncMock(side_effect=Exception("HTTP Error"))
206
195
 
207
- result = await self.auth_manager.verify_credentials(
208
- "https://example.youtrack.cloud", "invalid-token"
209
- )
196
+ result = await self.auth_manager.verify_credentials("https://example.youtrack.cloud", "invalid-token")
210
197
 
211
198
  assert result["status"] == "error"
212
199
  assert "HTTP Error" in result["message"]
@@ -55,9 +55,7 @@ class TestBoardManager:
55
55
  mock_resp.headers = {"content-type": "application/json"}
56
56
  mock_resp.raise_for_status.return_value = None
57
57
 
58
- mock_client.return_value.__aenter__.return_value.get.return_value = (
59
- mock_resp
60
- )
58
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
61
59
 
62
60
  result = await board_manager.list_boards()
63
61
 
@@ -84,9 +82,7 @@ class TestBoardManager:
84
82
  mock_resp.headers = {"content-type": "application/json"}
85
83
  mock_resp.raise_for_status.return_value = None
86
84
 
87
- mock_client.return_value.__aenter__.return_value.get.return_value = (
88
- mock_resp
89
- )
85
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
90
86
 
91
87
  result = await board_manager.list_boards(project_id="TEST")
92
88
 
@@ -124,9 +120,7 @@ class TestBoardManager:
124
120
  mock_resp.headers = {"content-type": "application/json"}
125
121
  mock_resp.raise_for_status.return_value = None
126
122
 
127
- mock_client.return_value.__aenter__.return_value.get.return_value = (
128
- mock_resp
129
- )
123
+ mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
130
124
 
131
125
  result = await board_manager.view_board("123")
132
126
 
@@ -161,9 +155,7 @@ class TestBoardManager:
161
155
  mock_resp.headers = {"content-type": "application/json"}
162
156
  mock_resp.raise_for_status.return_value = None
163
157
 
164
- mock_client.return_value.__aenter__.return_value.post.return_value = (
165
- mock_resp
166
- )
158
+ mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp
167
159
 
168
160
  result = await board_manager.update_board("123", name="Updated Board Name")
169
161
 
@@ -193,9 +185,7 @@ class TestBoardManager:
193
185
  async def test_list_boards_general_error(self, board_manager):
194
186
  """Test board listing with general error."""
195
187
  with patch("httpx.AsyncClient") as mock_client:
196
- mock_client.return_value.__aenter__.return_value.get.side_effect = (
197
- Exception("Connection error")
198
- )
188
+ mock_client.return_value.__aenter__.return_value.get.side_effect = Exception("Connection error")
199
189
 
200
190
  result = await board_manager.list_boards()
201
191
 
@@ -206,9 +196,7 @@ class TestBoardManager:
206
196
  async def test_view_board_general_error(self, board_manager):
207
197
  """Test board viewing with general error."""
208
198
  with patch("httpx.AsyncClient") as mock_client:
209
- mock_client.return_value.__aenter__.return_value.get.side_effect = (
210
- Exception("Connection error")
211
- )
199
+ mock_client.return_value.__aenter__.return_value.get.side_effect = Exception("Connection error")
212
200
 
213
201
  result = await board_manager.view_board("123")
214
202
 
@@ -219,9 +207,7 @@ class TestBoardManager:
219
207
  async def test_update_board_general_error(self, board_manager):
220
208
  """Test board updating with general error."""
221
209
  with patch("httpx.AsyncClient") as mock_client:
222
- mock_client.return_value.__aenter__.return_value.post.side_effect = (
223
- Exception("Connection error")
224
- )
210
+ mock_client.return_value.__aenter__.return_value.post.side_effect = Exception("Connection error")
225
211
 
226
212
  result = await board_manager.update_board("123", name="New Name")
227
213