phable-cli 0.1.6__tar.gz → 0.1.7__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.
- {phable_cli-0.1.6 → phable_cli-0.1.7}/PKG-INFO +1 -1
- {phable_cli-0.1.6 → phable_cli-0.1.7}/phable_cli/cache.py +1 -1
- {phable_cli-0.1.6 → phable_cli-0.1.7}/phable_cli/cli.py +61 -2
- phable_cli-0.1.7/phable_cli/config.py +67 -0
- {phable_cli-0.1.6 → phable_cli-0.1.7}/pyproject.toml +1 -1
- phable_cli-0.1.6/phable_cli/config.py +0 -34
- {phable_cli-0.1.6 → phable_cli-0.1.7}/LICENSE +0 -0
- {phable_cli-0.1.6 → phable_cli-0.1.7}/README.md +0 -0
- {phable_cli-0.1.6 → phable_cli-0.1.7}/phable_cli/phabricator.py +0 -0
- {phable_cli-0.1.6 → phable_cli-0.1.7}/phable_cli/utils.py +0 -0
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
CACHE_HOME_PER_PLATFORM = {
|
|
11
11
|
"darwin": Path.home() / "Library" / "Caches",
|
|
12
|
-
"linux": Path(os.getenv("XDG_CACHE_HOME", f"{Path.home()}/.
|
|
12
|
+
"linux": Path(os.getenv("XDG_CACHE_HOME", f"{Path.home()}/.cache")),
|
|
13
13
|
"windows": Path("c:/", "Users", os.getlogin(), "AppData", "Local", "Temp"),
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -18,7 +18,55 @@ VARIADIC = -1 # Used for click variadic arguments
|
|
|
18
18
|
atexit.register(cache.dump)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
class AliasedCommandGroup(click.Group):
|
|
22
|
+
"""Custom CLI group allowing the replaement of aliases commands on the fly
|
|
23
|
+
|
|
24
|
+
For example if we have the following configuraion:
|
|
25
|
+
[aliases]
|
|
26
|
+
done = move --column 'Done' --milestone
|
|
27
|
+
|
|
28
|
+
then calling `phable done T123456` will actually call
|
|
29
|
+
`phable move --column Done --milestone T123456` under the hood.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *args, **kwargs):
|
|
34
|
+
super().__init__(*args, **kwargs)
|
|
35
|
+
self._aliases = config.data.get("aliases", {})
|
|
36
|
+
|
|
37
|
+
def make_context(self, info_name, args, parent=None, **extra):
|
|
38
|
+
# First, let's parse the command and handle aliases
|
|
39
|
+
parsed_args = self.parse_command(info_name, args)
|
|
40
|
+
|
|
41
|
+
# Then create the context with the possibly modified args
|
|
42
|
+
ctx = super().make_context(
|
|
43
|
+
info_name=parsed_args[0], args=parsed_args[1:], parent=parent, **extra
|
|
44
|
+
)
|
|
45
|
+
return ctx
|
|
46
|
+
|
|
47
|
+
def parse_command(self, ctx_name, args):
|
|
48
|
+
"""Parse command line arguments and handle aliases"""
|
|
49
|
+
if not args:
|
|
50
|
+
return [ctx_name]
|
|
51
|
+
|
|
52
|
+
# Check if the first argument is an alias
|
|
53
|
+
if args[0] in self._aliases:
|
|
54
|
+
pattern = self._aliases[args[0]]
|
|
55
|
+
# Split the pattern and combine with remaining args
|
|
56
|
+
pattern_parts = click.parser.split_arg_string(pattern)
|
|
57
|
+
# Replace the alias with the pattern parts
|
|
58
|
+
args = pattern_parts + args[1:]
|
|
59
|
+
|
|
60
|
+
return [ctx_name] + args
|
|
61
|
+
|
|
62
|
+
def get_command(self, ctx, cmd_name):
|
|
63
|
+
"""Override to handle aliases in command lookup"""
|
|
64
|
+
if cmd_name in self._aliases:
|
|
65
|
+
return super().get_command(ctx, self._aliases[cmd_name].split()[0])
|
|
66
|
+
return super().get_command(ctx, cmd_name)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@click.group(cls=AliasedCommandGroup)
|
|
22
70
|
@click.version_option()
|
|
23
71
|
def cli():
|
|
24
72
|
"""Manage Phabricator tasks from the comfort of your terminal"""
|
|
@@ -30,6 +78,11 @@ def _cache():
|
|
|
30
78
|
"""Manage internal cache"""
|
|
31
79
|
|
|
32
80
|
|
|
81
|
+
@cli.group
|
|
82
|
+
def aliases():
|
|
83
|
+
"""Manage aliases"""
|
|
84
|
+
|
|
85
|
+
|
|
33
86
|
class Task(int):
|
|
34
87
|
@classmethod
|
|
35
88
|
def from_str(cls, value: str) -> int:
|
|
@@ -83,7 +136,7 @@ def echo_task(echo: Callable[[str], None], format: str, task: dict[str, Any]) ->
|
|
|
83
136
|
echo(json.dumps(task))
|
|
84
137
|
else:
|
|
85
138
|
parent_str = (
|
|
86
|
-
f"{Task.from_int(task[
|
|
139
|
+
f"{Task.from_int(task['parent']['id'])} - {task['parent']['fields']['name']}"
|
|
87
140
|
if task.get("parent")
|
|
88
141
|
else ""
|
|
89
142
|
)
|
|
@@ -429,6 +482,12 @@ def report_done_tasks(milestone: bool, format: str, source: str, destination: st
|
|
|
429
482
|
client.move_task_to_column(task["id"], column_destination_phid)
|
|
430
483
|
|
|
431
484
|
|
|
485
|
+
@aliases.command()
|
|
486
|
+
def list():
|
|
487
|
+
for name, alias in config.data.get("aliases", {}).items():
|
|
488
|
+
click.echo(f"{name} = {alias}")
|
|
489
|
+
|
|
490
|
+
|
|
432
491
|
def runcli():
|
|
433
492
|
cli(max_content_width=120)
|
|
434
493
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from configparser import ConfigParser
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from functools import partial
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_warnings = []
|
|
12
|
+
|
|
13
|
+
CONFIG_HOME_PER_PLATFORM = {
|
|
14
|
+
"darwin": Path.home() / "Library" / "Preferences",
|
|
15
|
+
"linux": Path(os.getenv("XDG_CACHE_HOME", f"{Path.home()}/.config")),
|
|
16
|
+
"windows": Path("c:/", "Users", os.getlogin(), "AppData", "Local", "Programs"),
|
|
17
|
+
}
|
|
18
|
+
config_filepath = CONFIG_HOME_PER_PLATFORM[sys.platform] / "phable" / "config.ini"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def os_getenv_or_raise(env_var_name: str):
|
|
22
|
+
if val := os.getenv(env_var_name):
|
|
23
|
+
return val
|
|
24
|
+
_warnings.append(f"Required environment variable {env_var_name} is not set")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def field_with_default_from_env(env_var_name):
|
|
28
|
+
return field(default_factory=partial(os_getenv_or_raise, env_var_name))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_config() -> dict:
|
|
32
|
+
if not config_filepath.parent.exists():
|
|
33
|
+
config_filepath.parent.mkdir()
|
|
34
|
+
if not config_filepath.exists():
|
|
35
|
+
return {}
|
|
36
|
+
else:
|
|
37
|
+
config = ConfigParser()
|
|
38
|
+
try:
|
|
39
|
+
config.read(config_filepath)
|
|
40
|
+
except Exception:
|
|
41
|
+
_warnings.append(
|
|
42
|
+
f"Configuration file {config_filepath} is invalid. Skipping parsing."
|
|
43
|
+
)
|
|
44
|
+
return {}
|
|
45
|
+
else:
|
|
46
|
+
return {
|
|
47
|
+
section: dict(config.items(section)) for section in config.sections()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass()
|
|
52
|
+
class Config:
|
|
53
|
+
phabricator_url: str = field_with_default_from_env("PHABRICATOR_URL")
|
|
54
|
+
phabricator_token: str = field_with_default_from_env("PHABRICATOR_TOKEN")
|
|
55
|
+
phabricator_default_project_phid: str = field_with_default_from_env(
|
|
56
|
+
"PHABRICATOR_DEFAULT_PROJECT_PHID"
|
|
57
|
+
)
|
|
58
|
+
filepath: Path = field(default=config_filepath)
|
|
59
|
+
data: dict = field(default_factory=read_config)
|
|
60
|
+
|
|
61
|
+
def __post_init__(self):
|
|
62
|
+
for warn in _warnings:
|
|
63
|
+
click.echo(click.style(warn, fg="yellow"), err=True)
|
|
64
|
+
return len(_warnings) == 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
config = Config()
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from dataclasses import dataclass, field
|
|
3
|
-
from functools import partial
|
|
4
|
-
|
|
5
|
-
import click
|
|
6
|
-
|
|
7
|
-
_warnings = []
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def os_getenv_or_raise(env_var_name: str):
|
|
11
|
-
if val := os.getenv(env_var_name):
|
|
12
|
-
return val
|
|
13
|
-
_warnings.append(f"Required environment variable {env_var_name} is not set")
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def field_with_default_from_env(env_var_name):
|
|
17
|
-
return field(default_factory=partial(os_getenv_or_raise, env_var_name))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass(frozen=True)
|
|
21
|
-
class Config:
|
|
22
|
-
phabricator_url: str = field_with_default_from_env("PHABRICATOR_URL")
|
|
23
|
-
phabricator_token: str = field_with_default_from_env("PHABRICATOR_TOKEN")
|
|
24
|
-
phabricator_default_project_phid: str = field_with_default_from_env(
|
|
25
|
-
"PHABRICATOR_DEFAULT_PROJECT_PHID"
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
def __post_init__(self):
|
|
29
|
-
for warn in _warnings:
|
|
30
|
-
click.echo(click.style(warn, fg="yellow"), err=True)
|
|
31
|
-
return len(_warnings) == 0
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
config = Config()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|