kd-cli 2.1.2__tar.gz → 2.1.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.
- {kd_cli-2.1.2/kd_cli.egg-info → kd_cli-2.1.4}/PKG-INFO +1 -1
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/api/http.py +25 -3
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/connection.py +6 -1
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/resources/CHANGELOG.md +13 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4/kd_cli.egg-info}/PKG-INFO +1 -1
- {kd_cli-2.1.2 → kd_cli-2.1.4}/pyproject.toml +1 -1
- {kd_cli-2.1.2 → kd_cli-2.1.4}/LICENSE +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/MANIFEST.in +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/README.md +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/api/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/api/articles.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/api/hub.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/api/saved_queries.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/api/tags.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/api/youtrack.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/article_marker.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/auth.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/core/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/core/command_base.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/core/command_parser.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/core/namespaces.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/core/option_registry.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/input.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/main.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/alias/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/article/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/init.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/issue/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/primer/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/project/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/query/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/release_notes.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/search/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/system/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/tag/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/time/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/namespaces/user/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/output.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/cli/tree.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/client.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/config.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/config_schema.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/debug.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/exceptions.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/history.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/article.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/assist.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/attachment.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/comment.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/common.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/issue.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/project.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/saved_query.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/search.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/tag.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/user.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/models/work_item.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/primer.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/query_parser.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/release_notes.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/resources/__init__.py +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/resources/command-synonyms.yaml +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/resources/config-template.yaml +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd/resources/error-messages.yaml +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd_cli.egg-info/SOURCES.txt +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd_cli.egg-info/dependency_links.txt +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd_cli.egg-info/entry_points.txt +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd_cli.egg-info/requires.txt +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/kd_cli.egg-info/top_level.txt +0 -0
- {kd_cli-2.1.2 → kd_cli-2.1.4}/setup.cfg +0 -0
|
@@ -61,7 +61,7 @@ class HttpClient:
|
|
|
61
61
|
if fields:
|
|
62
62
|
all_params["fields"] = fields
|
|
63
63
|
response = self._request("GET", path, params=all_params)
|
|
64
|
-
return
|
|
64
|
+
return self._decode_json(response)
|
|
65
65
|
|
|
66
66
|
def post(
|
|
67
67
|
self,
|
|
@@ -77,7 +77,7 @@ class HttpClient:
|
|
|
77
77
|
response = self._request("POST", path, json=json, params=all_params or None)
|
|
78
78
|
if response.status_code == 204 or not response.content:
|
|
79
79
|
return {}
|
|
80
|
-
return
|
|
80
|
+
return self._decode_json(response)
|
|
81
81
|
|
|
82
82
|
def post_file(
|
|
83
83
|
self,
|
|
@@ -99,7 +99,7 @@ class HttpClient:
|
|
|
99
99
|
)
|
|
100
100
|
if response.status_code == 204 or not response.content:
|
|
101
101
|
return {}
|
|
102
|
-
return
|
|
102
|
+
return self._decode_json(response)
|
|
103
103
|
|
|
104
104
|
def get_bytes(
|
|
105
105
|
self,
|
|
@@ -168,6 +168,28 @@ class HttpClient:
|
|
|
168
168
|
self._raise_for_status(last_response)
|
|
169
169
|
raise AssertionError("unreachable") # pragma: no cover
|
|
170
170
|
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _decode_json(response: httpx.Response) -> Any:
|
|
173
|
+
"""Decode a 2xx response as JSON, guarding against non-JSON bodies.
|
|
174
|
+
|
|
175
|
+
A misconfigured base URL (e.g. trailing slash producing ``//api/...``)
|
|
176
|
+
can yield a 200 HTML page from YouTrack's web UI. Surface that as a
|
|
177
|
+
clear APIError instead of letting ``JSONDecodeError`` bubble up.
|
|
178
|
+
"""
|
|
179
|
+
content_type = response.headers.get("content-type", "")
|
|
180
|
+
if "json" not in content_type.lower():
|
|
181
|
+
url = str(response.request.url) if response.request else "<unknown>"
|
|
182
|
+
raise APIError(
|
|
183
|
+
response.status_code,
|
|
184
|
+
(
|
|
185
|
+
f"Expected JSON response but got '{content_type or 'unknown'}' "
|
|
186
|
+
f"from {url}. Check the 'url' in your connection config — "
|
|
187
|
+
"a trailing slash or a non-YouTrack host can produce this."
|
|
188
|
+
),
|
|
189
|
+
response.text[:500],
|
|
190
|
+
)
|
|
191
|
+
return response.json()
|
|
192
|
+
|
|
171
193
|
@staticmethod
|
|
172
194
|
def _extract_message(body: str) -> str:
|
|
173
195
|
"""Extract a human-readable message from a YouTrack error response.
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
from dataclasses import dataclass
|
|
14
14
|
from typing import TYPE_CHECKING, Any, Literal
|
|
15
15
|
|
|
16
|
-
from pydantic import BaseModel, Field, model_validator
|
|
16
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
17
17
|
|
|
18
18
|
from kd.auth import resolve_connection_token
|
|
19
19
|
from kd.exceptions import ConfigError
|
|
@@ -82,6 +82,11 @@ class ConnectionConfig(BaseModel):
|
|
|
82
82
|
description="Version targeting (target + mode)",
|
|
83
83
|
)
|
|
84
84
|
|
|
85
|
+
@field_validator("url", mode="after")
|
|
86
|
+
@classmethod
|
|
87
|
+
def _normalize_url(cls, value: str) -> str:
|
|
88
|
+
return value.rstrip("/")
|
|
89
|
+
|
|
85
90
|
@model_validator(mode="after")
|
|
86
91
|
def _require_auth_source(self) -> ConnectionConfig:
|
|
87
92
|
if not self.token_cmd and not self.token_env:
|
|
@@ -4,6 +4,19 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.1.4] — 2026-04-15
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- Connection `url` with a trailing slash no longer produces opaque `JSONDecodeError`. URLs are now normalized on load (`https://host/` → `https://host`), and the HTTP layer guards against non-JSON 2xx responses — a misconfigured URL that lands on YouTrack's HTML web UI now raises a clear `APIError` naming the URL and content type (KDCLI-67).
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
- `docs/how-to/012-install-how-to.md` now walks through obtaining a YouTrack API token (Profile → Account Security → New token) and clarifies that the `token_env` name is user-configurable. The configuration how-to cross-links to it (KDCLI-68).
|
|
14
|
+
|
|
15
|
+
## [2.1.3] — 2026-04-15
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `sync-changelog` CI job now correctly publishes `CHANGELOG.md` on its first run against `kd-cli`. The 2.1.2 release exposed a guard that ignored untracked files, so the initial sync was a no-op and the PyPI Changelog link 404'd until this release (KDCLI-65).
|
|
19
|
+
|
|
7
20
|
## [2.1.2] — 2026-04-15
|
|
8
21
|
|
|
9
22
|
### Changed
|
|
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
|