cloud-files 4.26.0__tar.gz → 4.27.0__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 (37) hide show
  1. {cloud-files-4.26.0 → cloud-files-4.27.0}/ChangeLog +5 -0
  2. {cloud-files-4.26.0 → cloud-files-4.27.0}/PKG-INFO +1 -1
  3. {cloud-files-4.26.0 → cloud-files-4.27.0}/automated_test.py +9 -0
  4. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloud_files.egg-info/PKG-INFO +1 -1
  5. cloud-files-4.27.0/cloud_files.egg-info/pbr.json +1 -0
  6. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/cloudfiles.py +2 -1
  7. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/interfaces.py +37 -8
  8. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/paths.py +15 -4
  9. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/secrets.py +18 -0
  10. cloud-files-4.26.0/cloud_files.egg-info/pbr.json +0 -1
  11. {cloud-files-4.26.0 → cloud-files-4.27.0}/.github/workflows/test-suite.yml +0 -0
  12. {cloud-files-4.26.0 → cloud-files-4.27.0}/AUTHORS +0 -0
  13. {cloud-files-4.26.0 → cloud-files-4.27.0}/LICENSE +0 -0
  14. {cloud-files-4.26.0 → cloud-files-4.27.0}/MANIFEST.in +0 -0
  15. {cloud-files-4.26.0 → cloud-files-4.27.0}/README.md +0 -0
  16. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloud_files.egg-info/SOURCES.txt +0 -0
  17. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloud_files.egg-info/dependency_links.txt +0 -0
  18. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloud_files.egg-info/entry_points.txt +0 -0
  19. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloud_files.egg-info/not-zip-safe +0 -0
  20. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloud_files.egg-info/requires.txt +0 -0
  21. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloud_files.egg-info/top_level.txt +0 -0
  22. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/__init__.py +0 -0
  23. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/compression.py +0 -0
  24. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/connectionpools.py +0 -0
  25. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/exceptions.py +0 -0
  26. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/gcs.py +0 -0
  27. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/lib.py +0 -0
  28. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/resumable_tools.py +0 -0
  29. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/scheduler.py +0 -0
  30. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/threaded_queue.py +0 -0
  31. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles/typing.py +0 -0
  32. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles_cli/LICENSE +0 -0
  33. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles_cli/__init__.py +0 -0
  34. {cloud-files-4.26.0 → cloud-files-4.27.0}/cloudfiles_cli/cloudfiles_cli.py +0 -0
  35. {cloud-files-4.26.0 → cloud-files-4.27.0}/requirements.txt +0 -0
  36. {cloud-files-4.26.0 → cloud-files-4.27.0}/setup.cfg +0 -0
  37. {cloud-files-4.26.0 → cloud-files-4.27.0}/setup.py +0 -0
@@ -1,6 +1,11 @@
1
1
  CHANGES
2
2
  =======
3
3
 
4
+ 4.27.0
5
+ ------
6
+
7
+ * feat: add middleauth+https paths indicate CAVE interface (#106)
8
+
4
9
  4.26.0
5
10
  ------
6
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-files
3
- Version: 4.26.0
3
+ Version: 4.27.0
4
4
  Summary: Fast access to cloud storage and local FS.
5
5
  Home-page: https://github.com/seung-lab/cloud-files/
6
6
  Author: William Silversmith
@@ -564,6 +564,15 @@ def test_path_extraction():
564
564
  'a/username2/b/c/d', None, None
565
565
  ))
566
566
 
567
+ def test_middleauth_path_extraction():
568
+ import cloudfiles.paths
569
+ path = cloudfiles.paths.extract('middleauth+https://example.com/wow/cool/')
570
+ assert path.format == 'precomputed'
571
+ assert path.protocol == 'middleauth+https'
572
+ assert path.bucket is None
573
+ assert path.path == 'wow/cool/'
574
+ assert path.host == "https://example.com"
575
+
567
576
  @pytest.mark.parametrize("protocol", ('mem', 'file', 's3'))
568
577
  def test_access_non_cannonical_minimal_path(s3, protocol):
569
578
  from cloudfiles import CloudFiles, exceptions
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloud-files
3
- Version: 4.26.0
3
+ Version: 4.27.0
4
4
  Summary: Fast access to cloud storage and local FS.
5
5
  Home-page: https://github.com/seung-lab/cloud-files/
6
6
  Author: William Silversmith
@@ -0,0 +1 @@
1
+ {"git_version": "e4b04bf", "is_release": true}
@@ -44,7 +44,7 @@ from .scheduler import schedule_jobs
44
44
  from .interfaces import (
45
45
  FileInterface, HttpInterface,
46
46
  S3Interface, GoogleCloudStorageInterface,
47
- MemoryInterface
47
+ MemoryInterface, CaveInterface,
48
48
  )
49
49
 
50
50
  INTERFACES = {
@@ -54,6 +54,7 @@ INTERFACES = {
54
54
  'http': HttpInterface,
55
55
  'https': HttpInterface,
56
56
  'mem': MemoryInterface,
57
+ 'middleauth+https': CaveInterface,
57
58
  }
58
59
  for alias in ALIASES:
59
60
  INTERFACES[alias] = S3Interface
@@ -24,7 +24,12 @@ from .compression import COMPRESSION_TYPES
24
24
  from .connectionpools import S3ConnectionPool, GCloudBucketPool, MemoryPool, MEMORY_DATA
25
25
  from .exceptions import MD5IntegrityError, CompressionError
26
26
  from .lib import mkdir, sip, md5, validate_s3_multipart_etag
27
- from .secrets import http_credentials, CLOUD_FILES_DIR, CLOUD_FILES_LOCK_DIR
27
+ from .secrets import (
28
+ http_credentials,
29
+ cave_credentials,
30
+ CLOUD_FILES_DIR,
31
+ CLOUD_FILES_LOCK_DIR,
32
+ )
28
33
 
29
34
  COMPRESSION_EXTENSIONS = ('.gz', '.br', '.zstd','.bz2','.xz')
30
35
  GZIP_TYPES = (True, 'gzip', 1)
@@ -731,6 +736,9 @@ class HttpInterface(StorageInterface):
731
736
  if secrets and 'user' in secrets and 'password' in secrets:
732
737
  self.session.auth = (secrets['user'], secrets['password'])
733
738
 
739
+ def default_headers(self):
740
+ return {}
741
+
734
742
  def get_path_to_file(self, file_path):
735
743
  return posixpath.join(self._path.host, self._path.path, file_path)
736
744
 
@@ -749,7 +757,8 @@ class HttpInterface(StorageInterface):
749
757
  @retry
750
758
  def head(self, file_path):
751
759
  key = self.get_path_to_file(file_path)
752
- with self.session.head(key) as resp:
760
+ headers = self.default_headers()
761
+ with self.session.head(key, headers=headers) as resp:
753
762
  resp.raise_for_status()
754
763
  return resp.headers
755
764
 
@@ -761,13 +770,14 @@ class HttpInterface(StorageInterface):
761
770
  def get_file(self, file_path, start=None, end=None, part_size=None):
762
771
  key = self.get_path_to_file(file_path)
763
772
 
773
+ headers = self.default_headers()
764
774
  if start is not None or end is not None:
765
775
  start = int(start) if start is not None else 0
766
776
  end = int(end - 1) if end is not None else ''
767
- headers = { "Range": "bytes={}-{}".format(start, end) }
768
- resp = self.session.get(key, headers=headers)
769
- else:
770
- resp = self.session.get(key)
777
+ headers["Range"] = f"bytes={start}-{end}"
778
+
779
+ resp = self.session.get(key, headers=headers)
780
+
771
781
  if resp.status_code in (404, 403):
772
782
  return (None, None, None, None)
773
783
  resp.close()
@@ -788,7 +798,8 @@ class HttpInterface(StorageInterface):
788
798
  @retry
789
799
  def exists(self, file_path):
790
800
  key = self.get_path_to_file(file_path)
791
- with self.session.get(key, stream=True) as resp:
801
+ headers = self.default_headers()
802
+ with self.session.get(key, stream=True, headers=headers) as resp:
792
803
  return resp.ok
793
804
 
794
805
  def files_exist(self, file_paths):
@@ -805,11 +816,15 @@ class HttpInterface(StorageInterface):
805
816
  if prefix and prefix[-1] != '/':
806
817
  prefix += '/'
807
818
 
819
+ headers = self.default_headers()
820
+
808
821
  @retry
809
822
  def request(token):
823
+ nonlocal headers
810
824
  results = self.session.get(
811
825
  f"https://storage.googleapis.com/storage/v1/b/{bucket}/o",
812
826
  params={ "prefix": prefix, "pageToken": token },
827
+ headers=headers,
813
828
  )
814
829
  results.raise_for_status()
815
830
  results.close()
@@ -832,12 +847,13 @@ class HttpInterface(StorageInterface):
832
847
  baseurl = posixpath.join(self._path.host, self._path.path)
833
848
 
834
849
  directories = ['']
850
+ headers = self.default_headers()
835
851
 
836
852
  while directories:
837
853
  directory = directories.pop()
838
854
  url = posixpath.join(baseurl, directory)
839
855
 
840
- resp = requests.get(url)
856
+ resp = requests.get(url, headers=headers)
841
857
  resp.raise_for_status()
842
858
 
843
859
  if 'text/html' not in resp.headers["Content-Type"]:
@@ -1200,3 +1216,16 @@ class S3Interface(StorageInterface):
1200
1216
  with S3_BUCKET_POOL_LOCK:
1201
1217
  pool = S3_POOL[S3ConnectionPoolParams(service, self._path.bucket, self._request_payer)]
1202
1218
  pool.release_connection(self._conn)
1219
+
1220
+ class CaveInterface(HttpInterface):
1221
+ """
1222
+ CAVE is an internal system that powers proofreading
1223
+ systems in Seung Lab. If you have no idea what this
1224
+ is, don't worry about it.
1225
+ see: https://github.com/CAVEconnectome
1226
+ """
1227
+ def default_headers(self):
1228
+ cred = cave_credentials()
1229
+ return {
1230
+ "Authorization": f"Bearer {cred['token']}",
1231
+ }
@@ -26,7 +26,8 @@ ALIASES_FROM_FILE = None
26
26
  ALIASES = {}
27
27
  BASE_ALLOWED_PROTOCOLS = [
28
28
  'gs', 'file', 's3',
29
- 'http', 'https', 'mem'
29
+ 'http', 'https', 'mem',
30
+ 'middleauth+https', 'ngauth+https',
30
31
  ]
31
32
  ALLOWED_PROTOCOLS = list(BASE_ALLOWED_PROTOCOLS)
32
33
  ALLOWED_FORMATS = [
@@ -69,7 +70,13 @@ def cloudpath_error(cloudpath):
69
70
  def mkregexp():
70
71
  fmt_capture = r'|'.join(ALLOWED_FORMATS)
71
72
  fmt_capture = "(?:(?P<fmt>{})://)".format(fmt_capture)
72
- proto_capture = r'|'.join(ALLOWED_PROTOCOLS)
73
+
74
+ allowed_protos = [
75
+ p.replace('+', r'\+')
76
+ for p in ALLOWED_PROTOCOLS
77
+ ]
78
+
79
+ proto_capture = r'|'.join(allowed_protos)
73
80
  proto_capture = "(?:(?P<proto>{})://)".format(proto_capture)
74
81
  regexp = "{}?{}?".format(fmt_capture, proto_capture)
75
82
  return regexp
@@ -292,8 +299,12 @@ def extract_format_protocol(cloudpath:str, allow_defaults=True) -> tuple:
292
299
  proto = m.group('proto')
293
300
  endpoint = None
294
301
 
295
- if proto in ('http', 'https'):
296
- cloudpath = proto + "://" + cloudpath
302
+ tmp_proto = None
303
+ if proto is not None:
304
+ tmp_proto = proto.replace("middleauth+", "").replace("ngauth+", "")
305
+
306
+ if tmp_proto in ('http', 'https'):
307
+ cloudpath = tmp_proto + "://" + cloudpath
297
308
  parse = urllib.parse.urlparse(cloudpath)
298
309
  endpoint = parse.scheme + "://" + parse.netloc
299
310
  cloudpath = cloudpath.replace(endpoint, '', 1)
@@ -137,6 +137,24 @@ def aws_credentials(bucket = '', service = 'aws', skip_files=False):
137
137
  AWS_CREDENTIALS_CACHE[service][bucket] = aws_credentials
138
138
  return aws_credentials
139
139
 
140
+ CAVE_CREDENTIALS = None
141
+ def cave_credentials():
142
+ global CAVE_CREDENTIALS
143
+ default_file_path = 'cave-secret.json'
144
+ path = secretpath(default_file_path)
145
+
146
+ if CAVE_CREDENTIALS:
147
+ return CAVE_CREDENTIALS
148
+
149
+ if os.path.exists(path):
150
+ with open(path, 'rt') as f:
151
+ CAVE_CREDENTIALS = json.loads(f.read())
152
+ else:
153
+ CAVE_CREDENTIALS = None
154
+
155
+ return CAVE_CREDENTIALS
156
+
157
+
140
158
  HTTP_CREDENTIALS = None
141
159
  def http_credentials():
142
160
  global HTTP_CREDENTIALS
@@ -1 +0,0 @@
1
- {"git_version": "3ae7c76", "is_release": true}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes