Skip to content

Commit 300eb9a

Browse files
bbtfrliyangLoveEatCandy
authored
feat: aliases for SmartPath (#406)
* feat: aliases for SmartPath * make lint happy * update cli & readme * make lint happy 2 * fix review comments * fix review comments 2 --------- Co-authored-by: liyang <[email protected]> Co-authored-by: penghongyang <[email protected]>
1 parent e92ff9f commit 300eb9a

File tree

5 files changed

+149
-33
lines changed

5 files changed

+149
-33
lines changed

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,10 @@ You can update config file with `megfile` command easyly:
142142
```
143143
$ megfile config s3 accesskey secretkey
144144
145-
# for aliyun
145+
# for aliyun oss
146146
$ megfile config s3 accesskey secretkey \
147147
--addressing-style virtual \
148-
--endpoint-url http://oss-cn-hangzhou.aliyuncs.com \
148+
--endpoint-url http://oss-cn-hangzhou.aliyuncs.com
149149
```
150150

151151
You can get the configuration from `~/.aws/credentials`, like:
@@ -159,6 +159,25 @@ s3 =
159159
endpoint_url = http://oss-cn-hangzhou.aliyuncs.com
160160
```
161161

162+
### Create aliases
163+
```
164+
# for volcengine tos
165+
$ megfile config s3 accesskey secretkey \
166+
--addressing-style virtual \
167+
--endpoint-url https://tos-s3-cn-beijing.ivolces.com \
168+
--profile tos
169+
170+
# create alias
171+
$ megfile config tos s3+tos
172+
```
173+
174+
You can get the configuration from `~/.config/megfile/aliases.conf`, like:
175+
```
176+
[tos]
177+
protocol = s3+tos
178+
```
179+
180+
162181
## How to Contribute
163182
* We welcome everyone to contribute code to the `megfile` project, but the contributed code needs to meet the following conditions as much as possible:
164183

megfile/cli.py

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,12 @@ def config():
570570
pass
571571

572572

573-
@config.command(short_help="Return the config file for s3")
573+
def _safe_makedirs(path: str):
574+
if path not in ("", ".", "/"):
575+
os.makedirs(path, exist_ok=True)
576+
577+
578+
@config.command(short_help="Update the config file for s3")
574579
@click.option(
575580
"-p",
576581
"--path",
@@ -584,7 +589,8 @@ def config():
584589
@click.argument("aws_access_key_id")
585590
@click.argument("aws_secret_access_key")
586591
@click.option("-e", "--endpoint-url", help="endpoint-url")
587-
@click.option("-s", "--addressing-style", help="addressing-style")
592+
@click.option("-as", "--addressing-style", help="addressing-style")
593+
@click.option("-sv", "--signature-version", help="signature-version")
588594
@click.option("--no-cover", is_flag=True, help="Not cover the same-name config")
589595
def s3(
590596
path,
@@ -593,6 +599,7 @@ def s3(
593599
aws_secret_access_key,
594600
endpoint_url,
595601
addressing_style,
602+
signature_version,
596603
no_cover,
597604
):
598605
path = os.path.expanduser(path)
@@ -602,30 +609,27 @@ def s3(
602609
"aws_access_key_id": aws_access_key_id,
603610
"aws_secret_access_key": aws_secret_access_key,
604611
}
605-
s3 = {}
606-
if endpoint_url:
607-
s3.update({"endpoint_url": endpoint_url})
608-
if addressing_style:
609-
s3.update({"addressing_style": addressing_style})
610-
if s3:
611-
config_dict.update({"s3": s3})
612+
s3_config_dict = {
613+
"endpoint_url": endpoint_url,
614+
"addressing_style": addressing_style,
615+
"signature_version": signature_version,
616+
}
617+
618+
s3_config_dict = {k: v for k, v in s3_config_dict.items() if v}
619+
if s3_config_dict:
620+
config_dict["s3"] = s3_config_dict
612621

613622
def dumps(config_dict: dict) -> str:
614623
content = "[{}]\n".format(config_dict["name"])
615-
content += "aws_access_key_id = {}\n".format(config_dict["aws_access_key_id"])
616-
content += "aws_secret_access_key = {}\n".format(
617-
config_dict["aws_secret_access_key"]
618-
)
624+
for key in ("aws_access_key_id", "aws_secret_access_key"):
625+
content += "{} = {}\n".format(key, config_dict[key])
619626
if "s3" in config_dict.keys():
620627
content += "\ns3 = \n"
621-
s3: dict = config_dict["s3"]
622-
if "endpoint_url" in s3.keys():
623-
content += " endpoint_url = {}\n".format(s3["endpoint_url"])
624-
if "addressing_style" in s3.keys():
625-
content += " addressing_style = {}\n".format(s3["addressing_style"])
628+
for key, value in config_dict["s3"].items():
629+
content += " {} = {}\n".format(key, value)
626630
return content
627631

628-
os.makedirs(os.path.dirname(path), exist_ok=True) # make sure dirpath exist
632+
_safe_makedirs(os.path.dirname(path)) # make sure dirpath exist
629633
if not os.path.exists(path): # If this file doesn't exist.
630634
content_str = dumps(config_dict)
631635
with open(path, "w") as fp:
@@ -663,15 +667,15 @@ def dumps(config_dict: dict) -> str:
663667
click.echo(f"Your oss config has been saved into {path}")
664668

665669

666-
@config.command(short_help="Return the config file for s3")
667-
@click.argument("url")
670+
@config.command(short_help="Update the config file for hdfs")
668671
@click.option(
669672
"-p",
670673
"--path",
671674
default="~/.hdfscli.cfg",
672-
help="s3 config file, default is $HOME/.hdfscli.cfg",
675+
help="hdfs config file, default is $HOME/.hdfscli.cfg",
673676
)
674-
@click.option("-n", "--profile-name", default="default", help="s3 config file")
677+
@click.argument("url")
678+
@click.option("-n", "--profile-name", default="default", help="hdfs config file")
675679
@click.option("-u", "--user", help="user name")
676680
@click.option("-r", "--root", help="hdfs path's root dir")
677681
@click.option("-t", "--token", help="token for requesting hdfs server")
@@ -681,7 +685,7 @@ def dumps(config_dict: dict) -> str:
681685
help=f"request hdfs server timeout, default {DEFAULT_HDFS_TIMEOUT}",
682686
)
683687
@click.option("--no-cover", is_flag=True, help="Not cover the same-name config")
684-
def hdfs(url, path, profile_name, user, root, token, timeout, no_cover):
688+
def hdfs(path, url, profile_name, user, root, token, timeout, no_cover):
685689
path = os.path.expanduser(path)
686690
current_config = {
687691
"url": url,
@@ -704,11 +708,40 @@ def hdfs(url, path, profile_name, user, root, token, timeout, no_cover):
704708
for key, value in current_config.items():
705709
if value:
706710
config[profile_name][key] = value
711+
712+
_safe_makedirs(os.path.dirname(path)) # make sure dirpath exist
707713
with open(path, "w") as fp:
708714
config.write(fp)
709715
click.echo(f"Your hdfs config has been saved into {path}")
710716

711717

718+
@config.command(short_help="Update the config file for aliases")
719+
@click.option(
720+
"-p",
721+
"--path",
722+
default="~/.config/megfile/aliases.conf",
723+
help="alias config file, default is $HOME/.config/megfile/aliases.conf",
724+
)
725+
@click.argument("name")
726+
@click.argument("protocol")
727+
@click.option("--no-cover", is_flag=True, help="Not cover the same-name config")
728+
def alias(path, name, protocol, no_cover):
729+
path = os.path.expanduser(path)
730+
config = configparser.ConfigParser()
731+
if os.path.exists(path):
732+
config.read(path)
733+
if name in config.sections() and no_cover:
734+
raise NameError(f"alias-name has been used: {name}")
735+
config[name] = {
736+
"protocol": protocol,
737+
}
738+
739+
_safe_makedirs(os.path.dirname(path)) # make sure dirpath exist
740+
with open(path, "w") as fp:
741+
config.write(fp)
742+
click.echo(f"Your alias config has been saved into {path}")
743+
744+
712745
if __name__ == "__main__":
713746
# Usage: python -m megfile.cli
714747
safe_cli() # pragma: no cover

megfile/smart_path.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import os
2+
from configparser import ConfigParser
13
from pathlib import PurePath
2-
from typing import Tuple, Union
4+
from typing import Dict, Tuple, Union
35

46
from megfile.lib.compat import fspath
57
from megfile.lib.url import get_url_scheme
8+
from megfile.utils import classproperty
69

710
from .errors import ProtocolExistsError, ProtocolNotFoundError
811
from .interfaces import BasePath, BaseURIPath, PathLike
912

13+
aliases_config = "~/.config/megfile/aliases.conf"
14+
1015

1116
def _bind_function(name):
1217
def smart_method(self, *args, **kwargs):
@@ -25,6 +30,17 @@ def smart_property(self):
2530
return smart_property
2631

2732

33+
def _load_aliases_config(config_path) -> Dict[str, Dict[str, str]]:
34+
if not os.path.exists(config_path):
35+
return {}
36+
parser = ConfigParser()
37+
parser.read(config_path)
38+
configs = {}
39+
for section in parser.sections():
40+
configs[section] = dict(parser.items(section))
41+
return configs
42+
43+
2844
class SmartPath(BasePath):
2945
_registered_protocols = dict()
3046

@@ -38,6 +54,13 @@ def __init__(self, path: Union[PathLike, int], *other_paths: PathLike):
3854
self.path = str(pathlike)
3955
self.pathlike = pathlike
4056

57+
@classproperty
58+
def _aliases(cls) -> Dict[str, Dict[str, str]]:
59+
config_path = os.path.expanduser(aliases_config)
60+
aliases = _load_aliases_config(config_path)
61+
setattr(cls, "_aliases", aliases)
62+
return aliases
63+
4164
@staticmethod
4265
def _extract_protocol(path: Union[PathLike, int]) -> Tuple[str, Union[str, int]]:
4366
if isinstance(path, int):
@@ -61,7 +84,11 @@ def _extract_protocol(path: Union[PathLike, int]) -> Tuple[str, Union[str, int]]
6184

6285
@classmethod
6386
def _create_pathlike(cls, path: Union[PathLike, int]) -> BaseURIPath:
64-
protocol, _ = cls._extract_protocol(path)
87+
protocol, path_without_protocol = cls._extract_protocol(path)
88+
aliases: Dict[str, Dict[str, str]] = cls._aliases # pyre-ignore[9]
89+
if protocol in aliases:
90+
protocol = aliases[protocol]["protocol"]
91+
path = protocol + "://" + str(path_without_protocol)
6592
if protocol.startswith("s3+"):
6693
protocol = "s3"
6794
if protocol not in cls._registered_protocols:

tests/test_cli.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from click.testing import CliRunner
66

77
from megfile.cli import (
8+
alias,
89
cat,
910
cp,
1011
hdfs,
@@ -355,8 +356,10 @@ def test_config_s3(tmpdir, runner):
355356
str(tmpdir / "oss_config"),
356357
"-e",
357358
"Endpoint",
358-
"-s",
359-
"Addressing",
359+
"-as",
360+
"virtual",
361+
"-sv",
362+
"s3v4",
360363
"Aws_access_key_id",
361364
"Aws_secret_access_key",
362365
],
@@ -372,7 +375,7 @@ def test_config_s3(tmpdir, runner):
372375
"new_test",
373376
"-e",
374377
"end-point",
375-
"-s",
378+
"-as",
376379
"add",
377380
"1345",
378381
"2345",
@@ -472,3 +475,20 @@ def test_config_hdfs(tmpdir, runner):
472475
)
473476
config.read(str(tmpdir / "config"))
474477
assert result.exit_code == 1
478+
479+
480+
def test_config_alias(tmpdir, runner):
481+
result = runner.invoke(
482+
alias,
483+
[
484+
"-p",
485+
str(tmpdir / "config"),
486+
"a",
487+
"b",
488+
],
489+
)
490+
assert "Your alias config" in result.output
491+
492+
config = configparser.ConfigParser()
493+
config.read(str(tmpdir / "config"))
494+
assert config["a"]["protocol"] == "b"

tests/test_smart_path.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import boto3
55
import pytest
6-
from mock import patch
6+
from mock import PropertyMock, patch
77
from moto import mock_aws
88

99
from megfile.errors import (
@@ -16,7 +16,7 @@
1616
from megfile.interfaces import Access
1717
from megfile.s3_path import S3Path
1818
from megfile.sftp_path import SftpPath
19-
from megfile.smart_path import PurePath, SmartPath
19+
from megfile.smart_path import PurePath, SmartPath, _load_aliases_config, aliases_config
2020
from megfile.stdio_path import StdioPath
2121

2222
FS_PROTOCOL_PREFIX = FSPath.protocol + "://"
@@ -76,6 +76,23 @@ def test_register_result():
7676
assert SmartPath.from_uri(FS_TEST_ABSOLUTE_PATH) == SmartPath(FS_TEST_ABSOLUTE_PATH)
7777

7878

79+
def test_aliases(fs):
80+
config_path = os.path.expanduser(aliases_config)
81+
fs.create_file(
82+
config_path,
83+
contents="[oss2]\nprotocol = s3+oss2\n[tos]\nprotocol = s3+tos",
84+
)
85+
aliases = {"oss2": {"protocol": "s3+oss2"}, "tos": {"protocol": "s3+tos"}}
86+
assert _load_aliases_config(config_path) == aliases
87+
88+
with patch.object(SmartPath, "_aliases", new_callable=PropertyMock) as mock_aliases:
89+
mock_aliases.return_value = aliases
90+
assert (
91+
SmartPath.from_uri("oss2://bucket/dir/file").pathlike
92+
== SmartPath("s3+oss2://bucket/dir/file").pathlike
93+
)
94+
95+
7996
@patch.object(SmartPath, "_create_pathlike")
8097
def test_init(funcA):
8198
SmartPath(FS_TEST_ABSOLUTE_PATH)

0 commit comments

Comments
 (0)