|
| 1 | +(filesystem)= |
| 2 | + |
| 3 | +# S3 filesystem |
| 4 | + |
| 5 | +PyAthena ships its own [fsspec](https://filesystem-spec.readthedocs.io/en/latest/)-compatible |
| 6 | +filesystem implementation for Amazon S3 (`S3FileSystem`), instead of depending on |
| 7 | +[s3fs](https://github.com/fsspec/s3fs). This avoids the dependency and version-conflict |
| 8 | +problems between s3fs/aiobotocore and boto3/botocore, while providing an API surface |
| 9 | +compatible with s3fs for users migrating from it. |
| 10 | + |
| 11 | +The filesystem is used internally by the pandas/polars result sets to read query results |
| 12 | +from S3, and can also be used independently for S3 file operations. |
| 13 | + |
| 14 | +## fsspec registration |
| 15 | + |
| 16 | +Importing `pyathena.pandas` or `pyathena.polars` registers `S3FileSystem` as the fsspec |
| 17 | +`s3` / `s3a` protocols via `pyathena.filesystem.register_s3_filesystem`. This replaces |
| 18 | +fsspec's default lazy mapping of the `s3` protocol to s3fs, which means |
| 19 | +`fsspec.filesystem("s3")` returns PyAthena's implementation and s3fs-specific settings |
| 20 | +(such as the `S3FS_LOGGING_LEVEL` environment variable) have no effect. |
| 21 | + |
| 22 | +A filesystem class that has already been registered explicitly is also overwritten, |
| 23 | +with a warning log, so that the replacement is diagnosable. To restore another |
| 24 | +implementation, re-register it afterwards: |
| 25 | + |
| 26 | +```python |
| 27 | +import fsspec |
| 28 | +import s3fs |
| 29 | + |
| 30 | +import pyathena.pandas # Registers PyAthena's S3FileSystem. |
| 31 | + |
| 32 | +fsspec.register_implementation("s3", s3fs.S3FileSystem, clobber=True) |
| 33 | +``` |
| 34 | + |
| 35 | +## Basic usage |
| 36 | + |
| 37 | +The filesystem can be constructed from a PyAthena connection, or directly with |
| 38 | +s3fs-compatible credential arguments: |
| 39 | + |
| 40 | +```python |
| 41 | +from pyathena import connect |
| 42 | +from pyathena.filesystem.s3 import S3FileSystem |
| 43 | + |
| 44 | +fs = S3FileSystem(connect(region_name="us-west-2")) |
| 45 | + |
| 46 | +# Or with direct credentials (s3fs-compatible arguments). |
| 47 | +fs = S3FileSystem(key="YOUR_ACCESS_KEY", secret="YOUR_SECRET_KEY") |
| 48 | + |
| 49 | +# Or anonymously for public buckets. |
| 50 | +fs = S3FileSystem(anon=True) |
| 51 | +``` |
| 52 | + |
| 53 | +Standard fsspec operations work as expected: |
| 54 | + |
| 55 | +```python |
| 56 | +fs.ls("s3://YOUR_S3_BUCKET/path/to/") |
| 57 | +fs.find("s3://YOUR_S3_BUCKET/path/to/") |
| 58 | +fs.exists("s3://YOUR_S3_BUCKET/path/to/object") |
| 59 | +fs.info("s3://YOUR_S3_BUCKET/path/to/object") |
| 60 | + |
| 61 | +with fs.open("s3://YOUR_S3_BUCKET/path/to/object", "rb") as f: |
| 62 | + data = f.read() |
| 63 | + |
| 64 | +fs.pipe("s3://YOUR_S3_BUCKET/path/to/object", b"data") |
| 65 | +fs.cat("s3://YOUR_S3_BUCKET/path/to/object") |
| 66 | +fs.cp("s3://YOUR_S3_BUCKET/src", "s3://YOUR_S3_BUCKET/dst") |
| 67 | +fs.rm("s3://YOUR_S3_BUCKET/path/to/", recursive=True) |
| 68 | +``` |
| 69 | + |
| 70 | +Writes with `pipe`/`pipe_file` issue a single PutObject request for data up to the |
| 71 | +block size (5 MiB by default) and switch to a parallel multipart upload for larger |
| 72 | +data. Inside an [fsspec transaction](https://filesystem-spec.readthedocs.io/en/latest/features.html#transactions), |
| 73 | +writes are deferred until the transaction commits and are discarded on rollback. |
| 74 | + |
| 75 | +## Error translation |
| 76 | + |
| 77 | +S3 error responses are translated into standard Python exceptions, so filesystem |
| 78 | +operations raise natural errors instead of botocore's `ClientError`: |
| 79 | + |
| 80 | +| S3 error | Python exception | |
| 81 | +| --- | --- | |
| 82 | +| `404` / `NoSuchKey` / `NoSuchBucket` | `FileNotFoundError` | |
| 83 | +| `403` / `AccessDenied` | `PermissionError` | |
| 84 | +| `BucketAlreadyExists` / `BucketAlreadyOwnedByYou` | `FileExistsError` | |
| 85 | +| `RequestTimeout` | `TimeoutError` | |
| 86 | +| Others | `OSError` with the matching `errno` | |
| 87 | + |
| 88 | +## Object metadata, tags, and ACLs |
| 89 | + |
| 90 | +```python |
| 91 | +# User-defined metadata (x-amz-meta-*). |
| 92 | +fs.setxattr("s3://YOUR_S3_BUCKET/path/to/object", attr1="value1") |
| 93 | +metadata = fs.metadata("s3://YOUR_S3_BUCKET/path/to/object") |
| 94 | +metadata["attr1"] # User-defined metadata via the mapping interface. |
| 95 | +metadata.content_type # System-defined metadata as typed properties. |
| 96 | +fs.getxattr("s3://YOUR_S3_BUCKET/path/to/object", "attr1") |
| 97 | + |
| 98 | +# Object tagging. |
| 99 | +fs.put_tags("s3://YOUR_S3_BUCKET/path/to/object", {"tag1": "value1"}) |
| 100 | +fs.put_tags("s3://YOUR_S3_BUCKET/path/to/object", {"tag2": "value2"}, mode="m") # Merge. |
| 101 | +fs.get_tags("s3://YOUR_S3_BUCKET/path/to/object") |
| 102 | + |
| 103 | +# Canned ACLs. |
| 104 | +fs.chmod("s3://YOUR_S3_BUCKET/path/to/object", "bucket-owner-full-control") |
| 105 | +fs.chmod("s3://YOUR_S3_BUCKET/path/to/", "private", recursive=True) |
| 106 | +``` |
| 107 | + |
| 108 | +Note that `setxattr` rewrites the object by copying it onto itself (S3 does not allow |
| 109 | +updating the metadata of an existing object in place), which updates its last-modified |
| 110 | +time. |
| 111 | + |
| 112 | +## Multipart upload management |
| 113 | + |
| 114 | +Incomplete multipart uploads continue to accrue storage costs until they are completed |
| 115 | +or aborted. The filesystem can discover and abort them: |
| 116 | + |
| 117 | +```python |
| 118 | +uploads = fs.list_multipart_uploads("s3://YOUR_S3_BUCKET") |
| 119 | +for upload in uploads: |
| 120 | + print(upload.key, upload.upload_id, upload.initiated) |
| 121 | + |
| 122 | +# Abort all incomplete uploads under a bucket or key prefix. |
| 123 | +fs.clear_multipart_uploads("s3://YOUR_S3_BUCKET/path/to/") |
| 124 | +``` |
| 125 | + |
| 126 | +## Versioning |
| 127 | + |
| 128 | +With `version_aware=True`, reads pin the object version observed at open time, so a |
| 129 | +file handle keeps returning consistent data even if the object is overwritten while |
| 130 | +reading. Explicit versions can always be read with the `?versionId=` suffix or the |
| 131 | +`version_id` argument. |
| 132 | + |
| 133 | +```python |
| 134 | +fs = S3FileSystem(connect(region_name="us-west-2"), version_aware=True) |
| 135 | + |
| 136 | +with fs.open("s3://YOUR_S3_BUCKET/path/to/object", "rb") as f: |
| 137 | + data = f.read() # Pinned to the version observed at open time. |
| 138 | + |
| 139 | +# List all versions of the objects under a prefix. |
| 140 | +fs.ls("s3://YOUR_S3_BUCKET/path/to/", versions=True, detail=True) |
| 141 | + |
| 142 | +# Typed version information, including delete markers if requested. |
| 143 | +versions = fs.object_version_info("s3://YOUR_S3_BUCKET/path/to/object") |
| 144 | +for version in versions: |
| 145 | + print(version.version_id, version.is_latest, version.last_modified) |
| 146 | +``` |
| 147 | + |
| 148 | +Version-aware operations require the `s3:GetObjectVersion` and |
| 149 | +`s3:ListBucketVersions` permissions. |
| 150 | + |
| 151 | +## Bucket lifecycle |
| 152 | + |
| 153 | +Bucket creation and deletion are infrastructure-level changes and are disabled by |
| 154 | +default: `mkdir`/`makedirs` and `rmdir` raise `PermissionError` when they would |
| 155 | +create or delete a bucket. Pass the opt-in flags to enable them: |
| 156 | + |
| 157 | +```python |
| 158 | +fs = S3FileSystem( |
| 159 | + connect(region_name="us-west-2"), |
| 160 | + allow_bucket_creation=True, |
| 161 | + allow_bucket_deletion=True, |
| 162 | +) |
| 163 | +fs.mkdir("s3://YOUR_NEW_BUCKET") |
| 164 | +fs.rmdir("s3://YOUR_NEW_BUCKET") # The bucket must be empty. |
| 165 | +``` |
| 166 | + |
| 167 | +Creating a key prefix under an existing bucket requires no operation (S3 has no real |
| 168 | +directories below the bucket level) and is always a no-op. |
| 169 | + |
| 170 | +## Async filesystem |
| 171 | + |
| 172 | +`AioS3FileSystem` provides the same functionality on top of fsspec's |
| 173 | +`AsyncFileSystem`, dispatching parallel operations through the asyncio event loop. |
| 174 | +See {ref}`aio-s3-filesystem` for details. |
0 commit comments