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.
Files changed (133) hide show
  1. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/cli/__init__.py +103 -30
  2. ngpiris-5.1.1.dev2/NGPIris/hcp/exceptions.py +23 -0
  3. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hcp/hcp.py +131 -55
  4. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hcp/helpers.py +2 -0
  5. ngpiris-5.1.1.dev2/NGPIris/hcp/statistics.py +20 -0
  6. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2/NGPIris.egg-info}/PKG-INFO +18 -4
  7. ngpiris-5.1.1.dev2/NGPIris.egg-info/SOURCES.txt +127 -0
  8. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/requires.txt +3 -2
  9. ngpiris-5.1.1.dev2/PKG-INFO +302 -0
  10. ngpiris-5.0.1/NGPIris.egg-info/PKG-INFO → ngpiris-5.1.1.dev2/README.md +14 -15
  11. ngpiris-5.1.1.dev2/docs/_build/.DS_Store +0 -0
  12. ngpiris-5.1.1.dev2/docs/_build/doctrees/IRIS_5_changelog.doctree +0 -0
  13. ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.cli.doctree +0 -0
  14. ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.doctree +0 -0
  15. ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.hci.doctree +0 -0
  16. ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.hcp.doctree +0 -0
  17. ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.parse_credentials.doctree +0 -0
  18. ngpiris-5.1.1.dev2/docs/_build/doctrees/NGPIris.utils.doctree +0 -0
  19. ngpiris-5.1.1.dev2/docs/_build/doctrees/README.doctree +0 -0
  20. ngpiris-5.1.1.dev2/docs/_build/doctrees/Tutorial.doctree +0 -0
  21. ngpiris-5.1.1.dev2/docs/_build/doctrees/environment.pickle +0 -0
  22. ngpiris-5.1.1.dev2/docs/_build/doctrees/index.doctree +0 -0
  23. ngpiris-5.1.1.dev2/docs/_build/doctrees/modules.doctree +0 -0
  24. ngpiris-5.1.1.dev2/docs/_build/html/.buildinfo +4 -0
  25. ngpiris-5.1.1.dev2/docs/_build/html/IRIS_5_changelog.html +462 -0
  26. ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.cli.html +137 -0
  27. ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.hci.html +280 -0
  28. ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.hcp.html +357 -0
  29. ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.html +236 -0
  30. ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.parse_credentials.html +156 -0
  31. ngpiris-5.1.1.dev2/docs/_build/html/NGPIris.utils.html +143 -0
  32. ngpiris-5.1.1.dev2/docs/_build/html/README.html +681 -0
  33. ngpiris-5.1.1.dev2/docs/_build/html/Tutorial.html +796 -0
  34. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/cli.html +338 -0
  35. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hci/exceptions.html +106 -0
  36. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hci/hci.html +250 -0
  37. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hci/helpers.html +204 -0
  38. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/exceptions.html +117 -0
  39. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/hcp.html +655 -0
  40. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/helpers.html +148 -0
  41. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/hcp/statistics.html +137 -0
  42. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/parse_credentials/parse_credentials.html +170 -0
  43. ngpiris-5.1.1.dev2/docs/_build/html/_modules/NGPIris/utils/utils.html +115 -0
  44. ngpiris-5.1.1.dev2/docs/_build/html/_modules/index.html +108 -0
  45. ngpiris-5.1.1.dev2/docs/_build/html/_sources/IRIS_5_changelog.md.txt +43 -0
  46. ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.cli.rst.txt +10 -0
  47. ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.hci.rst.txt +37 -0
  48. ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.hcp.rst.txt +45 -0
  49. ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.parse_credentials.rst.txt +21 -0
  50. ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.rst.txt +22 -0
  51. ngpiris-5.1.1.dev2/docs/_build/html/_sources/NGPIris.utils.rst.txt +21 -0
  52. ngpiris-5.1.1.dev2/docs/_build/html/_sources/Tutorial.md.txt +326 -0
  53. ngpiris-5.1.1.dev2/docs/_build/html/_sources/index.rst.txt +21 -0
  54. ngpiris-5.1.1.dev2/docs/_build/html/_static/alabaster.css +708 -0
  55. ngpiris-5.1.1.dev2/docs/_build/html/_static/basic.css +925 -0
  56. ngpiris-5.1.1.dev2/docs/_build/html/_static/custom.css +1 -0
  57. ngpiris-5.1.1.dev2/docs/_build/html/_static/debug.css +69 -0
  58. ngpiris-5.1.1.dev2/docs/_build/html/_static/doctools.js +156 -0
  59. ngpiris-5.1.1.dev2/docs/_build/html/_static/documentation_options.js +13 -0
  60. ngpiris-5.1.1.dev2/docs/_build/html/_static/file.png +0 -0
  61. ngpiris-5.1.1.dev2/docs/_build/html/_static/language_data.js +199 -0
  62. ngpiris-5.1.1.dev2/docs/_build/html/_static/minus.png +0 -0
  63. ngpiris-5.1.1.dev2/docs/_build/html/_static/plus.png +0 -0
  64. ngpiris-5.1.1.dev2/docs/_build/html/_static/pygments.css +83 -0
  65. ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo-extensions.js +0 -0
  66. ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo.js +3 -0
  67. ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo.js.LICENSE.txt +7 -0
  68. ngpiris-5.1.1.dev2/docs/_build/html/_static/scripts/furo.js.map +1 -0
  69. ngpiris-5.1.1.dev2/docs/_build/html/_static/searchtools.js +619 -0
  70. ngpiris-5.1.1.dev2/docs/_build/html/_static/skeleton.css +296 -0
  71. ngpiris-5.1.1.dev2/docs/_build/html/_static/sphinx_highlight.js +154 -0
  72. ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo-extensions.css +2 -0
  73. ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo-extensions.css.map +1 -0
  74. ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo.css +2 -0
  75. ngpiris-5.1.1.dev2/docs/_build/html/_static/styles/furo.css.map +1 -0
  76. ngpiris-5.1.1.dev2/docs/_build/html/genindex.html +476 -0
  77. ngpiris-5.1.1.dev2/docs/_build/html/index.html +159 -0
  78. ngpiris-5.1.1.dev2/docs/_build/html/modules.html +148 -0
  79. ngpiris-5.1.1.dev2/docs/_build/html/objects.inv +0 -0
  80. ngpiris-5.1.1.dev2/docs/_build/html/py-modindex.html +188 -0
  81. ngpiris-5.1.1.dev2/docs/_build/html/search.html +120 -0
  82. ngpiris-5.1.1.dev2/docs/_build/html/searchindex.js +1 -0
  83. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/conf.py +18 -27
  84. ngpiris-5.1.1.dev2/docs/index.rst +21 -0
  85. ngpiris-5.1.1.dev2/docs/modules.rst +7 -0
  86. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/pyproject.toml +8 -6
  87. ngpiris-5.1.1.dev2/tests/data/incomplete_creds_0.json +14 -0
  88. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/test_hcp.py +9 -5
  89. ngpiris-5.0.1/NGPIris/hcp/exceptions.py +0 -8
  90. ngpiris-5.0.1/NGPIris/hcp/statistics.py +0 -25
  91. ngpiris-5.0.1/NGPIris.egg-info/SOURCES.txt +0 -52
  92. ngpiris-5.0.1/docs/index.rst +0 -21
  93. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/LICENSE +0 -0
  94. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/MANIFEST.in +0 -0
  95. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/__init__.py +0 -0
  96. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/__init__.py +0 -0
  97. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/exceptions.py +0 -0
  98. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/hci.py +0 -0
  99. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hci/helpers.py +0 -0
  100. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/hcp/__init__.py +0 -0
  101. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/parse_credentials/__init__.py +0 -0
  102. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/parse_credentials/parse_credentials.py +0 -0
  103. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/utils/__init__.py +0 -0
  104. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris/utils/utils.py +0 -0
  105. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/dependency_links.txt +0 -0
  106. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/entry_points.txt +0 -0
  107. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/NGPIris.egg-info/top_level.txt +0 -0
  108. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/IRIS_5_changelog.md +0 -0
  109. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/Makefile +0 -0
  110. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.cli.rst +0 -0
  111. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.hci.rst +0 -0
  112. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.hcp.rst +0 -0
  113. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.parse_credentials.rst +0 -0
  114. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.rst +0 -0
  115. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/NGPIris.utils.rst +0 -0
  116. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/Tutorial.md +0 -0
  117. /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
  118. /ngpiris-5.0.1/README.md → /ngpiris-5.1.1.dev2/docs/_build/html/_sources/README.md.txt +0 -0
  119. /ngpiris-5.0.1/docs/modules.rst → /ngpiris-5.1.1.dev2/docs/_build/html/_sources/modules.rst.txt +0 -0
  120. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/docs/make.bat +0 -0
  121. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/setup.cfg +0 -0
  122. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/80MB_test_file +0 -0
  123. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/a folder of data/80MB_test_file_0 +0 -0
  124. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/a folder of data/80MB_test_file_1 +0 -0
  125. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/complete_creds_0.json +0 -0
  126. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/complete_creds_1.json +0 -0
  127. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/complete_creds_2.json +0 -0
  128. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_1.json +0 -0
  129. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_2.json +0 -0
  130. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_3.json +0 -0
  131. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/data/incomplete_creds_4.json +0 -0
  132. {ngpiris-5.0.1 → ngpiris-5.1.1.dev2}/tests/test_hci.py +0 -0
  133. {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 dumps, dump
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("file-or-folder")
73
+ @click.argument("source")
74
+ @click.argument("destination")
32
75
  @click.pass_context
33
- def upload(context : Context, bucket : str, file_or_folder : 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
- FILE-OR-FOLDER is the path to the file or folder of files to be uploaded.
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(file_or_folder).is_dir():
44
- hcph.upload_folder(file_or_folder)
88
+ if Path(source).is_dir():
89
+ hcph.upload_folder(source, destination)
45
90
  else:
46
- hcph.upload_file(file_or_folder)
91
+ hcph.upload_file(source, destination)
47
92
 
48
93
  @cli.command()
49
94
  @click.argument("bucket")
50
- @click.argument("object_path")
51
- @click.argument("local_path")
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, object_path : str, local_path : str, force : bool):
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 upload destination bucket.
114
+ BUCKET is the name of the download source bucket.
64
115
 
65
- OBJECT_PATH is the path to the object to be downloaded.
116
+ SOURCE is the path to the object or object folder to be downloaded.
66
117
 
67
- LOCAL_PATH is the folder where the downloaded object is to be stored locally.
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
- hcph.download_file(object_path, downloaded_object_path.as_posix())
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
- objects_list = hcph.list_objects(name_only)
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
- tenant_parse = parse("https://{}.hcp1.vgregion.se", self.endpoint)
52
- if type(tenant_parse) is Result:
53
- self.tenant = str(tenant_parse[0])
54
- else: # pragma: no cover
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 = dict(self.s3_client.head_bucket(Bucket = bucket_name))
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
- print(e)
165
- raise BucketNotFound("Bucket \"" + bucket_name + "\" was not found")
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) -> list:
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 name_only: If True, return only a list of the object names. If False, return the full metadata about each object. Defaults to False.
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
- :return: A list of of either strings or a list of object metadata (the form of a dictionary)
209
- :rtype: list
210
- """
211
- response_list_objects = dict(self.s3_client.list_objects_v2(
212
- Bucket = self.bucket_name
213
- ))
214
- if "Contents" not in response_list_objects.keys(): # pragma: no cover
215
- return []
216
- list_of_objects : list[dict] = response_list_objects["Contents"]
217
- if name_only:
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 Exception("Could not find object", "\"" + key + "\"", "in bucket", "\"" + str(self.bucket_name) + "\"")
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 = path.basename(local_file_path)
378
+ file_name = Path(local_file_path).name
306
379
  key = file_name
307
380
 
308
- file_size : int = stat(local_file_path).st_size
309
- with tqdm(
310
- total = file_size,
311
- unit = "B",
312
- unit_scale = True,
313
- desc = local_file_path
314
- ) as pbar:
315
- self.s3_client.upload_file(
316
- Filename = local_file_path,
317
- Bucket = self.bucket_name,
318
- Key = key,
319
- Config = self.transfer_config,
320
- Callback = lambda bytes_transferred : pbar.update(bytes_transferred)
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
- diff : set[str] = set(keys) - set(list_of_objects_before)
367
- if diff:
368
- does_not_exist = []
369
- for key in diff:
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.0.1
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.18
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/