s3fs 2025.10.0__py3-none-any.whl → 2026.1.0__py3-none-any.whl

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.
s3fs/_version.py CHANGED
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-10-30T11:05:18-0400",
11
+ "date": "2026-01-09T10:29:07-0500",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "2750bf8dacee65710da91b4292e8ad9653edf782",
15
- "version": "2025.10.0"
14
+ "full-revisionid": "a34eac971b397f0874c7843f3251ec7e54c0e810",
15
+ "version": "2026.1.0"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
s3fs/core.py CHANGED
@@ -1,12 +1,11 @@
1
- # -*- coding: utf-8 -*-
2
1
  import asyncio
3
2
  import errno
4
3
  import io
5
4
  import logging
5
+ import math
6
6
  import mimetypes
7
7
  import os
8
8
  import socket
9
- from typing import Tuple, Optional
10
9
  import weakref
11
10
  import re
12
11
 
@@ -69,6 +68,8 @@ S3_RETRYABLE_ERRORS = (
69
68
  ResponseParserError,
70
69
  )
71
70
 
71
+ MAX_UPLOAD_PARTS = 10_000 # maximum number of parts for S3 multipart upload
72
+
72
73
  if ClientPayloadError is not None:
73
74
  S3_RETRYABLE_ERRORS += (ClientPayloadError,)
74
75
 
@@ -166,7 +167,7 @@ def _coalesce_version_id(*args):
166
167
  if len(version_ids) > 1:
167
168
  raise ValueError(
168
169
  "Cannot coalesce version_ids where more than one are defined,"
169
- " {}".format(version_ids)
170
+ f" {version_ids}"
170
171
  )
171
172
  elif len(version_ids) == 0:
172
173
  return None
@@ -174,6 +175,18 @@ def _coalesce_version_id(*args):
174
175
  return version_ids.pop()
175
176
 
176
177
 
178
+ def calculate_chunksize(filesize, chunksize=None, max_parts=MAX_UPLOAD_PARTS) -> int:
179
+ if chunksize is None:
180
+ chunksize = 50 * 2**20 # default chunksize set to 50 MiB
181
+ required_chunks = math.ceil(filesize / chunksize)
182
+ # increase chunksize to fit within the max_parts limit
183
+ if required_chunks > max_parts:
184
+ # S3 supports uploading objects up to 5 TiB in size,
185
+ # so each chunk can be up to ~524 MiB.
186
+ chunksize = math.ceil(filesize / max_parts)
187
+ return chunksize
188
+
189
+
177
190
  class S3FileSystem(AsyncFileSystem):
178
191
  """
179
192
  Access S3 as if it were a file system.
@@ -440,7 +453,7 @@ class S3FileSystem(AsyncFileSystem):
440
453
  s3_key = s3_components[1]
441
454
  return bucket, s3_key
442
455
 
443
- def split_path(self, path) -> Tuple[str, str, Optional[str]]:
456
+ def split_path(self, path) -> tuple[str, str, str | None]:
444
457
  """
445
458
  Normalise S3 path string into bucket and key.
446
459
 
@@ -764,6 +777,7 @@ class S3FileSystem(AsyncFileSystem):
764
777
  else:
765
778
  files.append(c)
766
779
  files += dirs
780
+ files.sort(key=lambda f: f["name"])
767
781
  except ClientError as e:
768
782
  raise translate_boto_error(e)
769
783
 
@@ -887,38 +901,49 @@ class S3FileSystem(AsyncFileSystem):
887
901
  sdirs = set()
888
902
  thisdircache = {}
889
903
  for o in out:
890
- par = self._parent(o["name"])
891
- if par not in sdirs:
892
- sdirs.add(par)
893
- d = False
894
- if len(path) <= len(par):
895
- d = {
896
- "Key": self.split_path(par)[1],
897
- "Size": 0,
898
- "name": par,
899
- "StorageClass": "DIRECTORY",
900
- "type": "directory",
901
- "size": 0,
902
- }
903
- dirs.append(d)
904
- thisdircache[par] = []
905
- ppar = self._parent(par)
906
- if ppar in thisdircache:
907
- if d and d not in thisdircache[ppar]:
908
- thisdircache[ppar].append(d)
909
- if par in sdirs:
910
- thisdircache[par].append(o)
904
+ # not self._parent, because that strips "/" from placeholders
905
+ par = o["name"].rsplit("/", maxsplit=1)[0]
906
+ o["Key"] = o["name"]
907
+ name = o["name"]
908
+ while "/" in par:
909
+ if par not in sdirs:
910
+ sdirs.add(par)
911
+ d = False
912
+ if len(path) <= len(par):
913
+ d = {
914
+ "Key": par,
915
+ "Size": 0,
916
+ "name": par,
917
+ "StorageClass": "DIRECTORY",
918
+ "type": "directory",
919
+ "size": 0,
920
+ }
921
+ dirs.append(d)
922
+ thisdircache[par] = []
923
+ ppar = self._parent(par)
924
+ if ppar in thisdircache:
925
+ if d and d not in thisdircache[ppar]:
926
+ thisdircache[ppar].append(d)
927
+ if par in sdirs and not name.endswith("/"):
928
+ # exclude placeholdees, they do not belong in the directory listing
929
+ thisdircache[par].append(o)
930
+ par, name, o = par.rsplit("/", maxsplit=1)[0], par, d
931
+ if par in thisdircache or par in self.dircache:
932
+ break
911
933
 
912
934
  # Explicitly add directories to their parents in the dircache
913
935
  for d in dirs:
914
936
  par = self._parent(d["name"])
915
- if par in thisdircache:
937
+ # extra condition here (in any()) to deal with directory-marking files
938
+ if par in thisdircache and not any(
939
+ _["name"] == d["name"] for _ in thisdircache[par]
940
+ ):
916
941
  thisdircache[par].append(d)
917
942
 
918
943
  if not prefix:
919
944
  for k, v in thisdircache.items():
920
945
  if k not in self.dircache and len(k) >= len(path):
921
- self.dircache[k] = v
946
+ self.dircache[k] = sorted(v, key=lambda x: x["name"])
922
947
  if withdirs:
923
948
  out = sorted(out + dirs, key=lambda x: x["name"])
924
949
  if detail:
@@ -1043,7 +1068,7 @@ class S3FileSystem(AsyncFileSystem):
1043
1068
  files = await self._lsdir(
1044
1069
  self._parent(path), refresh=refresh, versions=versions
1045
1070
  )
1046
- except IOError:
1071
+ except OSError:
1047
1072
  pass
1048
1073
  files = [
1049
1074
  o
@@ -1230,7 +1255,7 @@ class S3FileSystem(AsyncFileSystem):
1230
1255
  lpath,
1231
1256
  rpath,
1232
1257
  callback=_DEFAULT_CALLBACK,
1233
- chunksize=50 * 2**20,
1258
+ chunksize=None,
1234
1259
  max_concurrency=None,
1235
1260
  mode="overwrite",
1236
1261
  **kwargs,
@@ -1258,6 +1283,7 @@ class S3FileSystem(AsyncFileSystem):
1258
1283
  if content_type is not None:
1259
1284
  kwargs["ContentType"] = content_type
1260
1285
 
1286
+ chunksize = calculate_chunksize(size, chunksize=chunksize)
1261
1287
  with open(lpath, "rb") as f0:
1262
1288
  if size < min(5 * 2**30, 2 * chunksize):
1263
1289
  chunk = f0.read()
@@ -1276,8 +1302,8 @@ class S3FileSystem(AsyncFileSystem):
1276
1302
  key,
1277
1303
  mpu,
1278
1304
  f0,
1305
+ chunksize,
1279
1306
  callback=callback,
1280
- chunksize=chunksize,
1281
1307
  max_concurrency=max_concurrency,
1282
1308
  )
1283
1309
  parts = [
@@ -1305,8 +1331,8 @@ class S3FileSystem(AsyncFileSystem):
1305
1331
  key,
1306
1332
  mpu,
1307
1333
  f0,
1334
+ chunksize,
1308
1335
  callback=_DEFAULT_CALLBACK,
1309
- chunksize=50 * 2**20,
1310
1336
  max_concurrency=None,
1311
1337
  ):
1312
1338
  max_concurrency = max_concurrency or self.max_concurrency
@@ -2140,7 +2166,7 @@ class S3FileSystem(AsyncFileSystem):
2140
2166
  path = self._parent(path)
2141
2167
 
2142
2168
  async def _walk(self, path, maxdepth=None, **kwargs):
2143
- if path in ["", "*"] + ["{}://".format(p) for p in self.protocol]:
2169
+ if path in ["", "*"] + [f"{p}://" for p in self.protocol]:
2144
2170
  raise ValueError("Cannot crawl all of S3")
2145
2171
  async for _ in super()._walk(path, maxdepth=maxdepth, **kwargs):
2146
2172
  yield _
s3fs/errors.py CHANGED
@@ -155,7 +155,7 @@ def translate_boto_error(error, message=None, set_cause=True, *args, **kwargs):
155
155
  custom_exc = constructor(message, *args, **kwargs)
156
156
  else:
157
157
  # No match found, wrap this in an IOError with the appropriate message.
158
- custom_exc = IOError(errno.EIO, message or str(error), *args)
158
+ custom_exc = OSError(errno.EIO, message or str(error), *args)
159
159
 
160
160
  if set_cause:
161
161
  custom_exc.__cause__ = error
s3fs/utils.py CHANGED
@@ -118,7 +118,7 @@ def title_case(string):
118
118
  return "".join(x.capitalize() for x in string.split("_"))
119
119
 
120
120
 
121
- class ParamKwargsHelper(object):
121
+ class ParamKwargsHelper:
122
122
  """
123
123
  Utility class to help extract the subset of keys that an s3 method is
124
124
  actually using
@@ -152,7 +152,7 @@ class ParamKwargsHelper(object):
152
152
  return {k: v for k, v in d.items() if k in valid_keys}
153
153
 
154
154
 
155
- class SSEParams(object):
155
+ class SSEParams:
156
156
  def __init__(
157
157
  self,
158
158
  server_side_encryption=None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: s3fs
3
- Version: 2025.10.0
3
+ Version: 2026.1.0
4
4
  Summary: Convenient Filesystem interface over S3
5
5
  Home-page: http://github.com/fsspec/s3fs/
6
6
  Maintainer: Martin Durant
@@ -11,21 +11,17 @@ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: BSD License
13
13
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
19
- Requires-Python: >= 3.9
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Requires-Python: >= 3.10
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE.txt
22
- Requires-Dist: aiobotocore<3.0.0,>=2.5.4
23
- Requires-Dist: fsspec==2025.10.0
22
+ Requires-Dist: aiobotocore<4.0.0,>=2.5.4
23
+ Requires-Dist: fsspec==2026.1.0
24
24
  Requires-Dist: aiohttp!=4.0.0a0,!=4.0.0a1
25
- Provides-Extra: awscli
26
- Requires-Dist: aiobotocore[awscli]<3.0.0,>=2.5.4; extra == "awscli"
27
- Provides-Extra: boto3
28
- Requires-Dist: aiobotocore[boto3]<3.0.0,>=2.5.4; extra == "boto3"
29
25
  Dynamic: classifier
30
26
  Dynamic: description
31
27
  Dynamic: description-content-type
@@ -35,7 +31,6 @@ Dynamic: license
35
31
  Dynamic: license-file
36
32
  Dynamic: maintainer
37
33
  Dynamic: maintainer-email
38
- Dynamic: provides-extra
39
34
  Dynamic: requires-dist
40
35
  Dynamic: requires-python
41
36
  Dynamic: summary
@@ -0,0 +1,11 @@
1
+ s3fs/__init__.py,sha256=_6_Vs_vblhJSJw-62JVIBIM8kTKLhuwPFAIvt3hanls,160
2
+ s3fs/_version.py,sha256=k4RKi_2-guGjOxzH9C83o-kmchfOPk1mB8g_x-Rkp_g,500
3
+ s3fs/core.py,sha256=XuAkb1H4ACeNNG_hPdL1Gu3oiQZZzeP0wRnE8BihaOY,93419
4
+ s3fs/errors.py,sha256=dhdN1nTI786q6Gy1hqqdfidcFQ5qeYaYwYSUjxYcDxg,7961
5
+ s3fs/mapping.py,sha256=FoqEdMne7LXUL4HgPN4j6WsMsrwxpb53GynDhXs9VRI,237
6
+ s3fs/utils.py,sha256=VvGSdzmgAsqZIOLU7MKtv0oEClm3YZQ78f4PMhBAX9E,5209
7
+ s3fs-2026.1.0.dist-info/licenses/LICENSE.txt,sha256=3DWZ-ma8G8_6I2g4vi-04V-EzkgBJE7sk9kOUVz8WNE,1505
8
+ s3fs-2026.1.0.dist-info/METADATA,sha256=J4u_0HnUjCnx2ShDeIMATNmI8bF0Ww1f-aF6NvR86nU,1157
9
+ s3fs-2026.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ s3fs-2026.1.0.dist-info/top_level.txt,sha256=Lf6EI3TdjlPu7TN-92IIY6c-GdrHnkrKGb1n0iwKooI,5
11
+ s3fs-2026.1.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- s3fs/__init__.py,sha256=_6_Vs_vblhJSJw-62JVIBIM8kTKLhuwPFAIvt3hanls,160
2
- s3fs/_version.py,sha256=ZnHnPiuTSYiTKsfbhGXPQHm4OuVDo_xdpj-D2ga-X0c,501
3
- s3fs/core.py,sha256=xZ7IZTpE4r3c535YG9hjbW24SRYexw4iHzI0NKM86Mc,92058
4
- s3fs/errors.py,sha256=GepxwhJMNrCVKMWVE7WzEfuLMU62pibcD9xlr7RegSg,7961
5
- s3fs/mapping.py,sha256=FoqEdMne7LXUL4HgPN4j6WsMsrwxpb53GynDhXs9VRI,237
6
- s3fs/utils.py,sha256=33lK0sBH7uXTFnbO9gHjONn3RgF555koyVleK7EITlQ,5225
7
- s3fs-2025.10.0.dist-info/licenses/LICENSE.txt,sha256=3DWZ-ma8G8_6I2g4vi-04V-EzkgBJE7sk9kOUVz8WNE,1505
8
- s3fs-2025.10.0.dist-info/METADATA,sha256=sMJAchkouzqYffMZ_4uFiOBfzEcidzUxqHdWylTrFbs,1360
9
- s3fs-2025.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- s3fs-2025.10.0.dist-info/top_level.txt,sha256=Lf6EI3TdjlPu7TN-92IIY6c-GdrHnkrKGb1n0iwKooI,5
11
- s3fs-2025.10.0.dist-info/RECORD,,