Skip to content

Commit eb85387

Browse files
committed
re-organise code; start work on command-line client
2 parents 4f9477b + b063c2d commit eb85387

27 files changed

+381
-28
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ for filesystem mounts with FUSE, a UFTP driver for
1414
and a UNICORE implementation of a
1515
[Dask Cluster](https://distributed.dask.org/en/stable/)
1616

17-
This project has received funding from the European Union’s
18-
Horizon 2020 Framework Programme for Research and Innovation under the
19-
Specific Grant Agreement Nos. 720270, 785907 and 945539
17+
This project has received funding from the European Union’s
18+
Horizon 2020 Framework Programme for Research and Innovation under the
19+
Specific Grant Agreement Nos. 720270, 785907 and 945539
2020
(Human Brain Project SGA 1, 2 and 3)
2121

2222
See LICENSE file for licensing information
2323

2424
## Documentation
2525

26-
The complete documentation of PyUNICORE can be viewed
26+
The complete documentation of PyUNICORE can be viewed
2727
[here](https://pyunicore.readthedocs.io/en/latest/)
28-
28+
2929
## Installation
3030

3131
Install from PyPI with
@@ -61,7 +61,7 @@ client = uc_client.Client(credential, base_url)
6161
print(json.dumps(client.properties, indent = 2))
6262
```
6363

64-
PyUNICORE supports a variety of
64+
PyUNICORE supports a variety of
6565
[authentication options](https://pyunicore.readthedocs.io/en/latest/authentication.html).
6666

6767
### Run a job and read result files
@@ -188,7 +188,7 @@ unicore-cwl-runner echo.cwl hello_world.yml > hello_world.u
188188

189189
## Helpers
190190

191-
The `pyunicore.helpers` module provides a set of higher-level APIs:
191+
The `pyunicore.helpers` module provides helper code for:
192192

193193
* Connecting to
194194
* a Registry (`pyunicore.helpers.connect_to_registry`).

pyunicore/cli/__init__.py

Whitespace-only changes.

pyunicore/cli/base.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
""" Base command class """
2+
import argparse
3+
import getpass
4+
import json
5+
import os.path
6+
from base64 import b64decode
7+
8+
import pyunicore.client
9+
import pyunicore.credentials
10+
11+
12+
class Base:
13+
"""Base command class with support for common commandline args"""
14+
15+
def __init__(self, password_source=None):
16+
self.parser = argparse.ArgumentParser(
17+
prog="unicore", description="A commandline client for UNICORE"
18+
)
19+
self.config = {"verbose": False}
20+
self.args = None
21+
self.credential = None
22+
self.registry = None
23+
self.add_base_args()
24+
self.add_command_args()
25+
if password_source:
26+
self.password_source = password_source
27+
else:
28+
self.password_source = getpass.getpass
29+
30+
def _value(self, value: str):
31+
if value.lower() == "true":
32+
return True
33+
if value.lower() == "false":
34+
return False
35+
return value
36+
37+
def load_user_properties(self):
38+
with open(self.config_file) as f:
39+
for line in f.readlines():
40+
line = line.strip()
41+
if line.startswith("#"):
42+
continue
43+
try:
44+
key, value = line.split("=", 1)
45+
self.config[key] = self._value(value)
46+
except ValueError:
47+
pass
48+
49+
def add_base_args(self):
50+
self.parser.add_argument(
51+
"-v", "--verbose", required=False, action="store_true", help="Be verbose"
52+
)
53+
self.parser.add_argument(
54+
"-c",
55+
"--configuration",
56+
metavar="CONFIG",
57+
default=f"{os.getenv('HOME')}/.ucc/properties",
58+
help="Configuration file",
59+
)
60+
61+
def add_command_args(self):
62+
pass
63+
64+
def run(self, args):
65+
self.args = self.parser.parse_args(args)
66+
self.is_verbose = self.args.verbose
67+
self.config_file = self.args.configuration
68+
self.load_user_properties()
69+
self.create_credential()
70+
self.registry = self.create_registry()
71+
72+
def get_synopsis(self):
73+
return "N/A"
74+
75+
def create_credential(self):
76+
auth_method = self.config.get("authentication-method", "USERNAME").upper()
77+
if "USERNAME" == auth_method:
78+
username = self.config["username"]
79+
password = self._get_password()
80+
self.credential = pyunicore.credentials.create_credential(username, password)
81+
82+
def _get_password(self, key="password") -> str:
83+
password = self.config.get(key)
84+
if password is None:
85+
_p = os.getenv("UCC_PASSWORD")
86+
if not _p:
87+
pwd_prompt = "Enter password: "
88+
password = self.password_source(pwd_prompt)
89+
else:
90+
password = _p
91+
return password
92+
93+
def create_registry(self) -> pyunicore.client.Registry:
94+
self.create_credential()
95+
return pyunicore.client.Registry(self.credential, self.config["registry"])
96+
97+
def verbose(self, msg):
98+
if self.is_verbose:
99+
print(msg)
100+
101+
def human_readable(self, value, decimals=0):
102+
for unit in ["B", "KB", "MB", "GB"]:
103+
if value < 1024.0 or unit == "GB":
104+
break
105+
value /= 1024.0
106+
return f"{value:.{decimals}f} {unit}"
107+
108+
109+
class IssueToken(Base):
110+
def add_command_args(self):
111+
self.parser.prog = "unicore issue-token"
112+
self.parser.description = self.get_synopsis()
113+
self.parser.add_argument("URL", help="Token endpoint URL")
114+
self.parser.add_argument("-s", "--sitename", required=False, type=str, help="Site name")
115+
self.parser.add_argument(
116+
"-l",
117+
"--lifetime",
118+
required=False,
119+
type=int,
120+
default=-1,
121+
help="Initial lifetime (in seconds) for token.",
122+
)
123+
self.parser.add_argument(
124+
"-R",
125+
"--renewable",
126+
required=False,
127+
action="store_true",
128+
help="Token can be used to get a fresh token.",
129+
)
130+
self.parser.add_argument(
131+
"-L",
132+
"--limited",
133+
required=False,
134+
action="store_true",
135+
help="Token should be limited to the issuing server.",
136+
)
137+
self.parser.add_argument(
138+
"-I", "--inspect", required=False, action="store_true", help="Inspect the issued token."
139+
)
140+
141+
def get_synopsis(self):
142+
return """Get a JWT token from a UNICORE/X server"""
143+
144+
def run(self, args):
145+
super().run(args)
146+
endpoint = self.args.URL
147+
site_name = self.args.sitename
148+
if site_name:
149+
endpoint = self.registry.site(site_name).resource_url
150+
else:
151+
if endpoint is None:
152+
raise ValueError("Either --sitename or URL must be given.")
153+
endpoint = endpoint.split("/token")[0]
154+
token = self.issue_token(
155+
url=endpoint,
156+
lifetime=self.args.lifetime,
157+
limited=self.args.limited,
158+
renewable=self.args.renewable,
159+
)
160+
if self.args.inspect:
161+
self.show_token_details(token)
162+
print(token)
163+
164+
def issue_token(self, url: str, lifetime: int, limited: bool, renewable: bool) -> str:
165+
client = pyunicore.client.Client(self.credential, site_url=url)
166+
return client.issue_auth_token(lifetime, renewable, limited)
167+
168+
def show_token_details(self, token: str):
169+
_p = token.split(".")[1]
170+
_p += "=" * (-len(_p) % 4) # padding
171+
payload = json.loads(b64decode(_p))
172+
print(f"Subject: {payload['sub']}")
173+
print(f"Lifetime (s): {payload['exp']-payload['iat']}")
174+
print(f"Issued by: {payload['iss']}")
175+
print(f"Valid for: {payload.get('aud', '<unlimited>')}")
176+
print(f"Renewable: {payload.get('renewable', 'no')}")

pyunicore/cli/main.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
""" Main client class """
2+
import platform
3+
import sys
4+
5+
import pyunicore.cli.base
6+
7+
_commands = {
8+
"issue-token": pyunicore.cli.base.IssueToken,
9+
}
10+
11+
12+
def get_command(name):
13+
return _commands.get(name)()
14+
15+
16+
def show_version():
17+
print(
18+
"UNICORE Commandline Client (pyUNICORE) "
19+
"%s, https://www.unicore.eu" % pyunicore._version.get_versions().get("version", "n/a")
20+
)
21+
print("Python %s" % sys.version)
22+
print("OS: %s" % platform.platform())
23+
24+
25+
def help():
26+
s = """UNICORE Commandline Client (pyUNICORE) %s, https://www.unicore.eu
27+
Usage: unicore <command> [OPTIONS] <args>
28+
The following commands are available:""" % pyunicore._version.get_versions().get(
29+
"version", "n/a"
30+
)
31+
print(s)
32+
for cmd in sorted(_commands):
33+
print(f" {cmd:20} - {get_command(cmd).get_synopsis()}")
34+
print("Enter 'unicore <command> -h' for help on a particular command.")
35+
36+
37+
def run(args):
38+
_help = ["help", "-h", "--help"]
39+
if len(args) < 1 or args[0] in _help:
40+
help()
41+
return
42+
_version = ["version", "-V", "--version"]
43+
if args[0] in _version:
44+
show_version()
45+
return
46+
47+
command = None
48+
cmd = args[0]
49+
for k in _commands:
50+
if k.startswith(cmd):
51+
command = get_command(k)
52+
break
53+
if command is None:
54+
raise ValueError(f"No such command: {cmd}")
55+
command.run(args[1:])
56+
57+
58+
def main():
59+
"""
60+
Main entry point
61+
"""
62+
run(sys.argv[1:])
63+
64+
65+
if __name__ == "__main__":
66+
main()

pyunicore/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ def execute(self, cmd, login_node=None):
426426

427427
return Job(self.transport, job_url)
428428

429-
def issue_auth_token(self, lifetime=-1, renewable=False, limited=False):
429+
def issue_auth_token(self, lifetime=-1, renewable=False, limited=False) -> str:
430430
"""
431431
Issue an authentication token (JWT) from this UNICORE server
432432
Args:

pyunicore/cwl/__init__.py

Whitespace-only changes.
File renamed without changes.

pyunicore/cwltool.py renamed to pyunicore/cwl/cwltool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import yaml
66

7-
import pyunicore.cwlconverter
7+
import pyunicore.cwl.cwlconverter as cwlconverter
88

99

1010
def read_cwl_files(cwl_doc_path, cwl_inputs_object_path=None, debug=False):
@@ -51,7 +51,7 @@ def main():
5151
unicore_job,
5252
file_list,
5353
outputs_list,
54-
) = pyunicore.cwlconverter.convert_cmdline_tool(cwl_doc, cwl_inputs_object, debug=debug)
54+
) = cwlconverter.convert_cmdline_tool(cwl_doc, cwl_inputs_object, debug=debug)
5555
print(json.dumps(unicore_job, indent=2, sort_keys=True))
5656

5757
sys.exit(0)

pyunicore/testing/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

pyunicore/uftp/__init__.py

Whitespace-only changes.
File renamed without changes.

pyunicore/uftpfs.py renamed to pyunicore/uftp/uftpfs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from fs.opener import Opener
55

66
import pyunicore.credentials as uc_credentials
7-
from pyunicore.uftp import UFTP
7+
from pyunicore.uftp.uftp import UFTP
88

99

1010
class UFTPFS(FTPFS):

pyunicore/uftpfuse.py renamed to pyunicore/uftp/uftpfuse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from fuse import FuseOSError
1515
from fuse import Operations
1616

17-
from pyunicore.uftp import UFTP
17+
from pyunicore.uftp.uftp import UFTP
1818

1919

2020
class UFTPFile:

setup.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
Visit https://github.com/HumanBrainProject/pyunicore for more information.
1111
"""
1212

13-
python_requires = ">=3"
13+
python_requires = ">=3.7"
1414

1515
install_requires = [
16-
"PyJWT>=2.0",
16+
"pyjwt>=2.8",
1717
"requests>=2.5",
1818
]
1919

@@ -37,12 +37,13 @@
3737
extras_require=extras_require,
3838
entry_points={
3939
"fs.opener": [
40-
"uftp = pyunicore.uftpfs:UFTPOpener",
40+
"uftp = pyunicore.uftp.uftpfs:UFTPOpener",
4141
],
4242
"console_scripts": [
4343
"unicore-port-forwarder=pyunicore.forwarder:main",
44-
"unicore-cwl-runner=pyunicore.cwltool:main",
45-
"unicore-fusedriver=pyunicore.uftpfuse:main",
44+
"unicore-cwl-runner=pyunicore.cwl.cwltool:main",
45+
"unicore-fusedriver=pyunicore.uftp.uftpfuse:main",
46+
"unicore=pyunicore.cli.main:main",
4647
],
4748
},
4849
license="License :: OSI Approved :: BSD",

tests/integration/cli/preferences

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# User preferences for UCC
2+
3+
authentication-method=USERNAME
4+
username=demouser
5+
password=test123
6+
7+
verbose=true
8+
9+
#
10+
# The address(es) of the registries to contact
11+
# (space separated list)
12+
registry=https://localhost:8080/DEMO-SITE/rest/registries/default_registry
13+
contact-registry=true
14+
15+
#
16+
# default directory for output
17+
#
18+
output=/tmp

0 commit comments

Comments
 (0)