You're Invited:Meet the Socket Team at RSAC and BSidesSF 2026, March 23–26.RSVP
Socket
Book a DemoSign in
Socket

s3path

Package Overview
Dependencies
Maintainers
1
Versions
43
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

s3path - pypi Package Compare versions

Comparing version
0.5.8
to
0.6.0
+1
s3path/py.typed
# 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

@@ -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