youtrack-cli 0.3.2__tar.gz → 0.3.4__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.
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/PKG-INFO +11 -1
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/README.md +10 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/admin.rst +9 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/articles.rst +49 -8
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/pyproject.toml +1 -1
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_admin.py +32 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_articles.py +83 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/uv.lock +1 -1
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/admin.py +61 -18
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/articles.py +37 -2
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/.claude/settings.local.json +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/.github/dependabot.yml +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/.github/workflows/ci.yml +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/.github/workflows/release.yml +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/.gitignore +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/.pre-commit-config.yaml +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/.readthedocs.yaml +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/CLAUDE.md +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/PUBLISHING.md +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/Makefile +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/api/index.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/changelog.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/command-aliases.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/auth.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/boards.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/config.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/index.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/issues.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/projects.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/reports.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/time.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/commands/users.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/conf.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/configuration.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/development.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/index.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/installation.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/learning-path.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/logging.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/performance.md +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/progress-indicators.md +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/quickstart.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/requirements.txt +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/security.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/troubleshooting.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/workflows.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/docs/youtrack-concepts.rst +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/justfile +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/package-lock.json +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/package.json +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/__init__.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/conftest.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_auth.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_boards.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_config.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_issues.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_logging.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_main.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_projects.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_reports.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_security.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_time.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tests/test_users.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/tox.ini +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/__init__.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/articles.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/auth.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/boards.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/cache.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/cli_utils/__init__.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/cli_utils/aliases.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/client.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/__init__.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/boards.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/common.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/issues.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/projects.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/time_tracking.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/commands/users.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/common.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/config.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/exceptions.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/issues.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/logging.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/main.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/performance.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/progress.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/projects.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/py.typed +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/reports.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/security.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/time.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/users.py +0 -0
- {youtrack_cli-0.3.2 → youtrack_cli-0.3.4}/youtrack_cli/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: youtrack-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
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/
|
|
@@ -136,6 +136,9 @@ yt projects list
|
|
|
136
136
|
# Create an article
|
|
137
137
|
yt articles create "Getting Started" --content "Welcome to our documentation"
|
|
138
138
|
|
|
139
|
+
# Create an article from a markdown file
|
|
140
|
+
yt articles create "API Documentation" --file api-docs.md
|
|
141
|
+
|
|
139
142
|
# Log work time
|
|
140
143
|
yt time log ISSUE-123 "2h" --description "Feature development"
|
|
141
144
|
|
|
@@ -198,6 +201,13 @@ yt issues tag add ISSUE-456 urgent
|
|
|
198
201
|
```bash
|
|
199
202
|
# Create and manage knowledge base
|
|
200
203
|
yt articles create "API Guide" --content "Comprehensive API documentation"
|
|
204
|
+
|
|
205
|
+
# Create articles from markdown files
|
|
206
|
+
yt articles create "Installation Guide" --file docs/install.md
|
|
207
|
+
|
|
208
|
+
# Organize existing documentation
|
|
209
|
+
yt articles create "Developer Guide" --file dev-guide.md --project-id PROJECT-123
|
|
210
|
+
|
|
201
211
|
yt articles tree --project-id PROJECT-123
|
|
202
212
|
yt articles search "authentication"
|
|
203
213
|
```
|
|
@@ -96,6 +96,9 @@ yt projects list
|
|
|
96
96
|
# Create an article
|
|
97
97
|
yt articles create "Getting Started" --content "Welcome to our documentation"
|
|
98
98
|
|
|
99
|
+
# Create an article from a markdown file
|
|
100
|
+
yt articles create "API Documentation" --file api-docs.md
|
|
101
|
+
|
|
99
102
|
# Log work time
|
|
100
103
|
yt time log ISSUE-123 "2h" --description "Feature development"
|
|
101
104
|
|
|
@@ -158,6 +161,13 @@ yt issues tag add ISSUE-456 urgent
|
|
|
158
161
|
```bash
|
|
159
162
|
# Create and manage knowledge base
|
|
160
163
|
yt articles create "API Guide" --content "Comprehensive API documentation"
|
|
164
|
+
|
|
165
|
+
# Create articles from markdown files
|
|
166
|
+
yt articles create "Installation Guide" --file docs/install.md
|
|
167
|
+
|
|
168
|
+
# Organize existing documentation
|
|
169
|
+
yt articles create "Developer Guide" --file dev-guide.md --project-id PROJECT-123
|
|
170
|
+
|
|
161
171
|
yt articles tree --project-id PROJECT-123
|
|
162
172
|
yt articles search "authentication"
|
|
163
173
|
```
|
|
@@ -552,6 +552,15 @@ Common Issues and Solutions
|
|
|
552
552
|
**Health Check Issues**
|
|
553
553
|
Review system logs and resource availability.
|
|
554
554
|
|
|
555
|
+
**Health Check 404 Errors**
|
|
556
|
+
If the health check command returns a 404 error, this may indicate:
|
|
557
|
+
|
|
558
|
+
* Your YouTrack version doesn't support the system settings endpoint
|
|
559
|
+
* The API endpoint has changed in your YouTrack version
|
|
560
|
+
* Your YouTrack instance has a different API configuration
|
|
561
|
+
|
|
562
|
+
The command will automatically try fallback endpoints and provide specific guidance based on the error type.
|
|
563
|
+
|
|
555
564
|
System Diagnostics
|
|
556
565
|
~~~~~~~~~~~~~~~~~
|
|
557
566
|
|
|
@@ -54,7 +54,10 @@ Create a new article in YouTrack.
|
|
|
54
54
|
- Description
|
|
55
55
|
* - ``--content, -c``
|
|
56
56
|
- text
|
|
57
|
-
- Article content (
|
|
57
|
+
- Article content (required if --file not provided)
|
|
58
|
+
* - ``--file, -f``
|
|
59
|
+
- path
|
|
60
|
+
- Path to markdown file containing article content (required if --content not provided)
|
|
58
61
|
* - ``--project-id, -p``
|
|
59
62
|
- string
|
|
60
63
|
- Project ID to associate with the article
|
|
@@ -72,17 +75,23 @@ Create a new article in YouTrack.
|
|
|
72
75
|
|
|
73
76
|
.. code-block:: bash
|
|
74
77
|
|
|
75
|
-
# Create a simple article
|
|
78
|
+
# Create a simple article with inline content
|
|
76
79
|
yt articles create "Getting Started Guide" --content "This is a comprehensive guide..."
|
|
77
80
|
|
|
78
|
-
# Create an article
|
|
79
|
-
yt articles create "
|
|
81
|
+
# Create an article from a markdown file
|
|
82
|
+
yt articles create "Getting Started Guide" --file getting-started.md
|
|
83
|
+
|
|
84
|
+
# Create an article in a specific project from a file
|
|
85
|
+
yt articles create "API Documentation" --file api-docs.md --project-id PROJECT-123
|
|
86
|
+
|
|
87
|
+
# Create a nested article (child of another article) from a file
|
|
88
|
+
yt articles create "Advanced Features" --file advanced.md --parent-id ARTICLE-456
|
|
80
89
|
|
|
81
|
-
# Create a
|
|
82
|
-
yt articles create "
|
|
90
|
+
# Create a draft article (private visibility) from a file
|
|
91
|
+
yt articles create "Draft Article" --file draft.md --visibility private
|
|
83
92
|
|
|
84
|
-
# Create
|
|
85
|
-
yt articles create "
|
|
93
|
+
# Create an article with inline content (traditional approach)
|
|
94
|
+
yt articles create "API Documentation" --content "API usage guide" --project-id PROJECT-123
|
|
86
95
|
|
|
87
96
|
edit
|
|
88
97
|
~~~~
|
|
@@ -671,6 +680,24 @@ Content Management
|
|
|
671
680
|
# View article details
|
|
672
681
|
yt articles edit ARTICLE-123 --show-details
|
|
673
682
|
|
|
683
|
+
Working with Markdown Files
|
|
684
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
685
|
+
|
|
686
|
+
.. code-block:: bash
|
|
687
|
+
|
|
688
|
+
# Create articles from existing markdown files
|
|
689
|
+
yt articles create "Installation Guide" --file docs/installation.md
|
|
690
|
+
|
|
691
|
+
# Create multiple articles from markdown files
|
|
692
|
+
yt articles create "User Manual" --file user-manual.md --project-id PROJECT-123
|
|
693
|
+
yt articles create "Developer Guide" --file dev-guide.md --project-id PROJECT-123
|
|
694
|
+
|
|
695
|
+
# Organize markdown documentation into YouTrack articles
|
|
696
|
+
for file in docs/*.md; do
|
|
697
|
+
title=$(basename "$file" .md)
|
|
698
|
+
yt articles create "$title" --file "$file" --project-id PROJECT-123
|
|
699
|
+
done
|
|
700
|
+
|
|
674
701
|
Best Practices
|
|
675
702
|
--------------
|
|
676
703
|
|
|
@@ -690,6 +717,8 @@ Best Practices
|
|
|
690
717
|
|
|
691
718
|
8. **Consistent Formatting**: Follow consistent formatting and style guidelines across articles.
|
|
692
719
|
|
|
720
|
+
9. **Use Markdown Files**: For complex content, consider writing in markdown files first and using the ``--file`` option for better version control and editing experience.
|
|
721
|
+
|
|
693
722
|
Error Handling
|
|
694
723
|
--------------
|
|
695
724
|
|
|
@@ -710,6 +739,18 @@ Common error scenarios and solutions:
|
|
|
710
739
|
**Content Too Large**
|
|
711
740
|
YouTrack may have limits on article content size. Consider breaking large articles into smaller sections.
|
|
712
741
|
|
|
742
|
+
**File Not Found**
|
|
743
|
+
Ensure the file path provided with ``--file`` exists and is accessible.
|
|
744
|
+
|
|
745
|
+
**Invalid File Content**
|
|
746
|
+
The specified file must be a valid text file. Binary files or files with invalid encoding will be rejected.
|
|
747
|
+
|
|
748
|
+
**Empty File**
|
|
749
|
+
Files provided with ``--file`` must contain content. Empty files will be rejected.
|
|
750
|
+
|
|
751
|
+
**Both Content and File Specified**
|
|
752
|
+
You cannot use both ``--content`` and ``--file`` options simultaneously. Choose one method for providing article content.
|
|
753
|
+
|
|
713
754
|
See Also
|
|
714
755
|
--------
|
|
715
756
|
|
|
@@ -175,6 +175,38 @@ class TestAdminManager:
|
|
|
175
175
|
assert result["status"] == "success"
|
|
176
176
|
assert result["data"] == mock_health
|
|
177
177
|
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_get_system_health_404_error(self, admin_manager, auth_manager):
|
|
180
|
+
"""Test system health check with 404 error on all endpoints."""
|
|
181
|
+
with patch("httpx.AsyncClient") as mock_client:
|
|
182
|
+
mock_response = Mock()
|
|
183
|
+
mock_response.status_code = 404
|
|
184
|
+
mock_request = Mock()
|
|
185
|
+
http_error = httpx.HTTPStatusError("Not Found", request=mock_request, response=mock_response)
|
|
186
|
+
mock_client.return_value.__aenter__.return_value.get.side_effect = http_error
|
|
187
|
+
|
|
188
|
+
result = await admin_manager.get_system_health()
|
|
189
|
+
|
|
190
|
+
assert result["status"] == "error"
|
|
191
|
+
assert "System health endpoint not found (404)" in result["message"]
|
|
192
|
+
assert "YouTrack version doesn't support this endpoint" in result["message"]
|
|
193
|
+
|
|
194
|
+
@pytest.mark.asyncio
|
|
195
|
+
async def test_get_system_health_403_error(self, admin_manager, auth_manager):
|
|
196
|
+
"""Test system health check with 403 permission error."""
|
|
197
|
+
with patch("httpx.AsyncClient") as mock_client:
|
|
198
|
+
mock_response = Mock()
|
|
199
|
+
mock_response.status_code = 403
|
|
200
|
+
mock_request = Mock()
|
|
201
|
+
http_error = httpx.HTTPStatusError("Forbidden", request=mock_request, response=mock_response)
|
|
202
|
+
mock_client.return_value.__aenter__.return_value.get.side_effect = http_error
|
|
203
|
+
|
|
204
|
+
result = await admin_manager.get_system_health()
|
|
205
|
+
|
|
206
|
+
assert result["status"] == "error"
|
|
207
|
+
assert "Insufficient permissions for health check" in result["message"]
|
|
208
|
+
assert "Low-level Admin Read" in result["message"]
|
|
209
|
+
|
|
178
210
|
@pytest.mark.asyncio
|
|
179
211
|
async def test_clear_caches_success(self, admin_manager, auth_manager):
|
|
180
212
|
"""Test successful cache clearing."""
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Tests for article management functionality."""
|
|
2
2
|
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
from unittest.mock import MagicMock, Mock, patch
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
@@ -434,6 +435,88 @@ class TestArticlesCLI:
|
|
|
434
435
|
assert result.exit_code == 0
|
|
435
436
|
assert "Creating article" in result.output
|
|
436
437
|
|
|
438
|
+
def test_articles_create_command_with_file(self):
|
|
439
|
+
"""Test articles create command with file input."""
|
|
440
|
+
from youtrack_cli.main import main
|
|
441
|
+
|
|
442
|
+
runner = CliRunner()
|
|
443
|
+
|
|
444
|
+
with (
|
|
445
|
+
patch("youtrack_cli.main.asyncio.run") as mock_run,
|
|
446
|
+
patch("youtrack_cli.main.AuthManager"),
|
|
447
|
+
patch("youtrack_cli.articles.ArticleManager"),
|
|
448
|
+
runner.isolated_filesystem(),
|
|
449
|
+
):
|
|
450
|
+
# Create a test markdown file
|
|
451
|
+
test_file = Path("test_article.md")
|
|
452
|
+
test_file.write_text("# Test Article\n\nThis is test content from a markdown file.")
|
|
453
|
+
|
|
454
|
+
mock_run.return_value = {
|
|
455
|
+
"status": "success",
|
|
456
|
+
"message": "Article created successfully",
|
|
457
|
+
"data": {"id": "123"},
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
result = runner.invoke(main, ["articles", "create", "Test Title", "--file", str(test_file)])
|
|
461
|
+
|
|
462
|
+
assert result.exit_code == 0
|
|
463
|
+
assert "Reading content from" in result.output
|
|
464
|
+
assert "Creating article" in result.output
|
|
465
|
+
|
|
466
|
+
def test_articles_create_command_file_not_found(self):
|
|
467
|
+
"""Test articles create command with non-existent file."""
|
|
468
|
+
from youtrack_cli.main import main
|
|
469
|
+
|
|
470
|
+
runner = CliRunner()
|
|
471
|
+
|
|
472
|
+
result = runner.invoke(main, ["articles", "create", "Test Title", "--file", "nonexistent.md"])
|
|
473
|
+
|
|
474
|
+
assert result.exit_code != 0
|
|
475
|
+
assert "does not exist" in result.output
|
|
476
|
+
|
|
477
|
+
def test_articles_create_command_both_content_and_file(self):
|
|
478
|
+
"""Test articles create command with both content and file (should fail)."""
|
|
479
|
+
from youtrack_cli.main import main
|
|
480
|
+
|
|
481
|
+
runner = CliRunner()
|
|
482
|
+
|
|
483
|
+
with runner.isolated_filesystem():
|
|
484
|
+
test_file = Path("test_article.md")
|
|
485
|
+
test_file.write_text("Test content")
|
|
486
|
+
|
|
487
|
+
result = runner.invoke(
|
|
488
|
+
main, ["articles", "create", "Test Title", "--content", "Test content", "--file", str(test_file)]
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
assert result.exit_code != 0
|
|
492
|
+
assert "Cannot specify both --content and --file options" in result.output
|
|
493
|
+
|
|
494
|
+
def test_articles_create_command_no_content_or_file(self):
|
|
495
|
+
"""Test articles create command with neither content nor file (should fail)."""
|
|
496
|
+
from youtrack_cli.main import main
|
|
497
|
+
|
|
498
|
+
runner = CliRunner()
|
|
499
|
+
|
|
500
|
+
result = runner.invoke(main, ["articles", "create", "Test Title"])
|
|
501
|
+
|
|
502
|
+
assert result.exit_code != 0
|
|
503
|
+
assert "Either --content or --file must be specified" in result.output
|
|
504
|
+
|
|
505
|
+
def test_articles_create_command_empty_file(self):
|
|
506
|
+
"""Test articles create command with empty file."""
|
|
507
|
+
from youtrack_cli.main import main
|
|
508
|
+
|
|
509
|
+
runner = CliRunner()
|
|
510
|
+
|
|
511
|
+
with runner.isolated_filesystem():
|
|
512
|
+
test_file = Path("empty.md")
|
|
513
|
+
test_file.write_text("")
|
|
514
|
+
|
|
515
|
+
result = runner.invoke(main, ["articles", "create", "Test Title", "--file", str(test_file)])
|
|
516
|
+
|
|
517
|
+
assert result.exit_code != 0
|
|
518
|
+
assert "is empty" in result.output
|
|
519
|
+
|
|
437
520
|
def test_articles_list_command(self):
|
|
438
521
|
"""Test articles list command."""
|
|
439
522
|
from youtrack_cli.main import main
|
|
@@ -153,6 +153,7 @@ class AdminManager:
|
|
|
153
153
|
response = await client.get(
|
|
154
154
|
f"{credentials.base_url.rstrip('/')}/api/admin/globalSettings/license",
|
|
155
155
|
headers=headers,
|
|
156
|
+
params={"fields": "id,username,license,error"},
|
|
156
157
|
timeout=10.0,
|
|
157
158
|
)
|
|
158
159
|
response.raise_for_status()
|
|
@@ -167,6 +168,14 @@ class AdminManager:
|
|
|
167
168
|
"status": "error",
|
|
168
169
|
"message": "Insufficient permissions to view license.",
|
|
169
170
|
}
|
|
171
|
+
elif e.response.status_code == 404:
|
|
172
|
+
return {
|
|
173
|
+
"status": "error",
|
|
174
|
+
"message": (
|
|
175
|
+
"License endpoint not found. This may indicate an "
|
|
176
|
+
"incompatible YouTrack version or configuration."
|
|
177
|
+
),
|
|
178
|
+
}
|
|
170
179
|
return {"status": "error", "message": f"HTTP error: {e}"}
|
|
171
180
|
except Exception as e:
|
|
172
181
|
return {"status": "error", "message": f"Unexpected error: {e}"}
|
|
@@ -231,28 +240,62 @@ class AdminManager:
|
|
|
231
240
|
"Accept": "application/json",
|
|
232
241
|
}
|
|
233
242
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
timeout=10.0,
|
|
240
|
-
)
|
|
241
|
-
response.raise_for_status()
|
|
242
|
-
|
|
243
|
-
health_info = response.json()
|
|
244
|
-
return {"status": "success", "data": health_info}
|
|
243
|
+
# List of endpoints to try, in order of preference
|
|
244
|
+
endpoints = [
|
|
245
|
+
"/api/admin/globalSettings/systemSettings",
|
|
246
|
+
"/api/admin/globalSettings/systemSettings?fields=baseUrl,isApplicationReadOnly,maxUploadFileSize,maxExportItems",
|
|
247
|
+
]
|
|
245
248
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
+
async with httpx.AsyncClient() as client:
|
|
250
|
+
for endpoint in endpoints:
|
|
251
|
+
try:
|
|
252
|
+
response = await client.get(
|
|
253
|
+
f"{credentials.base_url.rstrip('/')}{endpoint}",
|
|
254
|
+
headers=headers,
|
|
255
|
+
timeout=10.0,
|
|
256
|
+
)
|
|
257
|
+
response.raise_for_status()
|
|
258
|
+
|
|
259
|
+
health_info = response.json()
|
|
260
|
+
return {"status": "success", "data": health_info}
|
|
261
|
+
|
|
262
|
+
except httpx.HTTPStatusError as e:
|
|
263
|
+
if e.response.status_code == 404:
|
|
264
|
+
continue # Try next endpoint
|
|
265
|
+
elif e.response.status_code == 403:
|
|
249
266
|
return {
|
|
250
267
|
"status": "error",
|
|
251
|
-
"message": "Insufficient permissions for health check."
|
|
268
|
+
"message": "Insufficient permissions for health check. "
|
|
269
|
+
"Requires 'Low-level Admin Read' permission.",
|
|
252
270
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
271
|
+
elif e.response.status_code == 401:
|
|
272
|
+
return {
|
|
273
|
+
"status": "error",
|
|
274
|
+
"message": "Authentication failed. Your token may have expired. Run 'yt auth login' again.",
|
|
275
|
+
}
|
|
276
|
+
else:
|
|
277
|
+
response_text = e.response.text if hasattr(e.response, "text") else str(e)
|
|
278
|
+
return {
|
|
279
|
+
"status": "error",
|
|
280
|
+
"message": f"HTTP {e.response.status_code}: {response_text}",
|
|
281
|
+
}
|
|
282
|
+
except httpx.RequestError as e:
|
|
283
|
+
return {
|
|
284
|
+
"status": "error",
|
|
285
|
+
"message": f"Network error: {e}. Check your YouTrack URL and network connection.",
|
|
286
|
+
}
|
|
287
|
+
except Exception as e:
|
|
288
|
+
return {"status": "error", "message": f"Unexpected error: {e}"}
|
|
289
|
+
|
|
290
|
+
# If all endpoints failed with 404
|
|
291
|
+
return {
|
|
292
|
+
"status": "error",
|
|
293
|
+
"message": "System health endpoint not found (404). This may indicate:\n"
|
|
294
|
+
"1. Your YouTrack version doesn't support this endpoint\n"
|
|
295
|
+
"2. The endpoint URL may have changed\n"
|
|
296
|
+
"3. Your YouTrack instance has a different API configuration\n"
|
|
297
|
+
"Please verify your YouTrack version and API access.",
|
|
298
|
+
}
|
|
256
299
|
|
|
257
300
|
async def clear_caches(self) -> dict[str, Any]:
|
|
258
301
|
"""Clear system caches.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Articles command group for YouTrack CLI."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
6
7
|
import click
|
|
@@ -20,9 +21,14 @@ def articles() -> None:
|
|
|
20
21
|
@click.option(
|
|
21
22
|
"--content",
|
|
22
23
|
"-c",
|
|
23
|
-
prompt=True,
|
|
24
24
|
help="Article content",
|
|
25
25
|
)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--file",
|
|
28
|
+
"-f",
|
|
29
|
+
type=click.Path(exists=True, path_type=Path),
|
|
30
|
+
help="Path to markdown file containing article content",
|
|
31
|
+
)
|
|
26
32
|
@click.option(
|
|
27
33
|
"--project-id",
|
|
28
34
|
"-p",
|
|
@@ -47,7 +53,8 @@ def articles() -> None:
|
|
|
47
53
|
def create(
|
|
48
54
|
ctx: click.Context,
|
|
49
55
|
title: str,
|
|
50
|
-
content: str,
|
|
56
|
+
content: Optional[str],
|
|
57
|
+
file: Optional[Path],
|
|
51
58
|
project_id: Optional[str],
|
|
52
59
|
parent_id: Optional[str],
|
|
53
60
|
summary: Optional[str],
|
|
@@ -57,11 +64,39 @@ def create(
|
|
|
57
64
|
from ..articles import ArticleManager
|
|
58
65
|
|
|
59
66
|
console = Console()
|
|
67
|
+
|
|
68
|
+
# Validate that either content or file is provided, but not both
|
|
69
|
+
if content and file:
|
|
70
|
+
console.print("❌ Cannot specify both --content and --file options", style="red")
|
|
71
|
+
raise click.ClickException("Use either --content or --file, not both")
|
|
72
|
+
|
|
73
|
+
if not content and not file:
|
|
74
|
+
console.print("❌ Either --content or --file must be specified", style="red")
|
|
75
|
+
raise click.ClickException("Article content is required")
|
|
76
|
+
|
|
77
|
+
# Read content from file if provided
|
|
78
|
+
if file:
|
|
79
|
+
try:
|
|
80
|
+
console.print(f"📖 Reading content from '{file}'...", style="blue")
|
|
81
|
+
content = file.read_text(encoding="utf-8")
|
|
82
|
+
if not content.strip():
|
|
83
|
+
console.print(f"❌ File '{file}' is empty", style="red")
|
|
84
|
+
raise click.ClickException("File content cannot be empty")
|
|
85
|
+
except UnicodeDecodeError:
|
|
86
|
+
console.print(f"❌ File '{file}' is not a valid text file", style="red")
|
|
87
|
+
raise click.ClickException("File must be a valid text file") from None
|
|
88
|
+
except Exception as e:
|
|
89
|
+
console.print(f"❌ Error reading file '{file}': {e}", style="red")
|
|
90
|
+
raise click.ClickException("Failed to read file") from e
|
|
91
|
+
|
|
60
92
|
auth_manager = AuthManager(ctx.obj.get("config"))
|
|
61
93
|
article_manager = ArticleManager(auth_manager)
|
|
62
94
|
|
|
63
95
|
console.print(f"📝 Creating article '{title}'...", style="blue")
|
|
64
96
|
|
|
97
|
+
# At this point, content is guaranteed to be a string due to validation above
|
|
98
|
+
assert content is not None
|
|
99
|
+
|
|
65
100
|
try:
|
|
66
101
|
result = asyncio.run(
|
|
67
102
|
article_manager.create_article(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|