jira2cli 0.1.0__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.
- jira2cli-0.1.0/PKG-INFO +62 -0
- jira2cli-0.1.0/README.md +52 -0
- jira2cli-0.1.0/pyproject.toml +23 -0
- jira2cli-0.1.0/src/jira2cli/__init__.py +7 -0
- jira2cli-0.1.0/src/jira2cli/cli.py +26 -0
- jira2cli-0.1.0/src/jira2cli/commands/__init__.py +25 -0
- jira2cli-0.1.0/src/jira2cli/commands/attachments.py +45 -0
- jira2cli-0.1.0/src/jira2cli/commands/links.py +95 -0
- jira2cli-0.1.0/src/jira2cli/commands/metadata.py +205 -0
- jira2cli-0.1.0/src/jira2cli/commands/read.py +118 -0
- jira2cli-0.1.0/src/jira2cli/commands/search.py +69 -0
- jira2cli-0.1.0/src/jira2cli/commands/write.py +168 -0
- jira2cli-0.1.0/src/jira2cli/output.py +107 -0
- jira2cli-0.1.0/src/jira2cli/parsing.py +39 -0
jira2cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: jira2cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for Jira AI integrations powered by jira2ai-core
|
|
5
|
+
Author: en-ver
|
|
6
|
+
Requires-Dist: jira2ai-core==0.1.0
|
|
7
|
+
Requires-Dist: typer>=0.16.0
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# jira2cli
|
|
12
|
+
|
|
13
|
+
CLI adapter for Jira Cloud, powered by `jira2ai-core`.
|
|
14
|
+
|
|
15
|
+
`jira2cli` is currently intended for local and development use from this workspace. Do not assume a published `uvx` or PyPI install path yet.
|
|
16
|
+
|
|
17
|
+
## Environment
|
|
18
|
+
|
|
19
|
+
`jira2cli` uses the same Jira credentials as `jira2mcp`:
|
|
20
|
+
|
|
21
|
+
| Variable | Description |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `JIRA_URL` | Your Jira instance URL |
|
|
24
|
+
| `JIRA_USER` | Your Jira account email |
|
|
25
|
+
| `JIRA_API_TOKEN` | Your Jira API token |
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export JIRA_URL="https://yourcompany.atlassian.net"
|
|
31
|
+
export JIRA_USER="you@company.com"
|
|
32
|
+
export JIRA_API_TOKEN="your-api-token"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Local usage
|
|
36
|
+
|
|
37
|
+
From the workspace root:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv sync --all-packages --group dev
|
|
41
|
+
uv run --package jira2cli jira2cli --help
|
|
42
|
+
uv run --package jira2cli jira2cli read PROJ-123
|
|
43
|
+
uv run --package jira2cli jira2cli search 'project = PROJ ORDER BY updated DESC'
|
|
44
|
+
uv run --package jira2cli jira2cli fields --project-key PROJ
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Workspace layout
|
|
48
|
+
|
|
49
|
+
- `packages/jira2ai-core` — shared operations.
|
|
50
|
+
- `packages/jira2mcp` — MCP adapter published as `jira2mcp`.
|
|
51
|
+
- `packages/jira2cli` — CLI adapter package.
|
|
52
|
+
|
|
53
|
+
For MCP installs and Claude setup, use `uvx jira2mcp` and `claude mcp add jira -- uvx jira2mcp` as documented in the repository root README.
|
|
54
|
+
|
|
55
|
+
## Maintainers
|
|
56
|
+
|
|
57
|
+
Do not assume `uvx jira2cli` or `pip install jira2cli` is available yet. Keep using local workspace commands until the release gates in the maintainer docs are completed.
|
|
58
|
+
|
|
59
|
+
Release sequencing, package tags, and Trusted Publishing boundaries:
|
|
60
|
+
|
|
61
|
+
- <https://github.com/en-ver/jira2ai/blob/main/docs/releasing.md>
|
|
62
|
+
- <https://github.com/en-ver/jira2ai/blob/main/CONTRIBUTING.md>
|
jira2cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# jira2cli
|
|
2
|
+
|
|
3
|
+
CLI adapter for Jira Cloud, powered by `jira2ai-core`.
|
|
4
|
+
|
|
5
|
+
`jira2cli` is currently intended for local and development use from this workspace. Do not assume a published `uvx` or PyPI install path yet.
|
|
6
|
+
|
|
7
|
+
## Environment
|
|
8
|
+
|
|
9
|
+
`jira2cli` uses the same Jira credentials as `jira2mcp`:
|
|
10
|
+
|
|
11
|
+
| Variable | Description |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `JIRA_URL` | Your Jira instance URL |
|
|
14
|
+
| `JIRA_USER` | Your Jira account email |
|
|
15
|
+
| `JIRA_API_TOKEN` | Your Jira API token |
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
export JIRA_URL="https://yourcompany.atlassian.net"
|
|
21
|
+
export JIRA_USER="you@company.com"
|
|
22
|
+
export JIRA_API_TOKEN="your-api-token"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Local usage
|
|
26
|
+
|
|
27
|
+
From the workspace root:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv sync --all-packages --group dev
|
|
31
|
+
uv run --package jira2cli jira2cli --help
|
|
32
|
+
uv run --package jira2cli jira2cli read PROJ-123
|
|
33
|
+
uv run --package jira2cli jira2cli search 'project = PROJ ORDER BY updated DESC'
|
|
34
|
+
uv run --package jira2cli jira2cli fields --project-key PROJ
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Workspace layout
|
|
38
|
+
|
|
39
|
+
- `packages/jira2ai-core` — shared operations.
|
|
40
|
+
- `packages/jira2mcp` — MCP adapter published as `jira2mcp`.
|
|
41
|
+
- `packages/jira2cli` — CLI adapter package.
|
|
42
|
+
|
|
43
|
+
For MCP installs and Claude setup, use `uvx jira2mcp` and `claude mcp add jira -- uvx jira2mcp` as documented in the repository root README.
|
|
44
|
+
|
|
45
|
+
## Maintainers
|
|
46
|
+
|
|
47
|
+
Do not assume `uvx jira2cli` or `pip install jira2cli` is available yet. Keep using local workspace commands until the release gates in the maintainer docs are completed.
|
|
48
|
+
|
|
49
|
+
Release sequencing, package tags, and Trusted Publishing boundaries:
|
|
50
|
+
|
|
51
|
+
- <https://github.com/en-ver/jira2ai/blob/main/docs/releasing.md>
|
|
52
|
+
- <https://github.com/en-ver/jira2ai/blob/main/CONTRIBUTING.md>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jira2cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CLI for Jira AI integrations powered by jira2ai-core"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "en-ver" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"jira2ai-core==0.1.0",
|
|
12
|
+
"typer>=0.16.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.uv.sources]
|
|
16
|
+
jira2ai-core = { workspace = true }
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
jira2cli = "jira2cli:main"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.10.4,<0.11.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Typer application for jira2cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .commands import register_commands
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="jira2cli",
|
|
11
|
+
help="Jira CLI powered by jira2ai-core.",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
add_completion=False,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
register_commands(app)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback()
|
|
20
|
+
def callback() -> None:
|
|
21
|
+
"""Jira CLI powered by jira2ai-core."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
"""Run the jira2cli application."""
|
|
26
|
+
app()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Command registration for jira2cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from .attachments import register_attachment_commands
|
|
8
|
+
from .links import register_link_commands
|
|
9
|
+
from .metadata import register_metadata_commands
|
|
10
|
+
from .read import register_read_commands
|
|
11
|
+
from .search import register_search_commands
|
|
12
|
+
from .write import register_write_commands
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_commands(app: typer.Typer) -> None:
|
|
16
|
+
"""Register jira2cli commands on the root Typer app."""
|
|
17
|
+
register_read_commands(app)
|
|
18
|
+
register_search_commands(app)
|
|
19
|
+
register_metadata_commands(app)
|
|
20
|
+
register_write_commands(app)
|
|
21
|
+
register_link_commands(app)
|
|
22
|
+
register_attachment_commands(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["register_commands"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Attachment-download jira2cli commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from jira2ai_core import client
|
|
7
|
+
from jira2ai_core.operations import attachments as attachment_operations
|
|
8
|
+
|
|
9
|
+
from jira2cli.output import raise_cli_exception
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def attachment_command(
|
|
13
|
+
attachment_id: str = typer.Argument(..., help="Attachment ID (e.g. 63899)"),
|
|
14
|
+
output_path: str | None = typer.Option(
|
|
15
|
+
None,
|
|
16
|
+
"--output-path",
|
|
17
|
+
help=(
|
|
18
|
+
"Path to save the attachment. Can be a directory or a full file path. "
|
|
19
|
+
"Defaults to the current directory."
|
|
20
|
+
),
|
|
21
|
+
),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Download a Jira attachment by its ID."""
|
|
24
|
+
try:
|
|
25
|
+
attachment_operations.validate_attachment_id(attachment_id)
|
|
26
|
+
api = client.get_api()
|
|
27
|
+
plan = attachment_operations.plan_attachment_download(
|
|
28
|
+
attachment_id,
|
|
29
|
+
output_path=output_path,
|
|
30
|
+
api=api,
|
|
31
|
+
)
|
|
32
|
+
attachment_operations.download_attachment_content(plan, api=api)
|
|
33
|
+
output = attachment_operations.format_attachment_download_result(plan)
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
raise_cli_exception(exc)
|
|
36
|
+
|
|
37
|
+
typer.echo(output)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_attachment_commands(app: typer.Typer) -> None:
|
|
41
|
+
"""Register attachment-download commands."""
|
|
42
|
+
app.command("attachment")(attachment_command)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["attachment_command", "register_attachment_commands"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Link-management jira2cli commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from jira2ai_core import client
|
|
7
|
+
from jira2ai_core.operations import links
|
|
8
|
+
|
|
9
|
+
from jira2cli.output import (
|
|
10
|
+
raise_cli_exception,
|
|
11
|
+
render_operation_result,
|
|
12
|
+
validate_output_options,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_link_command(
|
|
17
|
+
link_type: str = typer.Argument(..., help="Link type name (e.g. Blocks, Clones)"),
|
|
18
|
+
outward_key: str = typer.Argument(..., help="Issue key on the outward side."),
|
|
19
|
+
inward_key: str = typer.Argument(..., help="Issue key on the inward side."),
|
|
20
|
+
raw_output: bool = typer.Option(
|
|
21
|
+
False,
|
|
22
|
+
"--raw",
|
|
23
|
+
help="Render the raw API payload as JSON.",
|
|
24
|
+
),
|
|
25
|
+
json_output: bool = typer.Option(
|
|
26
|
+
False,
|
|
27
|
+
"--json",
|
|
28
|
+
help="Render structured output as JSON.",
|
|
29
|
+
),
|
|
30
|
+
) -> None:
|
|
31
|
+
"""Create a link between two Jira issues."""
|
|
32
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
api = client.get_api()
|
|
36
|
+
result = links.create_issue_link(
|
|
37
|
+
link_type,
|
|
38
|
+
outward_key,
|
|
39
|
+
inward_key,
|
|
40
|
+
api=api,
|
|
41
|
+
)
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
raise_cli_exception(exc)
|
|
44
|
+
|
|
45
|
+
typer.echo(
|
|
46
|
+
render_operation_result(
|
|
47
|
+
result,
|
|
48
|
+
json_output=json_output,
|
|
49
|
+
raw_output=raw_output,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def delete_link_command(
|
|
55
|
+
link_id: str = typer.Argument(..., help="Issue link ID to delete."),
|
|
56
|
+
raw_output: bool = typer.Option(
|
|
57
|
+
False,
|
|
58
|
+
"--raw",
|
|
59
|
+
help="Render the raw API payload as JSON.",
|
|
60
|
+
),
|
|
61
|
+
json_output: bool = typer.Option(
|
|
62
|
+
False,
|
|
63
|
+
"--json",
|
|
64
|
+
help="Render structured output as JSON.",
|
|
65
|
+
),
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Delete a Jira issue link by ID."""
|
|
68
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
api = client.get_api()
|
|
72
|
+
result = links.delete_issue_link(link_id, api=api)
|
|
73
|
+
except Exception as exc:
|
|
74
|
+
raise_cli_exception(exc)
|
|
75
|
+
|
|
76
|
+
typer.echo(
|
|
77
|
+
render_operation_result(
|
|
78
|
+
result,
|
|
79
|
+
json_output=json_output,
|
|
80
|
+
raw_output=raw_output,
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def register_link_commands(app: typer.Typer) -> None:
|
|
86
|
+
"""Register link-management commands."""
|
|
87
|
+
app.command("add-link")(add_link_command)
|
|
88
|
+
app.command("delete-link")(delete_link_command)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
__all__ = [
|
|
92
|
+
"add_link_command",
|
|
93
|
+
"delete_link_command",
|
|
94
|
+
"register_link_commands",
|
|
95
|
+
]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Metadata and reference jira2cli commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from jira2ai_core import client
|
|
7
|
+
from jira2ai_core.errors import Jira2AIValidationError
|
|
8
|
+
from jira2ai_core.jql import JQL_REFERENCE
|
|
9
|
+
from jira2ai_core.operations import fields as field_operations
|
|
10
|
+
from jira2ai_core.operations import links, projects, users
|
|
11
|
+
|
|
12
|
+
from jira2cli.output import (
|
|
13
|
+
raise_cli_exception,
|
|
14
|
+
render_operation_result,
|
|
15
|
+
validate_output_options,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def fields_command(
|
|
20
|
+
project_key: str | None = typer.Option(
|
|
21
|
+
None,
|
|
22
|
+
"--project-key",
|
|
23
|
+
help="Project key for issue type or create-field metadata.",
|
|
24
|
+
),
|
|
25
|
+
issue_type: str | None = typer.Option(
|
|
26
|
+
None,
|
|
27
|
+
"--issue-type",
|
|
28
|
+
help="Issue type name used with --project-key for create fields.",
|
|
29
|
+
),
|
|
30
|
+
issue_key: str | None = typer.Option(
|
|
31
|
+
None,
|
|
32
|
+
"--issue-key",
|
|
33
|
+
help="Existing issue key for edit-field metadata.",
|
|
34
|
+
),
|
|
35
|
+
raw_output: bool = typer.Option(
|
|
36
|
+
False,
|
|
37
|
+
"--raw",
|
|
38
|
+
help="Render the raw API payload as JSON.",
|
|
39
|
+
),
|
|
40
|
+
json_output: bool = typer.Option(
|
|
41
|
+
False,
|
|
42
|
+
"--json",
|
|
43
|
+
help="Render structured output as JSON.",
|
|
44
|
+
),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Get field metadata for creating or editing Jira issues."""
|
|
47
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if issue_key:
|
|
51
|
+
api = client.get_api()
|
|
52
|
+
result = field_operations.get_edit_fields(issue_key, api=api)
|
|
53
|
+
else:
|
|
54
|
+
if not project_key:
|
|
55
|
+
raise Jira2AIValidationError(
|
|
56
|
+
"Provide either --project-key (to list issue types / create fields) "
|
|
57
|
+
"or --issue-key (to list edit fields)."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
api = client.get_api()
|
|
61
|
+
if issue_type:
|
|
62
|
+
result = field_operations.get_create_fields(
|
|
63
|
+
project_key,
|
|
64
|
+
issue_type,
|
|
65
|
+
api=api,
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
result = field_operations.list_issue_types(project_key, api=api)
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
raise_cli_exception(exc)
|
|
71
|
+
|
|
72
|
+
typer.echo(
|
|
73
|
+
render_operation_result(
|
|
74
|
+
result,
|
|
75
|
+
json_output=json_output,
|
|
76
|
+
raw_output=raw_output,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def projects_command(
|
|
82
|
+
query: str | None = typer.Option(
|
|
83
|
+
None,
|
|
84
|
+
"--query",
|
|
85
|
+
help="Filter by project key or name.",
|
|
86
|
+
),
|
|
87
|
+
raw_output: bool = typer.Option(
|
|
88
|
+
False,
|
|
89
|
+
"--raw",
|
|
90
|
+
help="Render the raw API payload as JSON.",
|
|
91
|
+
),
|
|
92
|
+
json_output: bool = typer.Option(
|
|
93
|
+
False,
|
|
94
|
+
"--json",
|
|
95
|
+
help="Render structured output as JSON.",
|
|
96
|
+
),
|
|
97
|
+
) -> None:
|
|
98
|
+
"""List Jira projects accessible to the current user."""
|
|
99
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
api = client.get_api()
|
|
103
|
+
result = projects.list_projects(query, api=api)
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
raise_cli_exception(exc)
|
|
106
|
+
|
|
107
|
+
typer.echo(
|
|
108
|
+
render_operation_result(
|
|
109
|
+
result,
|
|
110
|
+
json_output=json_output,
|
|
111
|
+
raw_output=raw_output,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def users_command(
|
|
117
|
+
query: str = typer.Argument(..., help="Search string for user name or email"),
|
|
118
|
+
max_results: int = typer.Option(
|
|
119
|
+
10,
|
|
120
|
+
"--max-results",
|
|
121
|
+
min=1,
|
|
122
|
+
max=50,
|
|
123
|
+
help="Maximum users to return.",
|
|
124
|
+
),
|
|
125
|
+
raw_output: bool = typer.Option(
|
|
126
|
+
False,
|
|
127
|
+
"--raw",
|
|
128
|
+
help="Render the raw API payload as JSON.",
|
|
129
|
+
),
|
|
130
|
+
json_output: bool = typer.Option(
|
|
131
|
+
False,
|
|
132
|
+
"--json",
|
|
133
|
+
help="Render structured output as JSON.",
|
|
134
|
+
),
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Search Jira users by name or email."""
|
|
137
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
api = client.get_api()
|
|
141
|
+
result = users.search_users(query, max_results=max_results, api=api)
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
raise_cli_exception(exc)
|
|
144
|
+
|
|
145
|
+
typer.echo(
|
|
146
|
+
render_operation_result(
|
|
147
|
+
result,
|
|
148
|
+
json_output=json_output,
|
|
149
|
+
raw_output=raw_output,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def link_types_command(
|
|
155
|
+
raw_output: bool = typer.Option(
|
|
156
|
+
False,
|
|
157
|
+
"--raw",
|
|
158
|
+
help="Render the raw API payload as JSON.",
|
|
159
|
+
),
|
|
160
|
+
json_output: bool = typer.Option(
|
|
161
|
+
False,
|
|
162
|
+
"--json",
|
|
163
|
+
help="Render structured output as JSON.",
|
|
164
|
+
),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""List available Jira issue link types."""
|
|
167
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
api = client.get_api()
|
|
171
|
+
result = links.list_link_types(api=api)
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
raise_cli_exception(exc)
|
|
174
|
+
|
|
175
|
+
typer.echo(
|
|
176
|
+
render_operation_result(
|
|
177
|
+
result,
|
|
178
|
+
json_output=json_output,
|
|
179
|
+
raw_output=raw_output,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def jql_syntax_command() -> None:
|
|
185
|
+
"""Print the shared JQL syntax reference."""
|
|
186
|
+
typer.echo(JQL_REFERENCE)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def register_metadata_commands(app: typer.Typer) -> None:
|
|
190
|
+
"""Register metadata and reference commands."""
|
|
191
|
+
app.command("fields")(fields_command)
|
|
192
|
+
app.command("projects")(projects_command)
|
|
193
|
+
app.command("users")(users_command)
|
|
194
|
+
app.command("link-types")(link_types_command)
|
|
195
|
+
app.command("jql-syntax")(jql_syntax_command)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
__all__ = [
|
|
199
|
+
"fields_command",
|
|
200
|
+
"jql_syntax_command",
|
|
201
|
+
"link_types_command",
|
|
202
|
+
"projects_command",
|
|
203
|
+
"register_metadata_commands",
|
|
204
|
+
"users_command",
|
|
205
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Read-oriented jira2cli commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from jira2ai_core import client
|
|
9
|
+
from jira2ai_core.operations import comments, issues
|
|
10
|
+
|
|
11
|
+
from jira2cli.output import (
|
|
12
|
+
raise_cli_exception,
|
|
13
|
+
render_operation_result,
|
|
14
|
+
validate_output_options,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_command(
|
|
19
|
+
issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
|
|
20
|
+
extra_fields: list[str] | None = typer.Option(
|
|
21
|
+
None,
|
|
22
|
+
"--extra-field",
|
|
23
|
+
help=(
|
|
24
|
+
"Additional fields to retrieve beyond the standard read fields. "
|
|
25
|
+
"May be repeated."
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
raw_output: bool = typer.Option(
|
|
29
|
+
False,
|
|
30
|
+
"--raw",
|
|
31
|
+
help="Render the raw API payload as JSON.",
|
|
32
|
+
),
|
|
33
|
+
json_output: bool = typer.Option(
|
|
34
|
+
False,
|
|
35
|
+
"--json",
|
|
36
|
+
help="Render structured output as JSON.",
|
|
37
|
+
),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Read a Jira issue by key with full details."""
|
|
40
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
api = client.get_api()
|
|
44
|
+
result = issues.read_issue(issue_key, extra_fields=extra_fields, api=api)
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
raise_cli_exception(exc)
|
|
47
|
+
|
|
48
|
+
typer.echo(
|
|
49
|
+
render_operation_result(
|
|
50
|
+
result,
|
|
51
|
+
json_output=json_output,
|
|
52
|
+
raw_output=raw_output,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def comments_command(
|
|
58
|
+
issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
|
|
59
|
+
start_at: int = typer.Option(
|
|
60
|
+
0,
|
|
61
|
+
"--start-at",
|
|
62
|
+
min=0,
|
|
63
|
+
help="Index of the first comment to return.",
|
|
64
|
+
),
|
|
65
|
+
max_results: int = typer.Option(
|
|
66
|
+
50,
|
|
67
|
+
"--max-results",
|
|
68
|
+
min=1,
|
|
69
|
+
max=100,
|
|
70
|
+
help="Maximum comments to return.",
|
|
71
|
+
),
|
|
72
|
+
order_by: Literal["created", "-created"] = typer.Option(
|
|
73
|
+
"created",
|
|
74
|
+
"--order-by",
|
|
75
|
+
help="Use created for oldest first or -created for newest first.",
|
|
76
|
+
),
|
|
77
|
+
raw_output: bool = typer.Option(
|
|
78
|
+
False,
|
|
79
|
+
"--raw",
|
|
80
|
+
help="Render the raw API payload as JSON.",
|
|
81
|
+
),
|
|
82
|
+
json_output: bool = typer.Option(
|
|
83
|
+
False,
|
|
84
|
+
"--json",
|
|
85
|
+
help="Render structured output as JSON.",
|
|
86
|
+
),
|
|
87
|
+
) -> None:
|
|
88
|
+
"""List comments on a Jira issue."""
|
|
89
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
api = client.get_api()
|
|
93
|
+
result = comments.list_comments(
|
|
94
|
+
issue_key,
|
|
95
|
+
start_at=start_at,
|
|
96
|
+
max_results=max_results,
|
|
97
|
+
order_by=order_by,
|
|
98
|
+
api=api,
|
|
99
|
+
)
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
raise_cli_exception(exc)
|
|
102
|
+
|
|
103
|
+
typer.echo(
|
|
104
|
+
render_operation_result(
|
|
105
|
+
result,
|
|
106
|
+
json_output=json_output,
|
|
107
|
+
raw_output=raw_output,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def register_read_commands(app: typer.Typer) -> None:
|
|
113
|
+
"""Register read-oriented commands."""
|
|
114
|
+
app.command("read")(read_command)
|
|
115
|
+
app.command("comments")(comments_command)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
__all__ = ["comments_command", "read_command", "register_read_commands"]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Search jira2cli commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from jira2ai_core import client
|
|
7
|
+
from jira2ai_core.operations import search as search_operations
|
|
8
|
+
|
|
9
|
+
from jira2cli.output import (
|
|
10
|
+
raise_cli_exception,
|
|
11
|
+
render_operation_result,
|
|
12
|
+
validate_output_options,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def search_command(
|
|
17
|
+
jql: str = typer.Argument(..., help="JQL query string"),
|
|
18
|
+
max_results: int = typer.Option(
|
|
19
|
+
20,
|
|
20
|
+
"--max-results",
|
|
21
|
+
min=1,
|
|
22
|
+
max=50,
|
|
23
|
+
help="Maximum issues to return.",
|
|
24
|
+
),
|
|
25
|
+
fields: list[str] | None = typer.Option(
|
|
26
|
+
None,
|
|
27
|
+
"--field",
|
|
28
|
+
help="Field to include in the search response. May be repeated.",
|
|
29
|
+
),
|
|
30
|
+
raw_output: bool = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--raw",
|
|
33
|
+
help="Render the raw API payload as JSON.",
|
|
34
|
+
),
|
|
35
|
+
json_output: bool = typer.Option(
|
|
36
|
+
False,
|
|
37
|
+
"--json",
|
|
38
|
+
help="Render structured output as JSON.",
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Search Jira issues using JQL."""
|
|
42
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
api = client.get_api()
|
|
46
|
+
result = search_operations.search_issues(
|
|
47
|
+
jql,
|
|
48
|
+
max_results=max_results,
|
|
49
|
+
fields=fields,
|
|
50
|
+
api=api,
|
|
51
|
+
)
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
raise_cli_exception(exc)
|
|
54
|
+
|
|
55
|
+
typer.echo(
|
|
56
|
+
render_operation_result(
|
|
57
|
+
result,
|
|
58
|
+
json_output=json_output,
|
|
59
|
+
raw_output=raw_output,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def register_search_commands(app: typer.Typer) -> None:
|
|
65
|
+
"""Register search commands."""
|
|
66
|
+
app.command("search")(search_command)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["register_search_commands", "search_command"]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Write-oriented jira2cli commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from jira2ai_core import client
|
|
7
|
+
from jira2ai_core.operations import comments as comment_operations
|
|
8
|
+
from jira2ai_core.operations import issues
|
|
9
|
+
|
|
10
|
+
from jira2cli.output import (
|
|
11
|
+
raise_cli_exception,
|
|
12
|
+
render_operation_result,
|
|
13
|
+
validate_output_options,
|
|
14
|
+
)
|
|
15
|
+
from jira2cli.parsing import parse_fields_json
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_command(
|
|
19
|
+
project_key: str = typer.Argument(..., help="Project key (e.g. PROJ)"),
|
|
20
|
+
issue_type: str = typer.Argument(..., help="Issue type name (e.g. Bug, Task)"),
|
|
21
|
+
summary: str = typer.Argument(..., help="Issue title / summary"),
|
|
22
|
+
description: str | None = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--description",
|
|
25
|
+
help="Issue description in markdown.",
|
|
26
|
+
),
|
|
27
|
+
fields_json: str | None = typer.Option(
|
|
28
|
+
None,
|
|
29
|
+
"--fields-json",
|
|
30
|
+
help="Additional issue fields as a JSON object.",
|
|
31
|
+
),
|
|
32
|
+
raw_output: bool = typer.Option(
|
|
33
|
+
False,
|
|
34
|
+
"--raw",
|
|
35
|
+
help="Render the raw API payload as JSON.",
|
|
36
|
+
),
|
|
37
|
+
json_output: bool = typer.Option(
|
|
38
|
+
False,
|
|
39
|
+
"--json",
|
|
40
|
+
help="Render structured output as JSON.",
|
|
41
|
+
),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Create a Jira issue."""
|
|
44
|
+
fields = parse_fields_json(fields_json)
|
|
45
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
api = client.get_api()
|
|
49
|
+
result = issues.create_issue(
|
|
50
|
+
project_key,
|
|
51
|
+
issue_type,
|
|
52
|
+
summary,
|
|
53
|
+
description=description,
|
|
54
|
+
fields=fields,
|
|
55
|
+
api=api,
|
|
56
|
+
)
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
raise_cli_exception(exc)
|
|
59
|
+
|
|
60
|
+
typer.echo(
|
|
61
|
+
render_operation_result(
|
|
62
|
+
result,
|
|
63
|
+
json_output=json_output,
|
|
64
|
+
raw_output=raw_output,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def edit_command(
|
|
70
|
+
issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
|
|
71
|
+
summary: str | None = typer.Option(
|
|
72
|
+
None,
|
|
73
|
+
"--summary",
|
|
74
|
+
help="New issue title / summary.",
|
|
75
|
+
),
|
|
76
|
+
description: str | None = typer.Option(
|
|
77
|
+
None,
|
|
78
|
+
"--description",
|
|
79
|
+
help="New issue description in markdown.",
|
|
80
|
+
),
|
|
81
|
+
fields_json: str | None = typer.Option(
|
|
82
|
+
None,
|
|
83
|
+
"--fields-json",
|
|
84
|
+
help="Additional fields to update as a JSON object.",
|
|
85
|
+
),
|
|
86
|
+
raw_output: bool = typer.Option(
|
|
87
|
+
False,
|
|
88
|
+
"--raw",
|
|
89
|
+
help="Render the raw API payload as JSON.",
|
|
90
|
+
),
|
|
91
|
+
json_output: bool = typer.Option(
|
|
92
|
+
False,
|
|
93
|
+
"--json",
|
|
94
|
+
help="Render structured output as JSON.",
|
|
95
|
+
),
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Update a Jira issue."""
|
|
98
|
+
fields = parse_fields_json(fields_json)
|
|
99
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
100
|
+
raw = raw_output or json_output
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
api = client.get_api()
|
|
104
|
+
result = issues.edit_issue(
|
|
105
|
+
issue_key,
|
|
106
|
+
summary=summary,
|
|
107
|
+
description=description,
|
|
108
|
+
fields=fields,
|
|
109
|
+
raw=raw,
|
|
110
|
+
api=api,
|
|
111
|
+
)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
raise_cli_exception(exc)
|
|
114
|
+
|
|
115
|
+
typer.echo(
|
|
116
|
+
render_operation_result(
|
|
117
|
+
result,
|
|
118
|
+
json_output=json_output,
|
|
119
|
+
raw_output=raw_output,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def comment_command(
|
|
125
|
+
issue_key: str = typer.Argument(..., help="Issue key (e.g. PROJ-123)"),
|
|
126
|
+
body: str = typer.Argument(..., help="Comment text in markdown"),
|
|
127
|
+
raw_output: bool = typer.Option(
|
|
128
|
+
False,
|
|
129
|
+
"--raw",
|
|
130
|
+
help="Render the raw API payload as JSON.",
|
|
131
|
+
),
|
|
132
|
+
json_output: bool = typer.Option(
|
|
133
|
+
False,
|
|
134
|
+
"--json",
|
|
135
|
+
help="Render structured output as JSON.",
|
|
136
|
+
),
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Add a comment to a Jira issue."""
|
|
139
|
+
validate_output_options(json_output=json_output, raw_output=raw_output)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
api = client.get_api()
|
|
143
|
+
result = comment_operations.add_comment(issue_key, body, api=api)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
raise_cli_exception(exc)
|
|
146
|
+
|
|
147
|
+
typer.echo(
|
|
148
|
+
render_operation_result(
|
|
149
|
+
result,
|
|
150
|
+
json_output=json_output,
|
|
151
|
+
raw_output=raw_output,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def register_write_commands(app: typer.Typer) -> None:
|
|
157
|
+
"""Register write-oriented commands."""
|
|
158
|
+
app.command("create")(create_command)
|
|
159
|
+
app.command("edit")(edit_command)
|
|
160
|
+
app.command("comment")(comment_command)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
__all__ = [
|
|
164
|
+
"comment_command",
|
|
165
|
+
"create_command",
|
|
166
|
+
"edit_command",
|
|
167
|
+
"register_write_commands",
|
|
168
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""CLI rendering and error helpers for jira2cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, NoReturn
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from jira2ai_core.errors import Jira2AIError, Jira2AIValidationError
|
|
10
|
+
from jira2ai_core.results import OperationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_operation_result(
|
|
14
|
+
result: OperationResult,
|
|
15
|
+
*,
|
|
16
|
+
json_output: bool = False,
|
|
17
|
+
raw_output: bool = False,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Render an operation result for CLI stdout."""
|
|
20
|
+
if json_output or raw_output:
|
|
21
|
+
return json.dumps(
|
|
22
|
+
_json_payload(result),
|
|
23
|
+
indent=2,
|
|
24
|
+
sort_keys=True,
|
|
25
|
+
default=str,
|
|
26
|
+
)
|
|
27
|
+
return result.text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _json_payload(result: OperationResult) -> Any:
|
|
31
|
+
"""Select the most structured payload available for JSON output."""
|
|
32
|
+
if result.data is not None:
|
|
33
|
+
return result.data
|
|
34
|
+
|
|
35
|
+
if result.raw_content is None:
|
|
36
|
+
return result.text
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
return json.loads(result.raw_content)
|
|
40
|
+
except json.JSONDecodeError:
|
|
41
|
+
return result.raw_content
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def format_cli_error(error: Jira2AIError) -> str:
|
|
45
|
+
"""Format a core error for CLI stderr."""
|
|
46
|
+
if not error.details:
|
|
47
|
+
return error.message
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
f"{error.message}\n"
|
|
51
|
+
f"Details:\n{json.dumps(error.details, indent=2, sort_keys=True, default=str)}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def error_exit_code(error: Jira2AIError) -> int:
|
|
56
|
+
"""Return the CLI exit code for a core error."""
|
|
57
|
+
if isinstance(error, Jira2AIValidationError):
|
|
58
|
+
return 2
|
|
59
|
+
return 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def raise_cli_usage_error(
|
|
63
|
+
message: str,
|
|
64
|
+
*,
|
|
65
|
+
param_hint: str | None = None,
|
|
66
|
+
) -> NoReturn:
|
|
67
|
+
"""Write a CLI usage error to stderr and exit with code 2."""
|
|
68
|
+
if param_hint:
|
|
69
|
+
typer.echo(f"{param_hint}: {message}", err=True)
|
|
70
|
+
else:
|
|
71
|
+
typer.echo(message, err=True)
|
|
72
|
+
raise typer.Exit(code=2)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_output_options(*, json_output: bool, raw_output: bool) -> None:
|
|
76
|
+
"""Reject conflicting structured output flags."""
|
|
77
|
+
if json_output and raw_output:
|
|
78
|
+
raise_cli_usage_error(
|
|
79
|
+
"Use only one of --json or --raw.",
|
|
80
|
+
param_hint="--json / --raw",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def raise_cli_error(error: Jira2AIError) -> NoReturn:
|
|
85
|
+
"""Write a CLI-friendly core error message to stderr and exit."""
|
|
86
|
+
typer.echo(format_cli_error(error), err=True)
|
|
87
|
+
raise typer.Exit(code=error_exit_code(error))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def raise_cli_exception(error: Exception) -> NoReturn:
|
|
91
|
+
"""Write a CLI-friendly error message to stderr and exit."""
|
|
92
|
+
if isinstance(error, Jira2AIError):
|
|
93
|
+
raise_cli_error(error)
|
|
94
|
+
|
|
95
|
+
typer.echo(str(error), err=True)
|
|
96
|
+
raise typer.Exit(code=1)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"error_exit_code",
|
|
101
|
+
"format_cli_error",
|
|
102
|
+
"raise_cli_error",
|
|
103
|
+
"raise_cli_exception",
|
|
104
|
+
"raise_cli_usage_error",
|
|
105
|
+
"render_operation_result",
|
|
106
|
+
"validate_output_options",
|
|
107
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Parsing helpers for jira2cli options and arguments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from jira2cli.output import raise_cli_usage_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_json_object(
|
|
12
|
+
value: str | None,
|
|
13
|
+
*,
|
|
14
|
+
option_name: str,
|
|
15
|
+
) -> dict[str, Any] | None:
|
|
16
|
+
"""Parse an optional JSON object string into a dictionary."""
|
|
17
|
+
if value is None:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
parsed = json.loads(value)
|
|
22
|
+
except json.JSONDecodeError as exc:
|
|
23
|
+
raise_cli_usage_error(
|
|
24
|
+
f"must be valid JSON ({exc.msg} at line {exc.lineno}, column {exc.colno})",
|
|
25
|
+
param_hint=option_name,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if not isinstance(parsed, dict):
|
|
29
|
+
raise_cli_usage_error("must be a JSON object", param_hint=option_name)
|
|
30
|
+
|
|
31
|
+
return parsed
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_fields_json(value: str | None) -> dict[str, Any] | None:
|
|
35
|
+
"""Parse the ``--fields-json`` option into a dictionary."""
|
|
36
|
+
return parse_json_object(value, option_name="--fields-json")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["parse_fields_json", "parse_json_object"]
|