NGPIris 5.0.1__tar.gz → 5.1.1.dev2__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.
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/cli/__init__.py +103 -30
- ngpiris-5.1.1.dev2/NGPIris/hcp/exceptions.py +23 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hcp/hcp.py +131 -55
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hcp/helpers.py +2 -0
- ngpiris-5.1.1.dev2/NGPIris/hcp/statistics.py +20 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2/NGPIris.egg-info}/PKG-INFO +18 -4
- ngpiris-5.1.1.dev2/NGPIris.egg-info/SOURCES.txt +127 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/requires.txt +3 -2
- ngpiris-5.1.1.dev2/PKG-INFO +302 -0
- ngpiris-5.0.1/NGPIris.egg-info/PKG-INFO → ngpiris-5.1.1.dev2/README.md +14 -15
- ngpiris-5.1.1.dev2/docs/_build/.DS_Store +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/IRIS_5_changelog.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.cli.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.hci.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.hcp.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.parse_credentials.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.utils.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/README.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/Tutorial.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/environment.pickle +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/index.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/doctrees/modules.doctree +0 -0
- ngpiris-5.1.1.dev2/docs/_build/html/.buildinfo +4 -0
- ngpiris-5.1.1.dev2/docs/_build/html/IRIS_5_changelog.html +462 -0
- ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.cli.html +137 -0
- ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.hci.html +280 -0
- ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.hcp.html +357 -0
- ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.html +236 -0
- ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.parse_credentials.html +156 -0
- ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.utils.html +143 -0
- ngpiris-5.1.1.dev2/docs/_build/html/README.html +681 -0
- ngpiris-5.1.1.dev2/docs/_build/html/Tutorial.html +796 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/cli.html +338 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hci/exceptions.html +106 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hci/hci.html +250 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hci/helpers.html +204 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/exceptions.html +117 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/hcp.html +655 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/helpers.html +148 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/statistics.html +137 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/parse_credentials/parse_credentials.html +170 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/utils/utils.html +115 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_modules/index.html +108 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/IRIS_5_changelog.md.txt +43 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.cli.rst.txt +10 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.hci.rst.txt +37 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.hcp.rst.txt +45 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.parse_credentials.rst.txt +21 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.rst.txt +22 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.utils.rst.txt +21 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/Tutorial.md.txt +326 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_sources/index.rst.txt +21 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/alabaster.css +708 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/basic.css +925 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/custom.css +1 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/debug.css +69 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/doctools.js +156 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/documentation_options.js +13 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/file.png +0 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/language_data.js +199 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/minus.png +0 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/plus.png +0 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/pygments.css +83 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo-extensions.js +0 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo.js +3 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo.js.LICENSE.txt +7 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo.js.map +1 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/searchtools.js +619 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/skeleton.css +296 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/sphinx_highlight.js +154 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo-extensions.css +2 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo-extensions.css.map +1 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo.css +2 -0
- ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo.css.map +1 -0
- ngpiris-5.1.1.dev2/docs/_build/html/genindex.html +476 -0
- ngpiris-5.1.1.dev2/docs/_build/html/index.html +159 -0
- ngpiris-5.1.1.dev2/docs/_build/html/modules.html +148 -0
- ngpiris-5.1.1.dev2/docs/_build/html/objects.inv +0 -0
- ngpiris-5.1.1.dev2/docs/_build/html/py-modindex.html +188 -0
- ngpiris-5.1.1.dev2/docs/_build/html/search.html +120 -0
- ngpiris-5.1.1.dev2/docs/_build/html/searchindex.js +1 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/conf.py +18 -27
- ngpiris-5.1.1.dev2/docs/index.rst +21 -0
- ngpiris-5.1.1.dev2/docs/modules.rst +7 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/pyproject.toml +8 -6
- ngpiris-5.1.1.dev2/tests/data/incomplete_creds_0.json +14 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/test_hcp.py +9 -5
- ngpiris-5.0.1/NGPIris/hcp/exceptions.py +0 -8
- ngpiris-5.0.1/NGPIris/hcp/statistics.py +0 -25
- ngpiris-5.0.1/NGPIris.egg-info/SOURCES.txt +0 -52
- ngpiris-5.0.1/docs/index.rst +0 -21
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/LICENSE +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/MANIFEST.in +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/__init__.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/__init__.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/exceptions.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/hci.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/helpers.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hcp/__init__.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/parse_credentials/__init__.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/parse_credentials/parse_credentials.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/utils/__init__.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/utils/utils.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/dependency_links.txt +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/entry_points.txt +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/top_level.txt +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/IRIS_5_changelog.md +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/Makefile +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.cli.rst +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.hci.rst +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.hcp.rst +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.parse_credentials.rst +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.rst +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.utils.rst +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/Tutorial.md +0 -0
- /ngpiris-5.0.1/tests/data/incomplete_creds_0.json → /ngpiris-5.1.1.dev2/docs/_build/html/_downloads/ee00c4f6f22dc2671c766f728f00006f/credentials_template.json +0 -0
- /ngpiris-5.0.1/README.md → /ngpiris-5.1.1.dev2/docs/_build/html/_sources/README.md.txt +0 -0
- /ngpiris-5.0.1/docs/modules.rst → /ngpiris-5.1.1.dev2/docs/_build/html/_sources/modules.rst.txt +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/make.bat +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/setup.cfg +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/80MB_test_file +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/a folder of data/80MB_test_file_0 +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/a folder of data/80MB_test_file_1 +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/complete_creds_0.json +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/complete_creds_1.json +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/complete_creds_2.json +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_1.json +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_2.json +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_3.json +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_4.json +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/test_hci.py +0 -0
- {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/test_parse_credentials.py +0 -0
|
@@ -1,18 +1,60 @@
|
|
|
1
1
|
|
|
2
2
|
import click
|
|
3
3
|
from click.core import Context
|
|
4
|
-
from json import
|
|
4
|
+
from json import dump
|
|
5
5
|
from pathlib import Path
|
|
6
|
+
from botocore.paginate import PageIterator, Paginator
|
|
7
|
+
from typing import Any, Generator
|
|
8
|
+
from os import get_terminal_size
|
|
9
|
+
from math import floor
|
|
10
|
+
from tabulate import tabulate
|
|
11
|
+
from bitmath import Byte, TiB
|
|
6
12
|
|
|
7
13
|
from NGPIris.hcp import HCPHandler
|
|
8
14
|
|
|
9
|
-
def get_HCPHandler(context : Context)-> HCPHandler:
|
|
15
|
+
def get_HCPHandler(context : Context) -> HCPHandler:
|
|
10
16
|
return context.obj["hcph"]
|
|
11
17
|
|
|
12
18
|
def format_list(list_of_things : list) -> str:
|
|
13
19
|
list_of_buckets = list(map(lambda s : s + "\n", list_of_things))
|
|
14
20
|
return "".join(list_of_buckets).strip("\n")
|
|
15
21
|
|
|
22
|
+
def _list_objects_generator(hcph : HCPHandler, name_only : bool) -> Generator[str, Any, None]:
|
|
23
|
+
"""
|
|
24
|
+
Handle object list as a paginator that `click` can handle. It works slightly
|
|
25
|
+
different from `list_objects` in `hcp.py` in order to make the output
|
|
26
|
+
printable in a terminal
|
|
27
|
+
"""
|
|
28
|
+
paginator : Paginator = hcph.s3_client.get_paginator("list_objects_v2")
|
|
29
|
+
pages : PageIterator = paginator.paginate(Bucket = hcph.bucket_name)
|
|
30
|
+
(nb_of_cols, _) = get_terminal_size()
|
|
31
|
+
max_width = floor(nb_of_cols / 5)
|
|
32
|
+
if (not name_only):
|
|
33
|
+
yield tabulate(
|
|
34
|
+
[],
|
|
35
|
+
headers = ["Key", "LastModified", "ETag", "Size", "StorageClass"],
|
|
36
|
+
tablefmt = "plain",
|
|
37
|
+
stralign = "center"
|
|
38
|
+
) + "\n" + "-"*nb_of_cols + "\n"
|
|
39
|
+
for object in pages.search("Contents[?!ends_with(Key, '/')][]"): # filter objects that does not end with "/"
|
|
40
|
+
if name_only:
|
|
41
|
+
yield str(object["Key"]) + "\n"
|
|
42
|
+
else:
|
|
43
|
+
yield tabulate(
|
|
44
|
+
[
|
|
45
|
+
[str(object["Key"]),
|
|
46
|
+
str(object["LastModified"]),
|
|
47
|
+
str(object["ETag"]),
|
|
48
|
+
str(object["Size"]),
|
|
49
|
+
str(object["StorageClass"])]
|
|
50
|
+
],
|
|
51
|
+
maxcolwidths = max_width,
|
|
52
|
+
tablefmt = "plain"
|
|
53
|
+
) + "\n" + "-"*nb_of_cols + "\n"
|
|
54
|
+
|
|
55
|
+
def object_is_folder(object_path : str, hcph : HCPHandler) -> bool:
|
|
56
|
+
return (object_path[-1] == "/") and (hcph.get_object(object_path)["ContentLength"] == 0)
|
|
57
|
+
|
|
16
58
|
@click.group()
|
|
17
59
|
@click.argument("credentials")
|
|
18
60
|
@click.version_option(package_name = "NGPIris")
|
|
@@ -28,52 +70,90 @@ def cli(context : Context, credentials : str):
|
|
|
28
70
|
|
|
29
71
|
@cli.command()
|
|
30
72
|
@click.argument("bucket")
|
|
31
|
-
@click.argument("
|
|
73
|
+
@click.argument("source")
|
|
74
|
+
@click.argument("destination")
|
|
32
75
|
@click.pass_context
|
|
33
|
-
def upload(context : Context, bucket : str,
|
|
76
|
+
def upload(context : Context, bucket : str, source : str, destination : str):
|
|
34
77
|
"""
|
|
35
78
|
Upload files to an HCP bucket/namespace.
|
|
36
79
|
|
|
37
80
|
BUCKET is the name of the upload destination bucket.
|
|
38
81
|
|
|
39
|
-
|
|
82
|
+
SOURCE is the path to the file or folder of files to be uploaded.
|
|
83
|
+
|
|
84
|
+
DESTINATION is the destination path on the HCP.
|
|
40
85
|
"""
|
|
41
86
|
hcph : HCPHandler = get_HCPHandler(context)
|
|
42
87
|
hcph.mount_bucket(bucket)
|
|
43
|
-
if Path(
|
|
44
|
-
hcph.upload_folder(
|
|
88
|
+
if Path(source).is_dir():
|
|
89
|
+
hcph.upload_folder(source, destination)
|
|
45
90
|
else:
|
|
46
|
-
hcph.upload_file(
|
|
91
|
+
hcph.upload_file(source, destination)
|
|
47
92
|
|
|
48
93
|
@cli.command()
|
|
49
94
|
@click.argument("bucket")
|
|
50
|
-
@click.argument("
|
|
51
|
-
@click.argument("
|
|
95
|
+
@click.argument("source")
|
|
96
|
+
@click.argument("destination")
|
|
52
97
|
@click.option(
|
|
53
98
|
"-f",
|
|
54
99
|
"--force",
|
|
55
|
-
help = "Overwrite existing file with the same name",
|
|
100
|
+
help = "Overwrite existing file with the same name (single file download only)",
|
|
101
|
+
is_flag = True
|
|
102
|
+
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"-iw",
|
|
105
|
+
"--ignore_warning",
|
|
106
|
+
help = "Ignore the download limit",
|
|
56
107
|
is_flag = True
|
|
57
108
|
)
|
|
58
109
|
@click.pass_context
|
|
59
|
-
def download(context : Context, bucket : str,
|
|
110
|
+
def download(context : Context, bucket : str, source : str, destination : str, force : bool, ignore_warning : bool):
|
|
60
111
|
"""
|
|
61
|
-
Download a file from an HCP bucket/namespace.
|
|
112
|
+
Download a file or folder from an HCP bucket/namespace.
|
|
62
113
|
|
|
63
|
-
BUCKET is the name of the
|
|
114
|
+
BUCKET is the name of the download source bucket.
|
|
64
115
|
|
|
65
|
-
|
|
116
|
+
SOURCE is the path to the object or object folder to be downloaded.
|
|
66
117
|
|
|
67
|
-
|
|
118
|
+
DESTINATION is the folder where the downloaded object or object folder is to be stored locally.
|
|
68
119
|
"""
|
|
69
|
-
if not Path(local_path).exists():
|
|
70
|
-
Path(local_path).mkdir()
|
|
71
|
-
downloaded_object_path = Path(local_path) / Path(object_path).name
|
|
72
|
-
if downloaded_object_path.exists() and not force:
|
|
73
|
-
exit("Object already exists. If you wish to overwrite the existing file, use the -f, --force option")
|
|
74
120
|
hcph : HCPHandler = get_HCPHandler(context)
|
|
75
121
|
hcph.mount_bucket(bucket)
|
|
76
|
-
|
|
122
|
+
if not Path(destination).exists():
|
|
123
|
+
Path(destination).mkdir()
|
|
124
|
+
|
|
125
|
+
if object_is_folder(source, hcph):
|
|
126
|
+
if source == "/":
|
|
127
|
+
source = ""
|
|
128
|
+
|
|
129
|
+
cumulative_download_size = Byte(0)
|
|
130
|
+
if not ignore_warning:
|
|
131
|
+
click.echo("Computing download size...")
|
|
132
|
+
for object in hcph.list_objects(source):
|
|
133
|
+
object : dict
|
|
134
|
+
cumulative_download_size += Byte(object["Size"])
|
|
135
|
+
if cumulative_download_size >= TiB(1):
|
|
136
|
+
click.echo("WARNING: You are about to download more than 1 TB of data. Is this your intention? [y/N]: ", nl = False)
|
|
137
|
+
inp = click.getchar(True)
|
|
138
|
+
if inp == "y" or inp == "Y":
|
|
139
|
+
break
|
|
140
|
+
else: # inp == "n" or inp == "N" or something else
|
|
141
|
+
exit("\nAborting download")
|
|
142
|
+
|
|
143
|
+
hcph.download_folder(source, Path(destination).as_posix())
|
|
144
|
+
else:
|
|
145
|
+
if Byte(hcph.get_object(source)["ContentLength"]) >= TiB(1):
|
|
146
|
+
click.echo("WARNING: You are about to download more than 1 TB of data. Is this your intention? [y/N]: ", nl = False)
|
|
147
|
+
inp = click.getchar(True)
|
|
148
|
+
if inp == "y" or inp == "Y":
|
|
149
|
+
pass
|
|
150
|
+
else: # inp == "n" or inp == "N" or something else
|
|
151
|
+
exit("\nAborting download")
|
|
152
|
+
|
|
153
|
+
downloaded_source = Path(destination) / Path(source).name
|
|
154
|
+
if downloaded_source.exists() and not force:
|
|
155
|
+
exit("Object already exists. If you wish to overwrite the existing file, use the -f, --force option")
|
|
156
|
+
hcph.download_file(source, downloaded_source.as_posix())
|
|
77
157
|
|
|
78
158
|
@cli.command()
|
|
79
159
|
@click.argument("bucket")
|
|
@@ -133,14 +213,7 @@ def list_objects(context : Context, bucket : str, name_only : bool):
|
|
|
133
213
|
"""
|
|
134
214
|
hcph : HCPHandler = get_HCPHandler(context)
|
|
135
215
|
hcph.mount_bucket(bucket)
|
|
136
|
-
|
|
137
|
-
if name_only:
|
|
138
|
-
click.echo(format_list(objects_list))
|
|
139
|
-
else:
|
|
140
|
-
out = []
|
|
141
|
-
for d in objects_list:
|
|
142
|
-
out.append(dumps(d, indent = 4, default = str) + "\n")
|
|
143
|
-
click.echo("".join(out))
|
|
216
|
+
click.echo_via_pager(_list_objects_generator(hcph, name_only))
|
|
144
217
|
|
|
145
218
|
@cli.command()
|
|
146
219
|
@click.argument("bucket")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class VPNConnectionError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
class NoBucketMounted(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
class BucketNotFound(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
class BucketForbidden(Exception):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
class ObjectAlreadyExist(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class ObjectDoesNotExist(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
class DownloadLimitReached(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
class NotADirectory(Exception):
|
|
23
|
+
pass
|
|
@@ -5,16 +5,26 @@ from NGPIris.hcp.helpers import (
|
|
|
5
5
|
create_access_control_policy,
|
|
6
6
|
check_mounted
|
|
7
7
|
)
|
|
8
|
-
from NGPIris.hcp.exceptions import
|
|
8
|
+
from NGPIris.hcp.exceptions import (
|
|
9
|
+
VPNConnectionError,
|
|
10
|
+
BucketNotFound,
|
|
11
|
+
BucketForbidden,
|
|
12
|
+
ObjectAlreadyExist,
|
|
13
|
+
ObjectDoesNotExist,
|
|
14
|
+
DownloadLimitReached,
|
|
15
|
+
NotADirectory
|
|
16
|
+
)
|
|
9
17
|
|
|
10
18
|
from boto3 import client
|
|
11
19
|
from botocore.client import Config
|
|
20
|
+
from botocore.paginate import PageIterator, Paginator
|
|
12
21
|
from botocore.exceptions import EndpointConnectionError, ClientError
|
|
13
22
|
from boto3.s3.transfer import TransferConfig
|
|
14
23
|
from configparser import ConfigParser
|
|
15
24
|
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
16
27
|
from os import (
|
|
17
|
-
path,
|
|
18
28
|
stat,
|
|
19
29
|
listdir
|
|
20
30
|
)
|
|
@@ -28,6 +38,10 @@ from requests import get
|
|
|
28
38
|
from urllib3 import disable_warnings
|
|
29
39
|
from tqdm import tqdm
|
|
30
40
|
|
|
41
|
+
from bitmath import TiB, Byte
|
|
42
|
+
|
|
43
|
+
from typing import Generator
|
|
44
|
+
|
|
31
45
|
_KB = 1024
|
|
32
46
|
_MB = _KB * _KB
|
|
33
47
|
|
|
@@ -48,10 +62,15 @@ class HCPHandler:
|
|
|
48
62
|
credentials_handler = CredentialsHandler(credentials_path)
|
|
49
63
|
self.hcp = credentials_handler.hcp
|
|
50
64
|
self.endpoint = "https://" + self.hcp["endpoint"]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
|
|
66
|
+
self.tenant = None
|
|
67
|
+
for endpoint_format_string in ["https://{}.ngp-fs1000.vgregion.se", "https://{}.ngp-fs2000.vgregion.se", "https://{}.ngp-fs3000.vgregion.se", "https://{}.vgregion.sjunet.org"]:
|
|
68
|
+
tenant_parse = parse(endpoint_format_string, self.endpoint)
|
|
69
|
+
if type(tenant_parse) is Result:
|
|
70
|
+
self.tenant = str(tenant_parse[0])
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
if not self.tenant:
|
|
55
74
|
raise RuntimeError("Unable to parse endpoint. Make sure that you have entered the correct endpoint in your credentials JSON file. Hint: The endpoint should *not* contain \"https://\" or port numbers")
|
|
56
75
|
self.base_request_url = self.endpoint + ":9090/mapi/tenants/" + self.tenant
|
|
57
76
|
self.aws_access_key_id = self.hcp["aws_access_key_id"]
|
|
@@ -156,19 +175,20 @@ class HCPHandler:
|
|
|
156
175
|
else:
|
|
157
176
|
raise RuntimeError("No bucket selected. Either use `mount_bucket` first or supply the optional `bucket_name` paramter for `test_connection`")
|
|
158
177
|
try:
|
|
159
|
-
response =
|
|
178
|
+
response = dict(self.s3_client.head_bucket(Bucket = bucket_name))
|
|
160
179
|
except EndpointConnectionError as e: # pragma: no cover
|
|
161
180
|
print(e)
|
|
162
181
|
raise VPNConnectionError("Please check your connection and that you have your VPN enabled")
|
|
163
182
|
except ClientError as e:
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
status_code = e.response["ResponseMetadata"].get("HTTPStatusCode", -1)
|
|
184
|
+
match status_code:
|
|
185
|
+
case 404:
|
|
186
|
+
raise BucketNotFound("Bucket \"" + bucket_name + "\" was not found")
|
|
187
|
+
case 403:
|
|
188
|
+
raise BucketForbidden("Bucket \"" + bucket_name + "\" could not be accessed due to lack of permissions")
|
|
166
189
|
except Exception as e: # pragma: no cover
|
|
167
190
|
raise Exception(e)
|
|
168
191
|
|
|
169
|
-
if response["ResponseMetadata"].get("HTTPStatusCode", -1) != 200: # pragma: no cover
|
|
170
|
-
error_msg = "The response code from the request made at " + self.endpoint + " returned status code " + response["ResponseMetadata"]["HTTPStatusCode"]
|
|
171
|
-
raise Exception(error_msg)
|
|
172
192
|
return response
|
|
173
193
|
|
|
174
194
|
def mount_bucket(self, bucket_name : str) -> None:
|
|
@@ -185,6 +205,18 @@ class HCPHandler:
|
|
|
185
205
|
self.test_connection(bucket_name = bucket_name)
|
|
186
206
|
self.bucket_name = bucket_name
|
|
187
207
|
|
|
208
|
+
def create_bucket(self, bucket_name : str) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Create a bucket. The user in the given credentials will be the owner
|
|
211
|
+
of the bucket
|
|
212
|
+
|
|
213
|
+
:param bucket_name: Name of the new bucket
|
|
214
|
+
:type bucket_name: str
|
|
215
|
+
"""
|
|
216
|
+
self.s3_client.create_bucket(
|
|
217
|
+
Bucket = bucket_name
|
|
218
|
+
)
|
|
219
|
+
|
|
188
220
|
def list_buckets(self) -> list[str]:
|
|
189
221
|
"""
|
|
190
222
|
List all available buckets at endpoint.
|
|
@@ -198,27 +230,26 @@ class HCPHandler:
|
|
|
198
230
|
return list_of_buckets
|
|
199
231
|
|
|
200
232
|
@check_mounted
|
|
201
|
-
def list_objects(self, name_only : bool = False) ->
|
|
233
|
+
def list_objects(self, path_key : str = "", name_only : bool = False) -> Generator:
|
|
202
234
|
"""
|
|
203
|
-
List all objects in the mounted bucket
|
|
235
|
+
List all objects in the mounted bucket as a generator. If one wishes to
|
|
236
|
+
get the result as a list, use :py:function:`list` to type cast the generator
|
|
204
237
|
|
|
205
|
-
:param
|
|
238
|
+
:param path_key: Filter string for which keys to list, specifically for finding objects in certain folders.
|
|
239
|
+
:type path_key: str, optional
|
|
240
|
+
:param name_only: If True, yield only a the object names. If False, yield the full metadata about each object. Defaults to False.
|
|
206
241
|
:type name_only: bool, optional
|
|
207
|
-
|
|
208
|
-
:
|
|
209
|
-
|
|
210
|
-
""
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return [object["Key"] for object in list_of_objects]
|
|
219
|
-
else:
|
|
220
|
-
return list_of_objects
|
|
221
|
-
|
|
242
|
+
:yield: A generator of all objects in a bucket
|
|
243
|
+
:rtype: Generator
|
|
244
|
+
"""
|
|
245
|
+
paginator : Paginator = self.s3_client.get_paginator("list_objects_v2")
|
|
246
|
+
pages : PageIterator = paginator.paginate(Bucket = self.bucket_name)
|
|
247
|
+
for object in pages.search("Contents[?starts_with(Key, '" + path_key + "')][]"):
|
|
248
|
+
if name_only:
|
|
249
|
+
yield str(object["Key"])
|
|
250
|
+
else:
|
|
251
|
+
yield object
|
|
252
|
+
|
|
222
253
|
@check_mounted
|
|
223
254
|
def get_object(self, key : str) -> dict:
|
|
224
255
|
"""
|
|
@@ -264,9 +295,13 @@ class HCPHandler:
|
|
|
264
295
|
:param key: Name of the object
|
|
265
296
|
:type key: str
|
|
266
297
|
|
|
267
|
-
:param local_file_path: Path to a file on your local system where the contents of the object file can be put
|
|
298
|
+
:param local_file_path: Path to a file on your local system where the contents of the object file can be put
|
|
268
299
|
:type local_file_path: str
|
|
269
300
|
"""
|
|
301
|
+
try:
|
|
302
|
+
self.get_object(key)
|
|
303
|
+
except:
|
|
304
|
+
raise ObjectDoesNotExist("Could not find object", "\"" + key + "\"", "in bucket", "\"" + str(self.bucket_name) + "\"")
|
|
270
305
|
try:
|
|
271
306
|
file_size : int = self.s3_client.head_object(Bucket = self.bucket_name, Key = key)["ContentLength"]
|
|
272
307
|
with tqdm(
|
|
@@ -284,10 +319,48 @@ class HCPHandler:
|
|
|
284
319
|
)
|
|
285
320
|
except ClientError as e0:
|
|
286
321
|
print(str(e0))
|
|
287
|
-
raise
|
|
322
|
+
raise e0
|
|
288
323
|
except Exception as e: # pragma: no cover
|
|
289
324
|
raise Exception(e)
|
|
290
325
|
|
|
326
|
+
@check_mounted
|
|
327
|
+
def download_folder(self, folder_key : str, local_folder_path : str, use_download_limit : bool = False, download_limit_in_bytes : Byte = TiB(1).to_Byte()) -> None:
|
|
328
|
+
"""
|
|
329
|
+
Download multiple objects from a folder in the mounted bucket
|
|
330
|
+
|
|
331
|
+
:param folder_key: Name of the folder
|
|
332
|
+
:type folder_key: str
|
|
333
|
+
|
|
334
|
+
:param local_folder_path: Path to a folder on your local system where the contents of the objects can be put
|
|
335
|
+
:type local_folder_path: str
|
|
336
|
+
|
|
337
|
+
:param use_download_limit: Boolean choice for using a download limit. Defaults to False
|
|
338
|
+
:type use_download_limit: bool, optional
|
|
339
|
+
|
|
340
|
+
:param download_limit_in_bytes: The optional download limit in Byte (from the package `bitmath`). Defaults to 1 TB (`TiB(1).to_Byte()`)
|
|
341
|
+
:type download_limit_in_bytes: Byte, optional
|
|
342
|
+
|
|
343
|
+
:raises DownloadLimitReached: If download limit was reached while downloading files
|
|
344
|
+
:raises NotADirectory: If local_folder_path is not a directory
|
|
345
|
+
"""
|
|
346
|
+
try:
|
|
347
|
+
self.get_object(folder_key)
|
|
348
|
+
except:
|
|
349
|
+
raise ObjectDoesNotExist("Could not find object", "\"" + folder_key + "\"", "in bucket", "\"" + str(self.bucket_name) + "\"")
|
|
350
|
+
if Path(local_folder_path).is_dir():
|
|
351
|
+
current_download_size_in_bytes = Byte(0) # For tracking download limit
|
|
352
|
+
for object in self.list_objects(folder_key): # Build the tree with directories or add files
|
|
353
|
+
p = Path(local_folder_path) / Path(object["Key"])
|
|
354
|
+
if object["Key"][-1] == "/": # If the object is a "folder"
|
|
355
|
+
p.mkdir()
|
|
356
|
+
else: # If the object is a file
|
|
357
|
+
current_download_size_in_bytes += Byte(object["Size"])
|
|
358
|
+
if current_download_size_in_bytes >= download_limit_in_bytes and use_download_limit:
|
|
359
|
+
raise DownloadLimitReached("The download limit was reached when downloading files")
|
|
360
|
+
self.download_file(object["Key"], p.as_posix())
|
|
361
|
+
else:
|
|
362
|
+
raise NotADirectory(local_folder_path + " is not a directory")
|
|
363
|
+
|
|
291
364
|
@check_mounted
|
|
292
365
|
def upload_file(self, local_file_path : str, key : str = "") -> None:
|
|
293
366
|
"""
|
|
@@ -302,23 +375,26 @@ class HCPHandler:
|
|
|
302
375
|
raise_path_error(local_file_path)
|
|
303
376
|
|
|
304
377
|
if not key:
|
|
305
|
-
file_name =
|
|
378
|
+
file_name = Path(local_file_path).name
|
|
306
379
|
key = file_name
|
|
307
380
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
381
|
+
if self.object_exists(key):
|
|
382
|
+
raise ObjectAlreadyExist("The object \"" + key + "\" already exist in the mounted bucket")
|
|
383
|
+
else:
|
|
384
|
+
file_size : int = stat(local_file_path).st_size
|
|
385
|
+
with tqdm(
|
|
386
|
+
total = file_size,
|
|
387
|
+
unit = "B",
|
|
388
|
+
unit_scale = True,
|
|
389
|
+
desc = local_file_path
|
|
390
|
+
) as pbar:
|
|
391
|
+
self.s3_client.upload_file(
|
|
392
|
+
Filename = local_file_path,
|
|
393
|
+
Bucket = self.bucket_name,
|
|
394
|
+
Key = key,
|
|
395
|
+
Config = self.transfer_config,
|
|
396
|
+
Callback = lambda bytes_transferred : pbar.update(bytes_transferred)
|
|
397
|
+
)
|
|
322
398
|
|
|
323
399
|
@check_mounted
|
|
324
400
|
def upload_folder(self, local_folder_path : str, key : str = "") -> None:
|
|
@@ -355,19 +431,19 @@ class HCPHandler:
|
|
|
355
431
|
|
|
356
432
|
deletion_dict = {"Objects": object_list}
|
|
357
433
|
|
|
358
|
-
list_of_objects_before = self.list_objects(True)
|
|
359
|
-
|
|
360
434
|
response : dict = self.s3_client.delete_objects(
|
|
361
435
|
Bucket = self.bucket_name,
|
|
362
436
|
Delete = deletion_dict
|
|
363
437
|
)
|
|
364
438
|
if verbose:
|
|
365
439
|
print(dumps(response, indent=4))
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
440
|
+
|
|
441
|
+
deleted_dict_list : list[dict] = response["Deleted"]
|
|
442
|
+
does_not_exist = []
|
|
443
|
+
for deleted_dict in deleted_dict_list:
|
|
444
|
+
if not "VersionId" in deleted_dict:
|
|
370
445
|
does_not_exist.append("- " + key + "\n")
|
|
446
|
+
if does_not_exist:
|
|
371
447
|
print("The following could not be deleted because they didn't exist: \n" + "".join(does_not_exist))
|
|
372
448
|
|
|
373
449
|
@check_mounted
|
|
@@ -409,7 +485,7 @@ class HCPHandler:
|
|
|
409
485
|
@check_mounted
|
|
410
486
|
def search_objects_in_bucket(self, search_string : str, case_sensitive : bool = False) -> list[str]:
|
|
411
487
|
"""
|
|
412
|
-
Simple search method using substrings in order to find certain objects. Case insensitive by default.
|
|
488
|
+
Simple search method using substrings in order to find certain objects. Case insensitive by default. Does not utilise the HCI
|
|
413
489
|
|
|
414
490
|
:param search_string: Substring to be used in the search
|
|
415
491
|
:type search_string: str
|
|
@@ -421,7 +497,7 @@ class HCPHandler:
|
|
|
421
497
|
:rtype: list[str]
|
|
422
498
|
"""
|
|
423
499
|
search_result : list[str] = []
|
|
424
|
-
for key in self.list_objects(True):
|
|
500
|
+
for key in self.list_objects(name_only = True):
|
|
425
501
|
parse_object = search(
|
|
426
502
|
search_string,
|
|
427
503
|
key,
|
|
@@ -26,6 +26,8 @@ def raise_path_error(path : str):
|
|
|
26
26
|
if not p.exists(path):
|
|
27
27
|
raise FileNotFoundError("\"" + path + "\"" + " does not exist")
|
|
28
28
|
|
|
29
|
+
# Decorator for checking if a bucket is mounted. This is meant to be used by
|
|
30
|
+
# class methods, hence its possibly odd typing.
|
|
29
31
|
T = TypeVar("T")
|
|
30
32
|
P = ParamSpec("P")
|
|
31
33
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
|
|
2
|
+
from NGPIris.hcp import HCPHandler
|
|
3
|
+
from NGPIris.hcp.helpers import check_mounted
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HCPStatistics(HCPHandler):
|
|
7
|
+
def __init__(self, credentials_path: str, use_ssl: bool = False, proxy_path: str = "", custom_config_path: str = "") -> None:
|
|
8
|
+
super().__init__(credentials_path, use_ssl, proxy_path, custom_config_path)
|
|
9
|
+
|
|
10
|
+
@check_mounted
|
|
11
|
+
def get_namespace_settings(self) -> dict:
|
|
12
|
+
return self.get_response("/namespaces/" + self.bucket_name) #type: ignore
|
|
13
|
+
|
|
14
|
+
@check_mounted
|
|
15
|
+
def get_namespace_statistics(self) -> dict:
|
|
16
|
+
return self.get_response("/namespaces/" + self.bucket_name + "/statistics") #type: ignore
|
|
17
|
+
|
|
18
|
+
@check_mounted
|
|
19
|
+
def get_namespace_permissions(self) -> dict:
|
|
20
|
+
return self.get_response("/namespaces/" + self.bucket_name + "/permissions") #type: ignore
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: NGPIris
|
|
3
|
-
Version: 5.
|
|
4
|
-
Author-email: Erik Brink <erik.brink@gu.se>
|
|
3
|
+
Version: 5.1.1.dev2
|
|
4
|
+
Author-email: Erik Brink <erik.brink@gu.se>, Isak Sylvin <isak.sylvin@gu.se>
|
|
5
5
|
Description-Content-Type: text/markdown
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Dist: requests>=2.31.0
|
|
8
|
-
Requires-Dist: urllib3==1.26.
|
|
9
|
-
Requires-Dist: requests>=2.31.0
|
|
8
|
+
Requires-Dist: urllib3==1.26.19
|
|
10
9
|
Requires-Dist: boto3>=1.26.76
|
|
11
10
|
Requires-Dist: parse>=1.19.1
|
|
12
11
|
Requires-Dist: tqdm>=4.66.2
|
|
13
12
|
Requires-Dist: click>=8.1.7
|
|
13
|
+
Requires-Dist: bitmath==1.3.3.1
|
|
14
|
+
Requires-Dist: tabulate==0.9.0
|
|
14
15
|
|
|
15
16
|
# NGP IRIS 👀
|
|
16
17
|
NGP IRIS, or just Iris, is a tool for interacting with a Hitachi Content Platform (HCP) using S3 in the `boto3` package. NGP Iris is designed with two use cases in mind:
|
|
@@ -281,6 +282,19 @@ pytype
|
|
|
281
282
|
```shell
|
|
282
283
|
pytest
|
|
283
284
|
```
|
|
285
|
+
## Generating new documentation
|
|
286
|
+
With force flag `-f`:
|
|
287
|
+
```shell
|
|
288
|
+
cd docs/
|
|
289
|
+
sphinx-apidoc ../NGPIris/ -o . -F -f
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Without force flag
|
|
293
|
+
```shell
|
|
294
|
+
cd docs/
|
|
295
|
+
sphinx-apidoc ../NGPIris/ -o . -F
|
|
296
|
+
```
|
|
297
|
+
|
|
284
298
|
## Compiling the documentation
|
|
285
299
|
```shell
|
|
286
300
|
cd docs/
|