phable-cli 0.1.0__py3-none-any.whl
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.
- phable_cli/cli.py +246 -0
- phable_cli/phabricator.py +164 -0
- phable_cli/utils.py +28 -0
- phable_cli-0.1.0.dist-info/LICENSE +21 -0
- phable_cli-0.1.0.dist-info/METADATA +56 -0
- phable_cli-0.1.0.dist-info/RECORD +8 -0
- phable_cli-0.1.0.dist-info/WHEEL +4 -0
- phable_cli-0.1.0.dist-info/entry_points.txt +3 -0
phable_cli/cli.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from .phabricator import PhabricatorClient
|
|
7
|
+
from .utils import text_from_cli_arg_or_fs_or_editor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def cli():
|
|
12
|
+
"""Manage Phabricator tasks from the comfort of your terminal"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Task(int):
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_str(cls, value: str) -> int:
|
|
19
|
+
return int(value.lstrip("T"))
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_int(cls, value: int) -> str:
|
|
23
|
+
return f"T{value}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@cli.command(name="show")
|
|
27
|
+
@click.option(
|
|
28
|
+
"--format",
|
|
29
|
+
type=click.Choice(("plain", "json")),
|
|
30
|
+
default="plain",
|
|
31
|
+
help="Output format",
|
|
32
|
+
)
|
|
33
|
+
@click.argument("task-id", type=Task.from_str)
|
|
34
|
+
def show_task(task_id: int, format: str = "plain"):
|
|
35
|
+
"""Show task details
|
|
36
|
+
|
|
37
|
+
\b
|
|
38
|
+
Examples:
|
|
39
|
+
$ phable show T123456 # show task details as plaintext
|
|
40
|
+
$ phable show T123456 --format=json # show task details as json
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
client = PhabricatorClient()
|
|
44
|
+
if task := client.show_task(task_id):
|
|
45
|
+
author = client.show_user(phid=task["fields"]["authorPHID"])
|
|
46
|
+
if owner_id := task["fields"]["ownerPHID"]:
|
|
47
|
+
owner = client.show_user(phid=owner_id)["fields"]["username"]
|
|
48
|
+
else:
|
|
49
|
+
owner = "Unassigned"
|
|
50
|
+
if project_ids := task["attachments"]["projects"]["projectPHIDs"]:
|
|
51
|
+
tags = [
|
|
52
|
+
(
|
|
53
|
+
f"{project['fields']['parent']['name']} - {project['fields']['name']}"
|
|
54
|
+
if project["fields"]["parent"]
|
|
55
|
+
else project["fields"]["name"]
|
|
56
|
+
)
|
|
57
|
+
for project in client.show_projects(phids=project_ids)
|
|
58
|
+
]
|
|
59
|
+
else:
|
|
60
|
+
tags = []
|
|
61
|
+
subtasks = client.find_subtasks(parent_id=task_id)
|
|
62
|
+
task["subtasks"] = subtasks
|
|
63
|
+
parent = client.find_parent_task(subtask_id=task_id)
|
|
64
|
+
task["parent"] = parent
|
|
65
|
+
if format == "json":
|
|
66
|
+
click.echo(json.dumps(task))
|
|
67
|
+
else:
|
|
68
|
+
parent_str = (
|
|
69
|
+
f"{Task.from_int(parent['id'])} - {parent['fields']['name']}"
|
|
70
|
+
if parent
|
|
71
|
+
else ""
|
|
72
|
+
)
|
|
73
|
+
click.echo(f"URL: {client.base_url}/{Task.from_int(task_id)}")
|
|
74
|
+
click.echo(f"Task: {Task.from_int(task_id)}")
|
|
75
|
+
click.echo(f"Title: {task['fields']['name']}")
|
|
76
|
+
click.echo(f"Author: {author['fields']['username']}")
|
|
77
|
+
click.echo(f"Owner: {owner}")
|
|
78
|
+
click.echo(f"Tags: {', '.join(tags)}")
|
|
79
|
+
click.echo(f"Status: {task['fields']['status']['name']}")
|
|
80
|
+
click.echo(f"Priority: {task['fields']['priority']['name']}")
|
|
81
|
+
click.echo(f"Description: {task['fields']['description']['raw']}")
|
|
82
|
+
click.echo(f"Parent: {parent_str}")
|
|
83
|
+
click.echo("Subtasks:")
|
|
84
|
+
if subtasks:
|
|
85
|
+
for subtask in subtasks:
|
|
86
|
+
status = f"{'[x]' if subtask['fields']['status']['value'] == 'resolved' else '[ ]'}"
|
|
87
|
+
if subtask_owner_id := subtask["fields"]["ownerPHID"]:
|
|
88
|
+
owner = client.show_user(subtask_owner_id)["fields"]["username"]
|
|
89
|
+
else:
|
|
90
|
+
owner = ""
|
|
91
|
+
click.echo(
|
|
92
|
+
f"{status} - {Task.from_int(subtask['id'])} - @{owner:<10} - {subtask['fields']['name']}"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
click.echo(f"Task {Task.from_int(task_id)} not found")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cli.command(name="create")
|
|
99
|
+
@click.option("--title", required=True, help="Title of the task")
|
|
100
|
+
@click.option(
|
|
101
|
+
"--description",
|
|
102
|
+
help="Task description or path to a file containing the description body. If not provided, an editor will be opened.",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--priority",
|
|
106
|
+
type=click.Choice(["unbreaknow", "high", "normal", "low", "needs-triage"]),
|
|
107
|
+
help="Priority level of the task",
|
|
108
|
+
default="normal",
|
|
109
|
+
)
|
|
110
|
+
@click.option("--parent-id", type=int, help="ID of parent task")
|
|
111
|
+
@click.pass_context
|
|
112
|
+
def create_task(
|
|
113
|
+
ctx,
|
|
114
|
+
title: str,
|
|
115
|
+
description: str,
|
|
116
|
+
priority: str,
|
|
117
|
+
parent_id: str | None,
|
|
118
|
+
):
|
|
119
|
+
"""Create a new task
|
|
120
|
+
|
|
121
|
+
\b
|
|
122
|
+
Examples:
|
|
123
|
+
# Create a task with associated title, priority and desription
|
|
124
|
+
$ phable create --title 'Do the thing!' --priority high --description 'Address the thing right now'
|
|
125
|
+
\b
|
|
126
|
+
# Create a task with a given parent
|
|
127
|
+
$ phable create --title 'A subtask' --description 'Subtask description' --parent-id T123456
|
|
128
|
+
\b
|
|
129
|
+
# Create a task with a long description by pointing it to a description file
|
|
130
|
+
$ phable create --title 'A task' --description path/to/description.txt
|
|
131
|
+
\b
|
|
132
|
+
# Create a task with a long description by writing it in your favorite text editor
|
|
133
|
+
$ phable create --title 'A task'
|
|
134
|
+
"""
|
|
135
|
+
client = PhabricatorClient()
|
|
136
|
+
description = text_from_cli_arg_or_fs_or_editor(description)
|
|
137
|
+
task_params = {
|
|
138
|
+
"title": title,
|
|
139
|
+
"description": description,
|
|
140
|
+
"projects.add": [os.environ["PHABRICATOR_DEFAULT_PROJECT_PHID"]],
|
|
141
|
+
"priority": priority,
|
|
142
|
+
}
|
|
143
|
+
if parent_id:
|
|
144
|
+
parent = client.show_task(parent_id)
|
|
145
|
+
task_params["parents.add"] = [parent["phid"]]
|
|
146
|
+
|
|
147
|
+
task = client.create_or_edit_task(task_params)
|
|
148
|
+
ctx.invoke(show_task, task_id=task["result"]["object"]["id"])
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@cli.command(name="assign")
|
|
152
|
+
@click.option(
|
|
153
|
+
"--username",
|
|
154
|
+
required=False,
|
|
155
|
+
help="The username to assign the task to. Self-assign the task if not provided.",
|
|
156
|
+
)
|
|
157
|
+
@click.argument("task-ids", type=Task.from_str, nargs=-1)
|
|
158
|
+
@click.pass_context
|
|
159
|
+
def assign_task(ctx, task_ids: list[int], username: str | None):
|
|
160
|
+
"""Assign one or multiple task ids to a username
|
|
161
|
+
|
|
162
|
+
\b
|
|
163
|
+
Examples:
|
|
164
|
+
$ phable assign T123456 # self assign task
|
|
165
|
+
$ phable assign T123456 brouberol # asign to username
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
client = PhabricatorClient()
|
|
169
|
+
if not username:
|
|
170
|
+
user = client.current_user()
|
|
171
|
+
else:
|
|
172
|
+
user = client.find_user_by_username(username)
|
|
173
|
+
if not user:
|
|
174
|
+
ctx.fail(f"User {username} was not found")
|
|
175
|
+
for task_id in task_ids:
|
|
176
|
+
client.assign_task_to_user(task_id=task_id, user_phid=user["phid"])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@cli.command(name="move")
|
|
180
|
+
@click.option(
|
|
181
|
+
"--column",
|
|
182
|
+
type=str,
|
|
183
|
+
required=True,
|
|
184
|
+
help="Name of destination column on the current project board",
|
|
185
|
+
)
|
|
186
|
+
@click.argument("task-ids", type=Task.from_str, nargs=1)
|
|
187
|
+
@click.pass_context
|
|
188
|
+
def move_task(ctx, task_ids: list[int], column: str | None):
|
|
189
|
+
"""Move one or several task on their current project board
|
|
190
|
+
|
|
191
|
+
If the task is moved to a 'Done' column, it will be automatically
|
|
192
|
+
marked as 'Resolved' as well.
|
|
193
|
+
|
|
194
|
+
\b
|
|
195
|
+
Example:
|
|
196
|
+
$ phable move T123456 --column 'In Progress'
|
|
197
|
+
$ phable move T123456 T234567 --column 'Done'
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
client = PhabricatorClient()
|
|
201
|
+
if not (
|
|
202
|
+
current_milestone := client.get_project_current_milestone(
|
|
203
|
+
project_phid=os.environ["PHABRICATOR_DEFAULT_PROJECT_PHID"]
|
|
204
|
+
)
|
|
205
|
+
):
|
|
206
|
+
ctx.fail("Current milestone not found")
|
|
207
|
+
current_milestone_columns = client.list_project_columns(
|
|
208
|
+
project_phid=current_milestone["fields"]["proxyPHID"]
|
|
209
|
+
)
|
|
210
|
+
for col in current_milestone_columns:
|
|
211
|
+
if col["fields"]["name"].lower() == column:
|
|
212
|
+
column_phid = col["phid"]
|
|
213
|
+
break
|
|
214
|
+
else:
|
|
215
|
+
ctx.fail(
|
|
216
|
+
f"Column {column} not found in milestone {current_milestone['fields']['name']}"
|
|
217
|
+
)
|
|
218
|
+
for task_id in task_ids:
|
|
219
|
+
client.move_task_to_column(task_id=task_id, column_phid=column_phid)
|
|
220
|
+
if column["fields"]["name"].lower() == "done":
|
|
221
|
+
client.mark_task_as_resolved(task_id)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@cli.command(name="comment")
|
|
225
|
+
@click.option(
|
|
226
|
+
"--comment",
|
|
227
|
+
type=str,
|
|
228
|
+
help="Comment text or path to a text file containing the comment body. If not provided, an editor will be opened.",
|
|
229
|
+
)
|
|
230
|
+
@click.argument("task-id", type=Task.from_str)
|
|
231
|
+
def comment_on_task(task_id: int, comment: str | None):
|
|
232
|
+
"""Add a comment to a task
|
|
233
|
+
|
|
234
|
+
\b
|
|
235
|
+
Example:
|
|
236
|
+
$ phable comment T123456 --comment 'hello' # set comment body from the cli itself
|
|
237
|
+
$ phable comment T123456 --comment path/to/comment.txt # set comment body from a text file
|
|
238
|
+
$ phable comment T123456 # set comment body from your own text editor
|
|
239
|
+
"""
|
|
240
|
+
client = PhabricatorClient()
|
|
241
|
+
comment = text_from_cli_arg_or_fs_or_editor(comment)
|
|
242
|
+
client.create_or_edit_task(task_id=task_id, params={"comment": comment})
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
cli()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
from typing import Any, TypeVar
|
|
5
|
+
from functools import cache
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PhabricatorClient:
|
|
12
|
+
"""Phabricator API HTTP client.
|
|
13
|
+
|
|
14
|
+
See https://phabricator.wikimedia.org/conduit for the API capability and details.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.base_url = os.environ["PHABRICATOR_URL"].rstrip("/")
|
|
20
|
+
self.token = os.environ["PHABRICATOR_TOKEN"]
|
|
21
|
+
self.session = requests.Session()
|
|
22
|
+
self.timeout = 5
|
|
23
|
+
|
|
24
|
+
if not self.base_url or not self.token:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
"PHABRICATOR_URL and PHABRICATOR_TOKEN must be set in your envionment"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def _first(self, result_set: list[T]) -> T:
|
|
30
|
+
if result_set:
|
|
31
|
+
return result_set[0]
|
|
32
|
+
|
|
33
|
+
def _make_request(
|
|
34
|
+
self,
|
|
35
|
+
path: str,
|
|
36
|
+
params: dict[str, Any] = None,
|
|
37
|
+
headers: dict[str, str] = None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Helper method to make API requests"""
|
|
40
|
+
headers = headers or {}
|
|
41
|
+
headers |= {
|
|
42
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
43
|
+
}
|
|
44
|
+
params = params or {}
|
|
45
|
+
data = {}
|
|
46
|
+
data["api.token"] = self.token
|
|
47
|
+
data["output"] = "json"
|
|
48
|
+
data |= params
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
response = self.session.post(
|
|
52
|
+
f"{self.base_url}/api/{path}",
|
|
53
|
+
headers=headers,
|
|
54
|
+
data=data,
|
|
55
|
+
timeout=self.timeout,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
resp_json = response.json()
|
|
60
|
+
if resp_json["error_code"]:
|
|
61
|
+
raise Exception(f"API request failed: {resp_json}")
|
|
62
|
+
return response.json()
|
|
63
|
+
except requests.RequestException as e:
|
|
64
|
+
raise Exception(f"API request failed: {str(e)}")
|
|
65
|
+
|
|
66
|
+
def create_or_edit_task(
|
|
67
|
+
self, params: dict[str, Any], task_id: int | None = None
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
"""Create or edit (if a task_id is provided) a Maniphest task."""
|
|
70
|
+
raw_params = {}
|
|
71
|
+
for i, (key, value) in enumerate(params.items()):
|
|
72
|
+
raw_params[f"transactions[{i}][type]"] = key
|
|
73
|
+
if isinstance(value, list):
|
|
74
|
+
for j, subvalue in enumerate(value):
|
|
75
|
+
raw_params[f"transactions[{i}][value][{j}]"] = subvalue
|
|
76
|
+
else:
|
|
77
|
+
raw_params[f"transactions[{i}][value]"] = value
|
|
78
|
+
if task_id:
|
|
79
|
+
raw_params["objectIdentifier"] = task_id
|
|
80
|
+
return self._make_request("maniphest.edit", params=raw_params)
|
|
81
|
+
|
|
82
|
+
def show_task(self, task_id: int) -> dict[str, Any]:
|
|
83
|
+
"""Show a Maniphest task"""
|
|
84
|
+
return self._make_request(
|
|
85
|
+
"maniphest.search",
|
|
86
|
+
params={
|
|
87
|
+
"constraints[ids][0]": task_id,
|
|
88
|
+
"attachments[subscribers]": "true",
|
|
89
|
+
"attachments[projects]": "true",
|
|
90
|
+
"attachments[columns]": "true",
|
|
91
|
+
},
|
|
92
|
+
)["result"]["data"][0]
|
|
93
|
+
|
|
94
|
+
def find_subtasks(self, parent_id: int) -> list[dict[str, Any]]:
|
|
95
|
+
"""Return details of all Maniphest subtasks of the provided task id"""
|
|
96
|
+
return self._make_request(
|
|
97
|
+
"maniphest.search", params={"constraints[parentIDs][0]": parent_id}
|
|
98
|
+
)["result"]["data"]
|
|
99
|
+
|
|
100
|
+
def find_parent_task(self, subtask_id: int) -> dict[str, Any] | None:
|
|
101
|
+
"""Return details of the parent Maniphest task for the provided task id"""
|
|
102
|
+
return self._first(
|
|
103
|
+
self._make_request(
|
|
104
|
+
"maniphest.search", params={"constraints[subtaskIDs][0]": subtask_id}
|
|
105
|
+
)["result"]["data"]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def move_task_to_column(self, task_id: int, column_phid: str) -> dict[str, Any]:
|
|
109
|
+
"""Move the argument task to column of associated column id"""
|
|
110
|
+
return self.create_or_edit_task(task_id=task_id, params={"column": column_phid})
|
|
111
|
+
|
|
112
|
+
def mark_task_as_resolved(self, task_id: int) -> dict[str, Any]:
|
|
113
|
+
"""Set the status of the argument task to Resolved"""
|
|
114
|
+
return self.create_or_edit_task(task_id=task_id, params={"status": "Resolved"})
|
|
115
|
+
|
|
116
|
+
@cache
|
|
117
|
+
def show_user(self, phid: str) -> dict[str, Any] | None:
|
|
118
|
+
"""Show details of a Maniphest user"""
|
|
119
|
+
user = self._make_request(
|
|
120
|
+
"user.search", params={"constraints[phids][0]": phid}
|
|
121
|
+
)["result"]["data"]
|
|
122
|
+
return self._first(user)
|
|
123
|
+
|
|
124
|
+
def show_projects(self, phids: list[str]) -> dict[str, Any]:
|
|
125
|
+
"""Show details of the provided Maniphest projects"""
|
|
126
|
+
params = {}
|
|
127
|
+
for i, phid in enumerate(phids):
|
|
128
|
+
params[f"constraints[phids][{i}]"] = phid
|
|
129
|
+
return self._make_request("project.search", params=params)["result"]["data"]
|
|
130
|
+
|
|
131
|
+
def current_user(self) -> dict[str, Any]:
|
|
132
|
+
"""Return details of the user associated with the phabricator API token"""
|
|
133
|
+
return self._make_request("user.whoami")["result"]
|
|
134
|
+
|
|
135
|
+
def find_user_by_username(self, username: str) -> dict[str, Any] | None:
|
|
136
|
+
"""Return user details of the user with the provided username"""
|
|
137
|
+
user = self._make_request(
|
|
138
|
+
"user.search", params={"constraints[usernames][0]": username}
|
|
139
|
+
)["result"]["data"]
|
|
140
|
+
return self._first(user)
|
|
141
|
+
|
|
142
|
+
def assign_task_to_user(self, task_id: int, user_phid: int) -> dict[str, Any]:
|
|
143
|
+
"""Set the owner of the argument task to the argument user id"""
|
|
144
|
+
return self.create_or_edit_task(task_id=task_id, params={"owner": user_phid})
|
|
145
|
+
|
|
146
|
+
def list_project_columns(
|
|
147
|
+
self,
|
|
148
|
+
project_phid: str,
|
|
149
|
+
) -> list[dict[str, Any]]:
|
|
150
|
+
"""Return the details of each column in a given project"""
|
|
151
|
+
return self._make_request(
|
|
152
|
+
"project.column.search", params={"constraints[projects][0]": project_phid}
|
|
153
|
+
)["result"]["data"]
|
|
154
|
+
|
|
155
|
+
def get_project_current_milestone(self, project_phid: str) -> dict[str, Any] | None:
|
|
156
|
+
"""Return the first non hidden column associated with a subproject.
|
|
157
|
+
|
|
158
|
+
We assume it to be associated with the current milestone.
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
columns = self.list_project_columns(project_phid)
|
|
162
|
+
for column in columns:
|
|
163
|
+
if column["fields"]["proxyPHID"] and not column["fields"]["isHidden"]:
|
|
164
|
+
return column
|
phable_cli/utils.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def text_from_cli_arg_or_fs_or_editor(body_or_path: str) -> str:
|
|
8
|
+
"""Return argument text/file content, or return prompted input text.
|
|
9
|
+
|
|
10
|
+
If some argument text is passed, and it matches a file path, return the file content.
|
|
11
|
+
If it does not match a file path, return the text itself.
|
|
12
|
+
Finally, if no argument is passed, open an editor and return the text written by the
|
|
13
|
+
user.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
if body_or_path is not None and (local_file := Path(body_or_path)).exists():
|
|
18
|
+
return local_file.read_text()
|
|
19
|
+
except OSError:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
if not body_or_path:
|
|
23
|
+
txt_tmpfile = tempfile.NamedTemporaryFile(
|
|
24
|
+
encoding="utf-8", mode="w", suffix=".md"
|
|
25
|
+
)
|
|
26
|
+
subprocess.run([os.environ["EDITOR"], txt_tmpfile.name])
|
|
27
|
+
return Path(txt_tmpfile.name).read_text()
|
|
28
|
+
return body_or_path
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Balthazar Rouberol
|
|
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,56 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: phable-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Manage Phabricator tasks from the comfort of your terminal
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Balthazar Rouberol
|
|
7
|
+
Author-email: br@imap.cc
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
17
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# phable
|
|
21
|
+
Manage Phabricator tasks from the comfort of your terminal.
|
|
22
|
+
|
|
23
|
+
`phable` is a CLI allowing you to manage your [Phorge/Phabricator](https://we.forge.it) tasks.
|
|
24
|
+
|
|
25
|
+
It tries to be very simple and not go overboard with features. You can:
|
|
26
|
+
- create a new task
|
|
27
|
+
- display a task details
|
|
28
|
+
- move a task to a column on its current board
|
|
29
|
+
- assign a task to a user
|
|
30
|
+
- add a comment to a task
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```console
|
|
35
|
+
$ pip install phable-cli
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
$ phable --help
|
|
42
|
+
Usage: phable [OPTIONS] COMMAND [ARGS]...
|
|
43
|
+
|
|
44
|
+
Manage Phabricator tasks from the comfort of your terminal
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--help Show this message and exit.
|
|
48
|
+
|
|
49
|
+
Commands:
|
|
50
|
+
assign Assign one or multiple task ids to a username
|
|
51
|
+
comment Add a comment to a task
|
|
52
|
+
create Create a new task
|
|
53
|
+
move Move one or several task on their current project board
|
|
54
|
+
show Show task details
|
|
55
|
+
```
|
|
56
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
phable_cli/cli.py,sha256=eRCWP-1otDUcj-Dcdhr3LpyER_jtQcoYceq5AzGhPfU,8396
|
|
2
|
+
phable_cli/phabricator.py,sha256=ANs35riDoX0Bo2iRRRoC0gINeV5Nbx_684JXm1ZtgZ8,6176
|
|
3
|
+
phable_cli/utils.py,sha256=lUo8QkY_TWWVdGrxbQGxybB8JNXpgH7Qtd01jraA2jg,918
|
|
4
|
+
phable_cli-0.1.0.dist-info/LICENSE,sha256=7xJ-MHVfPr7mAs4bv81o2DNzoxr6MxZ3QzHyhbeThlk,1075
|
|
5
|
+
phable_cli-0.1.0.dist-info/METADATA,sha256=x9ZaI0q0-QpLXdYFxK_HqLCGzffI2s1FHtvk3bmdAu0,1552
|
|
6
|
+
phable_cli-0.1.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
|
|
7
|
+
phable_cli-0.1.0.dist-info/entry_points.txt,sha256=8PhwfYGWPHBOg1ABJCriZKR9TPKTi4ntnO1BUpcXM-c,45
|
|
8
|
+
phable_cli-0.1.0.dist-info/RECORD,,
|