kd-cli 2.5.0__tar.gz → 2.5.1__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 (82) hide show
  1. {kd_cli-2.5.0/kd_cli.egg-info → kd_cli-2.5.1}/PKG-INFO +1 -1
  2. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/issue/__init__.py +21 -1
  3. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/client.py +66 -34
  4. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/CHANGELOG.md +8 -0
  5. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/operating-guide.md +2 -0
  6. {kd_cli-2.5.0 → kd_cli-2.5.1/kd_cli.egg-info}/PKG-INFO +1 -1
  7. {kd_cli-2.5.0 → kd_cli-2.5.1}/pyproject.toml +1 -1
  8. {kd_cli-2.5.0 → kd_cli-2.5.1}/LICENSE +0 -0
  9. {kd_cli-2.5.0 → kd_cli-2.5.1}/MANIFEST.in +0 -0
  10. {kd_cli-2.5.0 → kd_cli-2.5.1}/README.md +0 -0
  11. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/__init__.py +0 -0
  12. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/api/__init__.py +0 -0
  13. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/api/articles.py +0 -0
  14. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/api/http.py +0 -0
  15. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/api/hub.py +0 -0
  16. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/api/saved_queries.py +0 -0
  17. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/api/tags.py +0 -0
  18. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/api/youtrack.py +0 -0
  19. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/article_marker.py +0 -0
  20. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/auth.py +0 -0
  21. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/__init__.py +0 -0
  22. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/core/__init__.py +0 -0
  23. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/core/command_base.py +0 -0
  24. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/core/command_parser.py +0 -0
  25. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/core/confirm.py +0 -0
  26. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/core/namespaces.py +0 -0
  27. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/core/option_registry.py +0 -0
  28. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/input.py +0 -0
  29. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/main.py +0 -0
  30. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/__init__.py +0 -0
  31. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/alias/__init__.py +0 -0
  32. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/article/__init__.py +0 -0
  33. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/init.py +0 -0
  34. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/primer/__init__.py +0 -0
  35. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/project/__init__.py +0 -0
  36. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/query/__init__.py +0 -0
  37. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/release_notes.py +0 -0
  38. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/search/__init__.py +0 -0
  39. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/system/__init__.py +0 -0
  40. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/tag/__init__.py +0 -0
  41. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/time/__init__.py +0 -0
  42. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/namespaces/user/__init__.py +0 -0
  43. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/output.py +0 -0
  44. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/cli/tree.py +0 -0
  45. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/config.py +0 -0
  46. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/config_read.py +0 -0
  47. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/config_schema.py +0 -0
  48. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/connection.py +0 -0
  49. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/debug.py +0 -0
  50. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/exceptions.py +0 -0
  51. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/history.py +0 -0
  52. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/locking.py +0 -0
  53. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/__init__.py +0 -0
  54. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/article.py +0 -0
  55. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/assist.py +0 -0
  56. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/attachment.py +0 -0
  57. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/comment.py +0 -0
  58. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/common.py +0 -0
  59. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/issue.py +0 -0
  60. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/project.py +0 -0
  61. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/saved_query.py +0 -0
  62. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/search.py +0 -0
  63. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/tag.py +0 -0
  64. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/tag_entities.py +0 -0
  65. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/user.py +0 -0
  66. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/models/work_item.py +0 -0
  67. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/operating_guide.py +0 -0
  68. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/primer.py +0 -0
  69. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/query_parser.py +0 -0
  70. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/release_notes.py +0 -0
  71. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/__init__.py +0 -0
  72. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/command-synonyms.yaml +0 -0
  73. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/config-template.yaml +0 -0
  74. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/error-messages.yaml +0 -0
  75. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/primers/__init__.py +0 -0
  76. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd/resources/primers/hitl.yaml +0 -0
  77. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd_cli.egg-info/SOURCES.txt +0 -0
  78. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd_cli.egg-info/dependency_links.txt +0 -0
  79. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd_cli.egg-info/entry_points.txt +0 -0
  80. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd_cli.egg-info/requires.txt +0 -0
  81. {kd_cli-2.5.0 → kd_cli-2.5.1}/kd_cli.egg-info/top_level.txt +0 -0
  82. {kd_cli-2.5.0 → kd_cli-2.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.5.0
3
+ Version: 2.5.1
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -270,11 +270,20 @@ class IssueCreateCommand(Command):
270
270
  "Create an issue",
271
271
  requires_client=True,
272
272
  usage='kd /issue create "Summary" --project P [--field NAME=VALUE] [--attach FILE]',
273
- long_description="Summary is the issue title. --project is the project key (e.g. FB).",
273
+ long_description=(
274
+ "Summary is the issue title. --project is the project key (e.g. FB). "
275
+ "--field NAME=VALUE supports bundle families (enum, state, owned, "
276
+ "version, build), user logins, period, and string custom fields; the "
277
+ "field name is fuzzy-matched and the value is passed verbatim. "
278
+ "Unsupported families (text, Multi*, and integer/float/date subtypes "
279
+ "of SimpleProjectCustomField) fail client-side with an error naming "
280
+ "both the project $type and fieldType.id subtype."
281
+ ),
274
282
  examples=[
275
283
  'kd /issue create "Fix router config" --project FB --type Task',
276
284
  'kd /issue create "Bug report" -p FB --attach screenshot.png --attach log.txt',
277
285
  'kd /issue create "Sprint task" -p FB --field "Sprint=Sprint 47"',
286
+ 'kd /issue create "Tracked" -p VIS --field "Milestone=M039"',
278
287
  'kd /issue create "Design doc" -p FB --description @design.md',
279
288
  ],
280
289
  see_also=["/issue list", "/issue show", "/issue/attachment add", "/project/field list"],
@@ -437,10 +446,21 @@ class IssueUpdateCommand(Command):
437
446
  "Update an issue",
438
447
  requires_client=True,
439
448
  usage="kd /issue update ID [--title T] [--field NAME=VALUE]",
449
+ long_description=(
450
+ "--field NAME=VALUE supports bundle families (enum, state, owned, "
451
+ "version, build), user logins, period, and string custom fields; the "
452
+ "field name is fuzzy-matched and the value is passed verbatim. "
453
+ "Clear optional custom fields with NAME= (empty value). Unsupported "
454
+ "families (text, Multi*, and integer/float/date subtypes of "
455
+ "SimpleProjectCustomField) fail client-side with an error naming "
456
+ "both the project $type and fieldType.id subtype."
457
+ ),
440
458
  examples=[
441
459
  'kd /issue update FB-42 --title "New title"',
442
460
  "kd /issue update FB-42 --state Open --priority Critical",
443
461
  'kd /issue update FB-42 --field "Sprint=Sprint 48"',
462
+ 'kd /issue update VIS-21 --field "Milestone=M039"',
463
+ 'kd /issue update FB-42 --field "Milestone="',
444
464
  "kd /issue update FB-42 --description @updated.md",
445
465
  ],
446
466
  see_also=["/issue show", "/project/field list"],
@@ -22,6 +22,7 @@ import builtins
22
22
  import difflib
23
23
  import shlex
24
24
  from collections import OrderedDict
25
+ from collections.abc import Callable
25
26
  from dataclasses import dataclass, field as dataclass_field
26
27
  from datetime import datetime, timezone
27
28
  from pathlib import Path
@@ -130,15 +131,43 @@ def _resolve_link_command(link_type: str) -> str:
130
131
  return result
131
132
 
132
133
 
133
- # Maps project custom field $type to the $type needed for issue mutations.
134
- _PROJECT_TO_ISSUE_TYPE: dict[str, str] = {
135
- "EnumProjectCustomField": "SingleEnumIssueCustomField",
136
- "StateProjectCustomField": "StateIssueCustomField",
137
- "UserProjectCustomField": "SingleUserIssueCustomField",
138
- "OwnedProjectCustomField": "SingleOwnedIssueCustomField",
139
- "VersionProjectCustomField": "MultiVersionIssueCustomField",
140
- "BuildProjectCustomField": "SingleBuildIssueCustomField",
141
- "PeriodProjectCustomField": "PeriodIssueCustomField",
134
+ # Dispatch over (project_field_type, field_type_id) → (issue_type, payload_builder).
135
+ #
136
+ # The builder wraps a validated value into the exact shape YouTrack expects in
137
+ # the ``customFields[].value`` slot of an issue create/update payload. Bundle
138
+ # families wrap as ``{"name": ...}``; user fields wrap as ``{"login": ...}``;
139
+ # the scalar simple-string family passes through verbatim.
140
+ #
141
+ # ``""`` as the second tuple element means "any field_type_id under this
142
+ # ``$type``" — used when every subtype of a family shares one payload shape.
143
+ # A specific ``field_type_id`` key wins over the wildcard when both are present
144
+ # (see ``SimpleProjectCustomField`` which only supports the ``string`` subtype
145
+ # this session; ``integer``/``float``/``date`` stay unmapped).
146
+
147
+ FieldValueBuilder = Callable[[str], Any]
148
+
149
+
150
+ def _bundle_name(value: str) -> dict[str, str]:
151
+ return {"name": value}
152
+
153
+
154
+ def _user_login(value: str) -> dict[str, str]:
155
+ return {"login": value}
156
+
157
+
158
+ def _scalar(value: str) -> str:
159
+ return value
160
+
161
+
162
+ _FIELD_DISPATCH: dict[tuple[str, str], tuple[str, FieldValueBuilder]] = {
163
+ ("EnumProjectCustomField", ""): ("SingleEnumIssueCustomField", _bundle_name),
164
+ ("StateProjectCustomField", ""): ("StateIssueCustomField", _bundle_name),
165
+ ("UserProjectCustomField", ""): ("SingleUserIssueCustomField", _user_login),
166
+ ("OwnedProjectCustomField", ""): ("SingleOwnedIssueCustomField", _bundle_name),
167
+ ("VersionProjectCustomField", ""): ("MultiVersionIssueCustomField", _bundle_name),
168
+ ("BuildProjectCustomField", ""): ("SingleBuildIssueCustomField", _bundle_name),
169
+ ("PeriodProjectCustomField", ""): ("PeriodIssueCustomField", _bundle_name),
170
+ ("SimpleProjectCustomField", "string"): ("SimpleIssueCustomField", _scalar),
142
171
  }
143
172
 
144
173
  # Default target field names for semantic CLI slots.
@@ -274,14 +303,10 @@ class FieldSchema:
274
303
  self._debug.debug("fields", f"resolving slot '{slot}' -> target '{target_name}'")
275
304
 
276
305
  field = self._find_field(project_fields, target_name, slot)
277
- issue_type = _PROJECT_TO_ISSUE_TYPE.get(field.project_field_type)
278
- if not issue_type:
279
- raise ValidationError(
280
- f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
281
- )
306
+ issue_type, builder = self._resolve_dispatch(field)
282
307
 
283
308
  validated_value = self._validate_field_value(field, value)
284
- field_value = self._field_value_payload(field, validated_value)
309
+ field_value = builder(validated_value) if validated_value is not None else None
285
310
  self._debug.debug(
286
311
  "fields",
287
312
  f"resolved '{slot}' -> field '{field.name}' ($type={issue_type})",
@@ -342,13 +367,9 @@ class FieldSchema:
342
367
  """
343
368
  project_fields = self.get_fields(project_id)
344
369
  field = self._find_field_fuzzy(project_fields, field_name)
345
- issue_type = _PROJECT_TO_ISSUE_TYPE.get(field.project_field_type)
346
- if not issue_type:
347
- raise ValidationError(
348
- f"Unsupported field type '{field.project_field_type}' for field '{field.name}'"
349
- )
370
+ issue_type, builder = self._resolve_dispatch(field)
350
371
  validated_value = self._validate_field_value(field, value)
351
- field_value = self._field_value_payload(field, validated_value)
372
+ field_value = builder(validated_value) if validated_value is not None else None
352
373
  self._debug.debug(
353
374
  "fields",
354
375
  f"resolved generic '{field_name}' -> field '{field.name}' "
@@ -388,19 +409,30 @@ class FieldSchema:
388
409
  """Return True if this field is a user-type field (user[1])."""
389
410
  return field.field_type_id.startswith("user")
390
411
 
391
- def _field_value_payload(
392
- self,
412
+ @staticmethod
413
+ def _resolve_dispatch(
393
414
  field: ProjectCustomField,
394
- value: str | None,
395
- ) -> dict[str, str] | None:
396
- """Build the YouTrack issue-field value payload.
397
-
398
- ``None`` is a deliberate clear operation and must serialize as
399
- ``"value": null`` in the final issue mutation payload.
415
+ ) -> tuple[str, FieldValueBuilder]:
416
+ """Look up the issue-side ``$type`` and payload builder for *field*.
417
+
418
+ Prefers an exact ``(project_field_type, field_type_id)`` key, then
419
+ falls back to the wildcard ``(project_field_type, "")`` key for
420
+ families where every subtype shares a payload shape. Unmapped
421
+ combinations raise a ``ValidationError`` naming both identifiers so
422
+ a follow-on ticket has the upstream type info it needs.
400
423
  """
401
- if value is None:
402
- return None
403
- return {"login": value} if self._is_user_field(field) else {"name": value}
424
+ key = (field.project_field_type, field.field_type_id)
425
+ if key in _FIELD_DISPATCH:
426
+ return _FIELD_DISPATCH[key]
427
+ wildcard = (field.project_field_type, "")
428
+ if wildcard in _FIELD_DISPATCH:
429
+ return _FIELD_DISPATCH[wildcard]
430
+ raise ValidationError(
431
+ f"Unsupported custom field type '{field.project_field_type}' "
432
+ f"with subtype '{field.field_type_id}' for field '{field.name}'. "
433
+ "kd does not know how to build a safe value payload for this "
434
+ "YouTrack field family."
435
+ )
404
436
 
405
437
  def _validate_field_value(
406
438
  self,
@@ -421,7 +453,7 @@ class FieldSchema:
421
453
  if self._is_user_field(field):
422
454
  return value # User fields: server validates against group members
423
455
  if not field.bundle_values:
424
- return value # Freeform field (period, text) — accept as-is
456
+ return value # Freeform family (period or scalar string) — server validates
425
457
 
426
458
  # Case-insensitive exact match → return canonical name
427
459
  value_lower = value.lower()
@@ -684,7 +716,7 @@ class IssueOperations:
684
716
  if "=" not in pair:
685
717
  raise ValidationError(f"Invalid field format: '{pair}'. Expected NAME=VALUE")
686
718
  name, value = pair.split("=", 1)
687
- cf = self._field_schema.resolve_field(project_obj.id, name.strip(), value.strip())
719
+ cf = self._field_schema.resolve_field(project_obj.id, name.strip(), value)
688
720
  result.append(cf)
689
721
  return result
690
722
 
@@ -4,6 +4,14 @@ All notable changes to kd-cli are recorded here. The project follows [Semantic V
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.5.1] — 2026-04-19
8
+
9
+ ### Fixed
10
+ - `kd /issue create --field NAME=VALUE` and `kd /issue update --field NAME=VALUE` now accept **string custom fields**. Before: `kd /issue update VIS-21 --field Milestone=M039` rejected with `Error: Unsupported field type 'SimpleProjectCustomField' for field 'Milestone'`, blocking workflows that track freeform per-issue metadata such as a rolling milestone string. `--field "NAME="` clears an optional string field just like it clears enum/user fields today; required string fields reject empty values with the same client-side error shipped in 2.2.2. Whitespace inside the value is preserved — `--field "Milestone= M 039 "` sends ` M 039 ` verbatim. Only the field **name** is trimmed (KDCLI-106).
11
+
12
+ ### Changed
13
+ - Unsupported custom-field errors now name both identifiers and the cause. Before: `Error: Unsupported field type 'X' for field 'Y'`. Now: `Error: Unsupported custom field type 'X' with subtype 'Y' for field 'Z'. kd does not know how to build a safe value payload for this YouTrack field family.` Applies to YouTrack text fields, multi-arity enum/user variants, and integer/float/date simple-field subtypes — paste the message into a bug report and the two type identifiers are already there (KDCLI-106).
14
+
7
15
  ## [2.5.0] — 2026-04-19
8
16
 
9
17
  Dogfooding friction fixes across tag and article workflows, plus self-describing `/system info` output for bug reports. Focus of this release: the taxonomy and article-create loops now behave the way field use expects, and a paste of `kd /system info -o yaml` replaces hand-typed version footers.
@@ -108,6 +108,8 @@ kd /project/field list KDCLI
108
108
 
109
109
  Clear optional custom fields with an empty value, for example `kd /issue update KDCLI-42 --field "Milestone="`. Required fields reject empty values before any upstream write.
110
110
 
111
+ `--field` supports bundle families (enum, state, owned, version, build), user fields, period, and string `SimpleProjectCustomField` subtypes; whitespace inside the value is preserved. Unsupported families (text, `Multi*` enum/user variants, and `SimpleProjectCustomField` with `integer` / `float` / `date` subtypes) fail client-side with a message naming both the project `$type` and the `fieldType.id` subtype so a follow-on bug report can skip the probe.
112
+
111
113
  Attachment metadata download URLs may start with `/api/`. kd handles this internally on download commands; do not assume the raw URL can be appended unchanged to an API base URL in custom scripts.
112
114
 
113
115
  Tag existence, tag visibility, and tag sharing rights are different concerns. A visible tag may not be usable for every operation, and sharing settings are provider-specific security posture, not just display metadata.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kd-cli
3
- Version: 2.5.0
3
+ Version: 2.5.1
4
4
  Summary: Knowledge Dispatcher for issue trackers and knowledge bases
5
5
  Author: Sergiy Gladkyy
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kd-cli"
7
- version = "2.5.0"
7
+ version = "2.5.1"
8
8
  description = "Knowledge Dispatcher for issue trackers and knowledge bases"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
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