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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: phable-cli
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Manage Phabricator tasks from the comfort of your terminal
5
5
  License: MIT
6
6
  Author: Balthazar Rouberol
@@ -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()}/.config")),
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
- @click.group()
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["parent"]['id'])} - {task["parent"]['fields']['name']}"
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "phable-cli"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  description = "Manage Phabricator tasks from the comfort of your terminal"
5
5
  authors = ["Balthazar Rouberol <br@imap.cc>"]
6
6
  license = "MIT"
@@ -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