kd-cli 2.0.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.
Files changed (70) hide show
  1. kd_cli-2.0.4/LICENSE +21 -0
  2. kd_cli-2.0.4/MANIFEST.in +32 -0
  3. kd_cli-2.0.4/PKG-INFO +149 -0
  4. kd_cli-2.0.4/README.md +115 -0
  5. kd_cli-2.0.4/kd/__init__.py +45 -0
  6. kd_cli-2.0.4/kd/api/__init__.py +15 -0
  7. kd_cli-2.0.4/kd/api/articles.py +232 -0
  8. kd_cli-2.0.4/kd/api/http.py +234 -0
  9. kd_cli-2.0.4/kd/api/hub.py +69 -0
  10. kd_cli-2.0.4/kd/api/saved_queries.py +76 -0
  11. kd_cli-2.0.4/kd/api/tags.py +107 -0
  12. kd_cli-2.0.4/kd/api/youtrack.py +414 -0
  13. kd_cli-2.0.4/kd/article_marker.py +348 -0
  14. kd_cli-2.0.4/kd/auth.py +96 -0
  15. kd_cli-2.0.4/kd/cli/__init__.py +5 -0
  16. kd_cli-2.0.4/kd/cli/core/__init__.py +25 -0
  17. kd_cli-2.0.4/kd/cli/core/command_base.py +133 -0
  18. kd_cli-2.0.4/kd/cli/core/command_parser.py +246 -0
  19. kd_cli-2.0.4/kd/cli/core/namespaces.py +175 -0
  20. kd_cli-2.0.4/kd/cli/core/option_registry.py +265 -0
  21. kd_cli-2.0.4/kd/cli/input.py +42 -0
  22. kd_cli-2.0.4/kd/cli/main.py +612 -0
  23. kd_cli-2.0.4/kd/cli/namespaces/__init__.py +38 -0
  24. kd_cli-2.0.4/kd/cli/namespaces/alias/__init__.py +87 -0
  25. kd_cli-2.0.4/kd/cli/namespaces/article/__init__.py +843 -0
  26. kd_cli-2.0.4/kd/cli/namespaces/init.py +42 -0
  27. kd_cli-2.0.4/kd/cli/namespaces/issue/__init__.py +959 -0
  28. kd_cli-2.0.4/kd/cli/namespaces/primer/__init__.py +337 -0
  29. kd_cli-2.0.4/kd/cli/namespaces/project/__init__.py +243 -0
  30. kd_cli-2.0.4/kd/cli/namespaces/query/__init__.py +465 -0
  31. kd_cli-2.0.4/kd/cli/namespaces/search/__init__.py +82 -0
  32. kd_cli-2.0.4/kd/cli/namespaces/system/__init__.py +528 -0
  33. kd_cli-2.0.4/kd/cli/namespaces/tag/__init__.py +337 -0
  34. kd_cli-2.0.4/kd/cli/namespaces/time/__init__.py +307 -0
  35. kd_cli-2.0.4/kd/cli/namespaces/user/__init__.py +99 -0
  36. kd_cli-2.0.4/kd/cli/output.py +287 -0
  37. kd_cli-2.0.4/kd/cli/tree.py +65 -0
  38. kd_cli-2.0.4/kd/client.py +1953 -0
  39. kd_cli-2.0.4/kd/config.py +853 -0
  40. kd_cli-2.0.4/kd/connection.py +156 -0
  41. kd_cli-2.0.4/kd/debug.py +107 -0
  42. kd_cli-2.0.4/kd/exceptions.py +189 -0
  43. kd_cli-2.0.4/kd/history.py +169 -0
  44. kd_cli-2.0.4/kd/models/__init__.py +75 -0
  45. kd_cli-2.0.4/kd/models/article.py +64 -0
  46. kd_cli-2.0.4/kd/models/assist.py +47 -0
  47. kd_cli-2.0.4/kd/models/attachment.py +20 -0
  48. kd_cli-2.0.4/kd/models/comment.py +25 -0
  49. kd_cli-2.0.4/kd/models/common.py +39 -0
  50. kd_cli-2.0.4/kd/models/issue.py +67 -0
  51. kd_cli-2.0.4/kd/models/project.py +60 -0
  52. kd_cli-2.0.4/kd/models/saved_query.py +54 -0
  53. kd_cli-2.0.4/kd/models/search.py +20 -0
  54. kd_cli-2.0.4/kd/models/tag.py +30 -0
  55. kd_cli-2.0.4/kd/models/user.py +42 -0
  56. kd_cli-2.0.4/kd/models/work_item.py +59 -0
  57. kd_cli-2.0.4/kd/primer.py +231 -0
  58. kd_cli-2.0.4/kd/query_parser.py +181 -0
  59. kd_cli-2.0.4/kd/resources/__init__.py +1 -0
  60. kd_cli-2.0.4/kd/resources/command-synonyms.yaml +19 -0
  61. kd_cli-2.0.4/kd/resources/config-template.yaml +71 -0
  62. kd_cli-2.0.4/kd/resources/error-messages.yaml +101 -0
  63. kd_cli-2.0.4/kd_cli.egg-info/PKG-INFO +149 -0
  64. kd_cli-2.0.4/kd_cli.egg-info/SOURCES.txt +68 -0
  65. kd_cli-2.0.4/kd_cli.egg-info/dependency_links.txt +1 -0
  66. kd_cli-2.0.4/kd_cli.egg-info/entry_points.txt +2 -0
  67. kd_cli-2.0.4/kd_cli.egg-info/requires.txt +13 -0
  68. kd_cli-2.0.4/kd_cli.egg-info/top_level.txt +1 -0
  69. kd_cli-2.0.4/pyproject.toml +66 -0
  70. kd_cli-2.0.4/setup.cfg +4 -0
kd_cli-2.0.4/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sergiy Gladkyy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+ include MANIFEST.in
5
+
6
+ recursive-include kd *.py *.yaml
7
+
8
+ prune tests
9
+ prune .kd
10
+ prune .claude
11
+ prune .github
12
+ prune .venv
13
+ prune playground
14
+ prune ref
15
+ prune scripts
16
+ prune docs
17
+ prune build
18
+ prune dist
19
+ prune .mypy_cache
20
+ prune .ruff_cache
21
+ prune .pytest_cache
22
+
23
+ global-exclude __pycache__
24
+ global-exclude *.py[cod]
25
+ global-exclude *.egg-info
26
+ global-exclude .DS_Store
27
+ global-exclude .env*
28
+ global-exclude .secrets*
29
+ global-exclude .mcp*.json
30
+ global-exclude uv.lock
31
+ global-exclude .envrc
32
+ global-exclude .pre-commit-config.yaml
kd_cli-2.0.4/PKG-INFO ADDED
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: kd-cli
3
+ Version: 2.0.4
4
+ Summary: Knowledge dispatcher for live knowledge systems
5
+ Author: Sergiy Gladkyy
6
+ License-Expression: MIT
7
+ Keywords: youtrack,cli,sdk,knowledge-management,knowledge-network
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: POSIX
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: tabulate>=0.9
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: pytest-httpx>=0.30; extra == "dev"
28
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
29
+ Requires-Dist: mypy>=1.0; extra == "dev"
30
+ Requires-Dist: pre-commit>=3.0; extra == "dev"
31
+ Requires-Dist: types-PyYAML; extra == "dev"
32
+ Requires-Dist: types-tabulate; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # kd
36
+
37
+ A knowledge dispatcher for live knowledge systems — pull what you need to reason, push your updates back, and stay on top of a live system rather than copying it.
38
+
39
+ ## Purpose
40
+
41
+ **kd is a knowledge-network tool, not a data-exchange tool.** It is built around a small, primer-shaped idea: the operator reasons *on top of* live trackers, not alongside copies of them. The primary loop is **pull → reason → push updates back**. Integrating multiple sources happens in the operator's head — not in kd's data model. Occasional cross-system moves exist, but they are deliberately thin: at the limit, a stub with a similar title and a backref to the original, not a faithful reproduction. kd is not a migration tool or a comprehensive import-export platform; it is a network-of-knowledge dispatcher that speaks YouTrack fluently and will, over time, speak other trackers through narrow domain protocols.
42
+
43
+ The **primer** (`kd /primer`) is the working illustration: a small set of pinned references plus curated notes, living in the workspace, sitting on top of live systems without replacing them. The rest of the tool extends that shape.
44
+
45
+ **SDK-first**: the Python library (`kd.client`) is the primary interface; CLI commands are thin wrappers over it. LLMs constructing commands programmatically are the primary consumer, so syntax clarity and structured output matter more than keystroke economy.
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ # pipx (recommended)
51
+ pipx install kd
52
+
53
+ # or uv
54
+ uv tool install kd
55
+ ```
56
+
57
+ ## Configure
58
+
59
+ ```bash
60
+ # Scaffold a project-local config in the current directory.
61
+ kd init
62
+
63
+ # Or set via environment (overrides workspace + global config).
64
+ export KD_TOKEN=your_token
65
+ export KD_CONNECTION=main
66
+ ```
67
+
68
+ Global config lives at `~/.config/kd/config.yaml`; workspace config lives at `.kd/config.yaml`.
69
+
70
+ ## Usage
71
+
72
+ ### CLI
73
+
74
+ ```bash
75
+ # Projects
76
+ kd /project list
77
+ kd /project show FB
78
+
79
+ # Issues
80
+ kd /issue list --project FB --query "#Unresolved"
81
+ kd /issue create "Fix router configuration" -p FB --type Task
82
+ kd /issue show FB-1
83
+
84
+ # Time tracking
85
+ kd /time log FB-1 45 --type Development --description "Router reconfiguration"
86
+
87
+ # Articles
88
+ kd /article list --project CMCSA
89
+ kd /article push guide.md --project CMCSA --title "Setup Guide" --tag "type:guide"
90
+ kd /article pull FB-A-42 > setup.md
91
+
92
+ # Tags
93
+ kd /tag list
94
+ kd /tag articles type:case --project CMCSA
95
+
96
+ # Saved queries
97
+ kd /query list
98
+ kd /query issues "Sprint 47" --project FB
99
+
100
+ # Primer (local knowledge, LLM context)
101
+ kd /primer/note add "Sprint field is called 'Board'"
102
+ kd /primer/article add FB-A-42 --summary "Architecture Overview"
103
+ kd /primer show
104
+
105
+ # Cross-entity search
106
+ kd /search "router" --types issue,article --project FB
107
+
108
+ # Output formats
109
+ kd /project list --output json | jq '.[].key'
110
+ kd /issue list --output yaml
111
+ ```
112
+
113
+ ### Python SDK
114
+
115
+ ```python
116
+ from kd import YouTrackClient
117
+
118
+ client = YouTrackClient(
119
+ url="https://your-instance.youtrack.cloud",
120
+ token="your_token",
121
+ )
122
+
123
+ projects = client.projects.list()
124
+ issue = client.issues.create(project="FB", summary="Fix router config")
125
+ client.time.log(issue_id="FB-1", minutes=45, work_type="Development")
126
+ articles = client.tags.articles(["type:case"], project="CMCSA")
127
+ results = client.search.search("router", types=["issue", "article"])
128
+ ```
129
+
130
+ ## What kd is not
131
+
132
+ - **Not a migration platform.** Cross-system moves are lossy cherry-picks: a stub on the target side with a backref to the source, nothing more.
133
+ - **Not a faithful mirror.** kd never claims to be a complete copy of upstream state; it reads what it needs and writes what changed.
134
+ - **Not a data warehouse.** Data lives in the upstream tracker; kd sits on top.
135
+
136
+ If you need a migration tool or an import-export platform, kd is the wrong tool.
137
+
138
+ ## Development
139
+
140
+ ```bash
141
+ pytest tests/ -v --tb=short
142
+
143
+ ruff format kd/ tests/
144
+ ruff check --fix kd/ tests/
145
+ mypy kd/
146
+ ```
147
+
148
+ ---
149
+ Updated: 2026-04-13
kd_cli-2.0.4/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # kd
2
+
3
+ A knowledge dispatcher for live knowledge systems — pull what you need to reason, push your updates back, and stay on top of a live system rather than copying it.
4
+
5
+ ## Purpose
6
+
7
+ **kd is a knowledge-network tool, not a data-exchange tool.** It is built around a small, primer-shaped idea: the operator reasons *on top of* live trackers, not alongside copies of them. The primary loop is **pull → reason → push updates back**. Integrating multiple sources happens in the operator's head — not in kd's data model. Occasional cross-system moves exist, but they are deliberately thin: at the limit, a stub with a similar title and a backref to the original, not a faithful reproduction. kd is not a migration tool or a comprehensive import-export platform; it is a network-of-knowledge dispatcher that speaks YouTrack fluently and will, over time, speak other trackers through narrow domain protocols.
8
+
9
+ The **primer** (`kd /primer`) is the working illustration: a small set of pinned references plus curated notes, living in the workspace, sitting on top of live systems without replacing them. The rest of the tool extends that shape.
10
+
11
+ **SDK-first**: the Python library (`kd.client`) is the primary interface; CLI commands are thin wrappers over it. LLMs constructing commands programmatically are the primary consumer, so syntax clarity and structured output matter more than keystroke economy.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ # pipx (recommended)
17
+ pipx install kd
18
+
19
+ # or uv
20
+ uv tool install kd
21
+ ```
22
+
23
+ ## Configure
24
+
25
+ ```bash
26
+ # Scaffold a project-local config in the current directory.
27
+ kd init
28
+
29
+ # Or set via environment (overrides workspace + global config).
30
+ export KD_TOKEN=your_token
31
+ export KD_CONNECTION=main
32
+ ```
33
+
34
+ Global config lives at `~/.config/kd/config.yaml`; workspace config lives at `.kd/config.yaml`.
35
+
36
+ ## Usage
37
+
38
+ ### CLI
39
+
40
+ ```bash
41
+ # Projects
42
+ kd /project list
43
+ kd /project show FB
44
+
45
+ # Issues
46
+ kd /issue list --project FB --query "#Unresolved"
47
+ kd /issue create "Fix router configuration" -p FB --type Task
48
+ kd /issue show FB-1
49
+
50
+ # Time tracking
51
+ kd /time log FB-1 45 --type Development --description "Router reconfiguration"
52
+
53
+ # Articles
54
+ kd /article list --project CMCSA
55
+ kd /article push guide.md --project CMCSA --title "Setup Guide" --tag "type:guide"
56
+ kd /article pull FB-A-42 > setup.md
57
+
58
+ # Tags
59
+ kd /tag list
60
+ kd /tag articles type:case --project CMCSA
61
+
62
+ # Saved queries
63
+ kd /query list
64
+ kd /query issues "Sprint 47" --project FB
65
+
66
+ # Primer (local knowledge, LLM context)
67
+ kd /primer/note add "Sprint field is called 'Board'"
68
+ kd /primer/article add FB-A-42 --summary "Architecture Overview"
69
+ kd /primer show
70
+
71
+ # Cross-entity search
72
+ kd /search "router" --types issue,article --project FB
73
+
74
+ # Output formats
75
+ kd /project list --output json | jq '.[].key'
76
+ kd /issue list --output yaml
77
+ ```
78
+
79
+ ### Python SDK
80
+
81
+ ```python
82
+ from kd import YouTrackClient
83
+
84
+ client = YouTrackClient(
85
+ url="https://your-instance.youtrack.cloud",
86
+ token="your_token",
87
+ )
88
+
89
+ projects = client.projects.list()
90
+ issue = client.issues.create(project="FB", summary="Fix router config")
91
+ client.time.log(issue_id="FB-1", minutes=45, work_type="Development")
92
+ articles = client.tags.articles(["type:case"], project="CMCSA")
93
+ results = client.search.search("router", types=["issue", "article"])
94
+ ```
95
+
96
+ ## What kd is not
97
+
98
+ - **Not a migration platform.** Cross-system moves are lossy cherry-picks: a stub on the target side with a backref to the source, nothing more.
99
+ - **Not a faithful mirror.** kd never claims to be a complete copy of upstream state; it reads what it needs and writes what changed.
100
+ - **Not a data warehouse.** Data lives in the upstream tracker; kd sits on top.
101
+
102
+ If you need a migration tool or an import-export platform, kd is the wrong tool.
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ pytest tests/ -v --tb=short
108
+
109
+ ruff format kd/ tests/
110
+ ruff check --fix kd/ tests/
111
+ mypy kd/
112
+ ```
113
+
114
+ ---
115
+ Updated: 2026-04-13
@@ -0,0 +1,45 @@
1
+ """kd — knowledge dispatcher for live knowledge systems.
2
+
3
+ SDK-first design: import YouTrackClient for programmatic access,
4
+ or use the ``kd`` command for CLI operations.
5
+
6
+ Usage::
7
+
8
+ from kd import YouTrackClient
9
+
10
+ client = YouTrackClient(url="https://example.youtrack.cloud", token="...")
11
+ projects = client.projects.list()
12
+ """
13
+
14
+ from importlib.metadata import version as _pkg_version
15
+
16
+ __version__ = _pkg_version("kd-cli")
17
+
18
+ from kd.client import YouTrackClient
19
+ from kd.config import ConfigSource, ResolvedConfig, resolve_config
20
+ from kd.debug import DebugLevel, DebugLog
21
+ from kd.exceptions import (
22
+ APIError,
23
+ AuthError,
24
+ ConfigError,
25
+ ConflictError,
26
+ NotFoundError,
27
+ ValidationError,
28
+ KdError,
29
+ )
30
+
31
+ __all__ = [
32
+ "APIError",
33
+ "AuthError",
34
+ "ConfigError",
35
+ "ConfigSource",
36
+ "ConflictError",
37
+ "DebugLevel",
38
+ "DebugLog",
39
+ "NotFoundError",
40
+ "ResolvedConfig",
41
+ "ValidationError",
42
+ "YouTrackClient",
43
+ "KdError",
44
+ "resolve_config",
45
+ ]
@@ -0,0 +1,15 @@
1
+ """Low-level API wrappers.
2
+
3
+ Four API layers, same bearer token:
4
+ - ArticlesAPI: ``/api/articles`` — knowledge base articles
5
+ - TagsAPI: ``/api/tags`` — tags, issue/article tagging
6
+ - YouTrackAPI: ``/api/...`` — projects, issues, fields, time tracking
7
+ - HubAPI: ``/hub/api/rest/...`` — users, groups, project teams, roles
8
+ """
9
+
10
+ from kd.api.articles import ArticlesAPI
11
+ from kd.api.hub import HubAPI
12
+ from kd.api.tags import TagsAPI
13
+ from kd.api.youtrack import YouTrackAPI
14
+
15
+ __all__ = ["ArticlesAPI", "HubAPI", "TagsAPI", "YouTrackAPI"]
@@ -0,0 +1,232 @@
1
+ """Articles REST API wrapper.
2
+
3
+ Covers ``/api/articles`` endpoints:
4
+ - Articles: list (global + project-scoped), get, create, update, delete
5
+ - Child articles: list, attach, detach parent
6
+ - Comments: list, create
7
+ - Attachments: list, get, create, delete
8
+
9
+ Articles have no ``query`` parameter — filtering is project-scoped only.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from kd.api.http import HttpClient
15
+ from kd.models import (
16
+ Article,
17
+ ArticleCreate,
18
+ ArticleUpdate,
19
+ Attachment,
20
+ Comment,
21
+ CommentCreate,
22
+ FieldSet,
23
+ )
24
+
25
+ # List fields omit content to keep responses lightweight
26
+ _ARTICLE_LIST_FIELDS = str(
27
+ FieldSet(
28
+ "id",
29
+ "idReadable",
30
+ "summary",
31
+ "created",
32
+ "updated",
33
+ "hasChildren",
34
+ project=FieldSet("id", "name"),
35
+ reporter=FieldSet("id", "name"),
36
+ parentArticle=FieldSet("id", "idReadable"),
37
+ )
38
+ )
39
+
40
+ # Tag-scan fields include tags for client-side tag filtering.
41
+ # Kept separate from _ARTICLE_LIST_FIELDS so normal article list
42
+ # calls don't pay the tag payload cost.
43
+ _ARTICLE_TAG_SCAN_FIELDS = str(
44
+ FieldSet(
45
+ "id",
46
+ "idReadable",
47
+ "summary",
48
+ "created",
49
+ "updated",
50
+ "hasChildren",
51
+ project=FieldSet("id", "name"),
52
+ reporter=FieldSet("id", "name"),
53
+ parentArticle=FieldSet("id", "idReadable"),
54
+ tags=FieldSet("id", "name"),
55
+ )
56
+ )
57
+
58
+ # Detail fields include content for single-article views
59
+ _ARTICLE_DETAIL_FIELDS = str(
60
+ FieldSet(
61
+ "id",
62
+ "idReadable",
63
+ "summary",
64
+ "content",
65
+ "created",
66
+ "updated",
67
+ "hasChildren",
68
+ project=FieldSet("id", "name"),
69
+ reporter=FieldSet("id", "name"),
70
+ parentArticle=FieldSet("id", "idReadable"),
71
+ tags=FieldSet("id", "name"),
72
+ )
73
+ )
74
+
75
+
76
+ _ARTICLE_COMMENT_FIELDS = "id,text,pinned,author(id,login,name),created,updated"
77
+ _ARTICLE_ATTACHMENT_FIELDS = (
78
+ "id,name,size,mimeType,extension,author(id,login,name),created,updated,url"
79
+ )
80
+
81
+
82
+ class ArticlesAPI:
83
+ """Articles REST API wrapper (/api/articles)."""
84
+
85
+ def __init__(self, http: HttpClient) -> None:
86
+ self._http = http
87
+
88
+ # --- List ---
89
+
90
+ def list_articles(self, top: int = 50, skip: int = 0) -> list[Article]:
91
+ data = self._http.get(
92
+ "/articles",
93
+ fields=_ARTICLE_LIST_FIELDS,
94
+ params={"$top": top, "$skip": skip},
95
+ )
96
+ return [Article.model_validate(a) for a in data]
97
+
98
+ def list_project_articles(self, project_id: str, top: int = 50, skip: int = 0) -> list[Article]:
99
+ data = self._http.get(
100
+ f"/admin/projects/{project_id}/articles",
101
+ fields=_ARTICLE_LIST_FIELDS,
102
+ params={"$top": top, "$skip": skip},
103
+ )
104
+ return [Article.model_validate(a) for a in data]
105
+
106
+ # --- Tag scan (articles with tags for client-side filtering) ---
107
+
108
+ def list_articles_with_tags(self, top: int = 50, skip: int = 0) -> list[Article]:
109
+ data = self._http.get(
110
+ "/articles",
111
+ fields=_ARTICLE_TAG_SCAN_FIELDS,
112
+ params={"$top": top, "$skip": skip},
113
+ )
114
+ return [Article.model_validate(a) for a in data]
115
+
116
+ def list_project_articles_with_tags(
117
+ self, project_id: str, top: int = 50, skip: int = 0
118
+ ) -> list[Article]:
119
+ data = self._http.get(
120
+ f"/admin/projects/{project_id}/articles",
121
+ fields=_ARTICLE_TAG_SCAN_FIELDS,
122
+ params={"$top": top, "$skip": skip},
123
+ )
124
+ return [Article.model_validate(a) for a in data]
125
+
126
+ # --- Get ---
127
+
128
+ def get_article(self, article_id: str) -> Article:
129
+ data = self._http.get(
130
+ f"/articles/{article_id}",
131
+ fields=_ARTICLE_DETAIL_FIELDS,
132
+ )
133
+ return Article.model_validate(data)
134
+
135
+ # --- Children ---
136
+
137
+ def list_child_articles(self, article_id: str, top: int = 50, skip: int = 0) -> list[Article]:
138
+ data = self._http.get(
139
+ f"/articles/{article_id}/childArticles",
140
+ fields=_ARTICLE_LIST_FIELDS,
141
+ params={"$top": top, "$skip": skip},
142
+ )
143
+ return [Article.model_validate(a) for a in data]
144
+
145
+ # --- Create / Update / Delete ---
146
+
147
+ def create_article(self, article: ArticleCreate) -> Article:
148
+ data = self._http.post(
149
+ "/articles",
150
+ json=article.model_dump(by_alias=True, exclude_none=True),
151
+ fields=_ARTICLE_DETAIL_FIELDS,
152
+ )
153
+ return Article.model_validate(data)
154
+
155
+ def update_article(self, article_id: str, update: ArticleUpdate) -> Article:
156
+ data = self._http.post(
157
+ f"/articles/{article_id}",
158
+ json=update.model_dump(by_alias=True, exclude_none=True),
159
+ fields=_ARTICLE_DETAIL_FIELDS,
160
+ )
161
+ return Article.model_validate(data)
162
+
163
+ def delete_article(self, article_id: str) -> None:
164
+ self._http.delete(f"/articles/{article_id}")
165
+
166
+ # --- Comments ---
167
+
168
+ def list_article_comments(self, article_id: str) -> list[Comment]:
169
+ data = self._http.get(
170
+ f"/articles/{article_id}/comments",
171
+ fields=_ARTICLE_COMMENT_FIELDS,
172
+ )
173
+ return [Comment.model_validate(c) for c in data]
174
+
175
+ def create_article_comment(self, article_id: str, comment: CommentCreate) -> Comment:
176
+ data = self._http.post(
177
+ f"/articles/{article_id}/comments",
178
+ json=comment.model_dump(exclude_none=True),
179
+ fields=_ARTICLE_COMMENT_FIELDS,
180
+ )
181
+ return Comment.model_validate(data)
182
+
183
+ # --- Attachments ---
184
+
185
+ def list_article_attachments(self, article_id: str) -> list[Attachment]:
186
+ data = self._http.get(
187
+ f"/articles/{article_id}/attachments",
188
+ fields=_ARTICLE_ATTACHMENT_FIELDS,
189
+ )
190
+ return [Attachment.model_validate(a) for a in data]
191
+
192
+ def get_article_attachment(self, article_id: str, attachment_id: str) -> Attachment:
193
+ data = self._http.get(
194
+ f"/articles/{article_id}/attachments/{attachment_id}",
195
+ fields=_ARTICLE_ATTACHMENT_FIELDS,
196
+ )
197
+ return Attachment.model_validate(data)
198
+
199
+ def create_article_attachment(
200
+ self, article_id: str, file_name: str, file_content: bytes
201
+ ) -> Attachment:
202
+ data = self._http.post_file(
203
+ f"/articles/{article_id}/attachments",
204
+ file_name=file_name,
205
+ file_content=file_content,
206
+ fields=_ARTICLE_ATTACHMENT_FIELDS,
207
+ )
208
+ if isinstance(data, list):
209
+ return Attachment.model_validate(data[0])
210
+ return Attachment.model_validate(data)
211
+
212
+ def delete_article_attachment(self, article_id: str, attachment_id: str) -> None:
213
+ self._http.delete(f"/articles/{article_id}/attachments/{attachment_id}")
214
+
215
+ # --- Hierarchy ---
216
+
217
+ def attach_child_article(self, parent_id: str, child_id: str) -> None:
218
+ """Attach an existing article as a child of another."""
219
+ self._http.post(
220
+ f"/articles/{parent_id}/childArticles",
221
+ json={"id": child_id},
222
+ )
223
+
224
+ def detach_parent_article(self, article_id: str) -> None:
225
+ """Remove an article's parent (move to project root).
226
+
227
+ Uses POST update with parentArticle set to null.
228
+ """
229
+ self._http.post(
230
+ f"/articles/{article_id}",
231
+ json={"parentArticle": None},
232
+ )