s3path
Advanced tools
| # Marker file for PEP 561. The mypy package uses inline types. |
+3
-3
| Metadata-Version: 2.1 | ||
| Name: s3path | ||
| Version: 0.5.8 | ||
| Version: 0.6.0 | ||
| Home-page: https://github.com/liormizr/s3path | ||
@@ -14,3 +14,2 @@ Author: Lior Mizrahi | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
@@ -20,3 +19,4 @@ Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Requires-Python: >=3.8 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Requires-Python: >=3.9 | ||
| Description-Content-Type: text/x-rst | ||
@@ -23,0 +23,0 @@ License-File: LICENSE |
| Metadata-Version: 2.1 | ||
| Name: s3path | ||
| Version: 0.5.8 | ||
| Version: 0.6.0 | ||
| Home-page: https://github.com/liormizr/s3path | ||
@@ -14,3 +14,2 @@ Author: Lior Mizrahi | ||
| Classifier: Programming Language :: Python | ||
| Classifier: Programming Language :: Python :: 3.8 | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
@@ -20,3 +19,4 @@ Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Requires-Python: >=3.8 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Requires-Python: >=3.9 | ||
| Description-Content-Type: text/x-rst | ||
@@ -23,0 +23,0 @@ License-File: LICENSE |
@@ -10,2 +10,3 @@ LICENSE | ||
| s3path/old_versions.py | ||
| s3path/py.typed | ||
| s3path.egg-info/PKG-INFO | ||
@@ -12,0 +13,0 @@ s3path.egg-info/SOURCES.txt |
@@ -8,3 +8,3 @@ """ | ||
| __version__ = '0.5.8' | ||
| __version__ = '0.6.0' | ||
| __all__ = ( | ||
@@ -11,0 +11,0 @@ 'Path', |
| import sys | ||
| import importlib.util | ||
| from warnings import warn | ||
| from os import stat_result | ||
@@ -496,2 +497,4 @@ from threading import Lock | ||
| if glob_new_algorithm is not None: | ||
| warn(f'glob_new_algorithm Configuration is Deprecated, ' | ||
| f'in the new version we use only the new algorithm for Globing', category=DeprecationWarning) | ||
| self.general_options[path_name] = {'glob_new_algorithm': glob_new_algorithm} | ||
@@ -498,0 +501,0 @@ self.get_configuration.cache_clear() |
@@ -44,4 +44,3 @@ from __future__ import annotations | ||
| class _S3Flavour: | ||
| class _S3Parser: | ||
| def __getattr__(self, name): | ||
@@ -51,5 +50,2 @@ return getattr(posixpath, name) | ||
| flavour = _S3Flavour() | ||
| class PureS3Path(PurePath): | ||
@@ -61,3 +57,5 @@ """ | ||
| """ | ||
| _flavour = flavour | ||
| parser = _flavour = _S3Parser() # _flavour is not relevant after Python version 3.13 | ||
| __slots__ = () | ||
@@ -76,3 +74,6 @@ | ||
| self._raw_paths = new_parts | ||
| self._load_parts() | ||
| if sys.version_info >= (3, 13): | ||
| self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path) | ||
| else: | ||
| self._load_parts() | ||
@@ -102,3 +103,3 @@ @classmethod | ||
| """ | ||
| bucket = cls(cls._flavour.sep, bucket) | ||
| bucket = cls(cls.parser.sep, bucket) | ||
| if len(bucket.parts) != 2: | ||
@@ -135,3 +136,3 @@ raise ValueError(f'bucket argument contains more then one path element: {bucket}') | ||
| self._absolute_path_validation() | ||
| key = self._flavour.sep.join(self.parts[2:]) | ||
| key = self.parser.sep.join(self.parts[2:]) | ||
| return key | ||
@@ -285,3 +286,3 @@ | ||
| class S3Path(_PathNotSupportedMixin, Path, PureS3Path): | ||
| class S3Path(_PathNotSupportedMixin, PureS3Path, Path): | ||
| def stat(self, *, follow_symlinks: bool = True) -> accessor.StatResult: | ||
@@ -322,3 +323,3 @@ """ | ||
| def rename(self, target): # todo: Union[str, S3Path]) -> S3Path: | ||
| def rename(self, target): | ||
| """ | ||
@@ -337,3 +338,3 @@ Renames this file or Bucket / key prefix / key to the given target. | ||
| def replace(self, target): # todo: Union[str, S3Path]) -> S3Path: | ||
| def replace(self, target): | ||
| """ | ||
@@ -396,3 +397,3 @@ Renames this Bucket / key prefix / key to the given target. | ||
| raise FileNotFoundError(f'Only bucket path can be created, got {self}') | ||
| if type(self)(self._flavour.sep, self.bucket).exists(): | ||
| if type(self)(self.parser.sep, self.bucket).exists(): | ||
| raise FileExistsError(f'Bucket {self.bucket} already exists') | ||
@@ -438,3 +439,3 @@ accessor.mkdir(self, mode) | ||
| def iterdir(self): # todo: -> Generator[S3Path, None, None]: | ||
| def iterdir(self): | ||
| """ | ||
@@ -445,3 +446,3 @@ When the path points to a Bucket or a key prefix, yield path objects of the directory contents | ||
| for name in accessor.listdir(self): | ||
| yield self._make_child_relpath(name) | ||
| yield self / name | ||
@@ -467,27 +468,46 @@ def open( | ||
| def glob(self, pattern: str): # todo: -> Generator[S3Path, None, None]: | ||
| def glob(self, pattern: str, *, case_sensitive=None, recurse_symlinks=False): | ||
| """ | ||
| Glob the given relative pattern in the Bucket / key prefix represented by this path, | ||
| yielding all matching files (of any kind) | ||
| The glob method is using a new Algorithm that better fit S3 API | ||
| """ | ||
| self._absolute_path_validation() | ||
| general_options = accessor.configuration_map.get_general_options(self) | ||
| glob_new_algorithm = general_options['glob_new_algorithm'] | ||
| if not glob_new_algorithm: | ||
| yield from super().glob(pattern) | ||
| return | ||
| yield from self._glob(pattern) | ||
| if case_sensitive is False or recurse_symlinks is True: | ||
| raise ValueError('Glob is case-sensitive and no symbolic links are allowed') | ||
| def rglob(self, pattern: str): # todo: -> Generator[S3Path, None, None]: | ||
| sys.audit("pathlib.Path.glob", self, pattern) | ||
| if not pattern: | ||
| raise ValueError(f'Unacceptable pattern: {pattern}') | ||
| drv, root, pattern_parts = self._parse_path(pattern) | ||
| if drv or root: | ||
| raise NotImplementedError("Non-relative patterns are unsupported") | ||
| for part in pattern_parts: | ||
| if part != '**' and '**' in part: | ||
| raise ValueError("Invalid pattern: '**' can only be an entire path component") | ||
| selector = _Selector(self, pattern=pattern) | ||
| yield from selector.select() | ||
| def rglob(self, pattern: str, *, case_sensitive=None, recurse_symlinks=False): | ||
| """ | ||
| This is like calling S3Path.glob with "**/" added in front of the given relative pattern | ||
| The rglob method is using a new Algorithm that better fit S3 API | ||
| """ | ||
| self._absolute_path_validation() | ||
| general_options = accessor.configuration_map.get_general_options(self) | ||
| glob_new_algorithm = general_options['glob_new_algorithm'] | ||
| if not glob_new_algorithm: | ||
| yield from super().rglob(pattern) | ||
| return | ||
| yield from self._rglob(pattern) | ||
| sys.audit("pathlib.Path.rglob", self, pattern) | ||
| if not pattern: | ||
| raise ValueError(f'Unacceptable pattern: {pattern}') | ||
| drv, root, pattern_parts = self._parse_path(pattern) | ||
| if drv or root: | ||
| raise NotImplementedError("Non-relative patterns are unsupported") | ||
| for part in pattern_parts: | ||
| if part != '**' and '**' in part: | ||
| raise ValueError("Invalid pattern: '**' can only be an entire path component") | ||
| pattern = f'**{self.parser.sep}{pattern}' | ||
| selector = _Selector(self, pattern=pattern) | ||
| yield from selector.select() | ||
| def get_presigned_url(self, expire_in: Union[timedelta, int] = 3600) -> str: | ||
@@ -570,32 +590,3 @@ """ | ||
| def _glob(self, pattern): | ||
| """ Glob with new Algorithm that better fit S3 API """ | ||
| sys.audit("pathlib.Path.glob", self, pattern) | ||
| if not pattern: | ||
| raise ValueError(f'Unacceptable pattern: {pattern}') | ||
| drv, root, pattern_parts = self._parse_path(pattern) | ||
| if drv or root: | ||
| raise NotImplementedError("Non-relative patterns are unsupported") | ||
| for part in pattern_parts: | ||
| if part != '**' and '**' in part: | ||
| raise ValueError("Invalid pattern: '**' can only be an entire path component") | ||
| selector = _Selector(self, pattern=pattern) | ||
| yield from selector.select() | ||
| def _rglob(self, pattern): | ||
| """ RGlob with new Algorithm that better fit S3 API """ | ||
| sys.audit("pathlib.Path.rglob", self, pattern) | ||
| if not pattern: | ||
| raise ValueError(f'Unacceptable pattern: {pattern}') | ||
| drv, root, pattern_parts = self._parse_path(pattern) | ||
| if drv or root: | ||
| raise NotImplementedError("Non-relative patterns are unsupported") | ||
| for part in pattern_parts: | ||
| if part != '**' and '**' in part: | ||
| raise ValueError("Invalid pattern: '**' can only be an entire path component") | ||
| pattern = f'**{self._flavour.sep}{pattern}' | ||
| selector = _Selector(self, pattern=pattern) | ||
| yield from selector.select() | ||
| class PureVersionedS3Path(PureS3Path): | ||
@@ -687,3 +678,6 @@ """ | ||
| def __init__(self, *args, version_id): | ||
| super().__init__(*args) | ||
| def _is_wildcard_pattern(pat): | ||
@@ -705,3 +699,3 @@ # Whether this pattern needs actual matching using fnmatch, or can | ||
| for target in self._deep_cached_dir_scan(): | ||
| target = f'{self._path._flavour.sep}{self._path.bucket}{target}' | ||
| target = f'{self._path.parser.sep}{self._path.bucket}{target}' | ||
| if self.match(target): | ||
@@ -713,3 +707,3 @@ yield type(self._path)(target) | ||
| if self._path.key: | ||
| return f'{self._path.key}{self._path._flavour.sep}{pattern}', '' | ||
| return f'{self._path.key}{self._path.parser.sep}{pattern}', '' | ||
| return pattern, '' | ||
@@ -722,3 +716,3 @@ | ||
| break | ||
| prefix += f'{part}{self._path._flavour.sep}' | ||
| prefix += f'{part}{self._path.parser.sep}' | ||
@@ -730,3 +724,3 @@ if pattern.startswith(prefix): | ||
| if key_prefix: | ||
| prefix = self._path._flavour.sep.join((key_prefix, prefix)) | ||
| prefix = self._path.parser.sep.join((key_prefix, prefix)) | ||
| return prefix, pattern | ||
@@ -738,3 +732,3 @@ | ||
| if self._prefix: | ||
| pattern = f'{self._prefix}{self._path._flavour.sep}{pattern}' | ||
| pattern = f'{self._prefix}{self._path.parser.sep}{pattern}' | ||
| *_, pattern_parts = self._path._parse_path(pattern) | ||
@@ -754,6 +748,6 @@ return len(pattern_parts) | ||
| cache = set() | ||
| prefix_sep_count = self._prefix.count(self._path._flavour.sep) | ||
| prefix_sep_count = self._prefix.count(self._path.parser.sep) | ||
| for key in accessor.iter_keys(self._path, prefix=self._prefix, full_keys=self._full_keys): | ||
| key_sep_count = key.count(self._path._flavour.sep) + 1 | ||
| key_parts = key.rsplit(self._path._flavour.sep, maxsplit=key_sep_count - prefix_sep_count) | ||
| key_sep_count = key.count(self._path.parser.sep) + 1 | ||
| key_parts = key.rsplit(self._path.parser.sep, maxsplit=key_sep_count - prefix_sep_count) | ||
| target_path_parts = key_parts[:self._target_level] | ||
@@ -764,3 +758,3 @@ target_path = '' | ||
| continue | ||
| target_path += f'{self._path._flavour.sep}{part}' | ||
| target_path += f'{self._path.parser.sep}{part}' | ||
| if target_path in cache: | ||
@@ -772,3 +766,3 @@ continue | ||
| def _compile_pattern_parts(self, prefix, pattern, bucket): | ||
| pattern = self._path._flavour.sep.join(( | ||
| pattern = self._path.parser.sep.join(( | ||
| '', | ||
@@ -783,12 +777,12 @@ bucket, | ||
| for part in pattern_parts: | ||
| if part == self._path._flavour.sep: | ||
| if part == self._path.parser.sep: | ||
| continue | ||
| if '**' in part: | ||
| new_regex_pattern += f'{self._path._flavour.sep}*(?s:{part.replace("**", ".*")})' | ||
| new_regex_pattern += f'{self._path.parser.sep}*(?s:{part.replace("**", ".*")})' | ||
| continue | ||
| if '*' == part: | ||
| new_regex_pattern += f'{self._path._flavour.sep}(?s:[^/]+)' | ||
| new_regex_pattern += f'{self._path.parser.sep}(?s:[^/]+)' | ||
| continue | ||
| new_regex_pattern += f'{self._path._flavour.sep}{fnmatch.translate(part)[:-2]}' | ||
| new_regex_pattern += f'{self._path.parser.sep}{fnmatch.translate(part)[:-2]}' | ||
| new_regex_pattern += r'/*\Z' | ||
| return re.compile(new_regex_pattern).fullmatch |
+5
-7
@@ -8,3 +8,3 @@ #!/usr/bin/env python | ||
| name='s3path', | ||
| version='0.5.8', | ||
| version='0.6.0', | ||
| url='https://github.com/liormizr/s3path', | ||
@@ -14,10 +14,8 @@ author='Lior Mizrahi', | ||
| packages=['s3path'], | ||
| install_requires=[ | ||
| 'boto3>=1.16.35', | ||
| 'smart-open>=5.1.0', | ||
| ], | ||
| package_data={'s3path': ["py.typed"]}, | ||
| install_requires=['boto3>=1.16.35','smart-open>=5.1.0',], | ||
| license='Apache 2.0', | ||
| long_description=long_description, | ||
| long_description_content_type='text/x-rst', | ||
| python_requires='>=3.8', | ||
| python_requires='>=3.9', | ||
| include_package_data=True, | ||
@@ -31,3 +29,2 @@ classifiers=[ | ||
| 'Programming Language :: Python', | ||
| 'Programming Language :: Python :: 3.8', | ||
| 'Programming Language :: Python :: 3.9', | ||
@@ -37,3 +34,4 @@ 'Programming Language :: Python :: 3.10', | ||
| 'Programming Language :: Python :: 3.12', | ||
| 'Programming Language :: Python :: 3.13', | ||
| ], | ||
| ) |
@@ -168,9 +168,9 @@ import shutil | ||
| def test_glob_old_algo(s3_mock, enable_old_glob): | ||
| test_glob(s3_mock) | ||
| if sys.version_info > (3, 12): | ||
| with pytest.deprecated_call(): | ||
| test_glob(s3_mock) | ||
| else: | ||
| test_glob(s3_mock) | ||
| def test_glob_nested_folders_issue_no_115_old_algo(s3_mock, enable_old_glob): | ||
| test_glob_nested_folders_issue_no_115(s3_mock) | ||
| def test_glob_issue_160(s3_mock): | ||
@@ -249,14 +249,2 @@ s3 = boto3.resource('s3') | ||
| def test_glob_issue_160_old_algo(s3_mock, enable_old_glob): | ||
| test_glob_issue_160(s3_mock) | ||
| def test_glob_issue_160_weird_behavior_old_algo(s3_mock, enable_old_glob): | ||
| test_glob_issue_160_weird_behavior(s3_mock) | ||
| def test_glob_nested_folders_issue_no_179_old_algo(s3_mock, enable_old_glob): | ||
| test_glob_nested_folders_issue_no_179(s3_mock) | ||
| def test_rglob(s3_mock): | ||
@@ -290,4 +278,8 @@ s3 = boto3.resource('s3') | ||
| def test_rglob_new_algo(s3_mock, enable_old_glob): | ||
| test_rglob(s3_mock) | ||
| def test_rglob_old_algo(s3_mock, enable_old_glob): | ||
| if sys.version_info > (3, 12): | ||
| with pytest.deprecated_call(): | ||
| test_rglob(s3_mock) | ||
| else: | ||
| test_rglob(s3_mock) | ||
@@ -319,4 +311,8 @@ | ||
| def test_accessor_scandir_new_algo(s3_mock, enable_old_glob): | ||
| test_accessor_scandir(s3_mock) | ||
| def test_accessor_scandir_old_algo(s3_mock, enable_old_glob): | ||
| if sys.version_info > (3, 12): | ||
| with pytest.deprecated_call(): | ||
| test_accessor_scandir(s3_mock) | ||
| else: | ||
| test_accessor_scandir(s3_mock) | ||
@@ -477,4 +473,3 @@ | ||
| s3_path = S3Path('/test-bucket/docs') | ||
| assert sorted(s3_path.iterdir()) == [ | ||
| S3Path('/test-bucket/docs/Makefile'), | ||
| assert sorted(s3_path.iterdir()) == sorted([ | ||
| S3Path('/test-bucket/docs/_build'), | ||
@@ -486,3 +481,4 @@ S3Path('/test-bucket/docs/_static'), | ||
| S3Path('/test-bucket/docs/make.bat'), | ||
| ] | ||
| S3Path('/test-bucket/docs/Makefile'), | ||
| ]) | ||
@@ -851,2 +847,3 @@ | ||
| def test_absolute(s3_mock): | ||
@@ -853,0 +850,0 @@ s3 = boto3.resource('s3') |
| import os | ||
| import sys | ||
| import pytest | ||
@@ -3,0 +4,0 @@ from pathlib import Path, PurePosixPath, PureWindowsPath |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
20
5.26%175897
-0.08%3355
-0.06%