s3fs-access-grants 0.2.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 (29) hide show
  1. s3fs_access_grants-0.2.0/.changeset/README.md +11 -0
  2. s3fs_access_grants-0.2.0/.changeset/config.json +14 -0
  3. s3fs_access_grants-0.2.0/.editorconfig +20 -0
  4. s3fs_access_grants-0.2.0/.github/actions/setup/action.yaml +41 -0
  5. s3fs_access_grants-0.2.0/.github/workflows/lint-and-test.yaml +71 -0
  6. s3fs_access_grants-0.2.0/.github/workflows/release.yaml +70 -0
  7. s3fs_access_grants-0.2.0/.gitignore +30 -0
  8. s3fs_access_grants-0.2.0/.python-version +1 -0
  9. s3fs_access_grants-0.2.0/AGENTS.md +157 -0
  10. s3fs_access_grants-0.2.0/CHANGELOG.md +21 -0
  11. s3fs_access_grants-0.2.0/CLAUDE.md +1 -0
  12. s3fs_access_grants-0.2.0/LICENSE +21 -0
  13. s3fs_access_grants-0.2.0/PKG-INFO +133 -0
  14. s3fs_access_grants-0.2.0/README.md +108 -0
  15. s3fs_access_grants-0.2.0/bun.lock +426 -0
  16. s3fs_access_grants-0.2.0/commitlint.config.cjs +1 -0
  17. s3fs_access_grants-0.2.0/lefthook.yml +21 -0
  18. s3fs_access_grants-0.2.0/package.json +23 -0
  19. s3fs_access_grants-0.2.0/pyproject.toml +128 -0
  20. s3fs_access_grants-0.2.0/scripts/ci-bump-versions.js +28 -0
  21. s3fs_access_grants-0.2.0/src/s3fs_access_grants/__init__.py +117 -0
  22. s3fs_access_grants-0.2.0/src/s3fs_access_grants/filesystem.py +257 -0
  23. s3fs_access_grants-0.2.0/src/s3fs_access_grants/py.typed +0 -0
  24. s3fs_access_grants-0.2.0/tests/__init__.py +0 -0
  25. s3fs_access_grants-0.2.0/tests/conftest.py +12 -0
  26. s3fs_access_grants-0.2.0/tests/test_about.py +18 -0
  27. s3fs_access_grants-0.2.0/tests/test_filesystem.py +183 -0
  28. s3fs_access_grants-0.2.0/tests/test_register.py +63 -0
  29. s3fs_access_grants-0.2.0/uv.lock +1130 -0
@@ -0,0 +1,11 @@
1
+ # Changesets
2
+
3
+ This folder is managed by [@changesets/cli](https://github.com/changesets/changesets).
4
+
5
+ It holds `.md` changeset files — each one describes a change that should appear in the changelog and bump the package version. Add a new changeset with:
6
+
7
+ ```bash
8
+ bun run changeset:add
9
+ ```
10
+
11
+ Changeset files are consumed when `changeset version` runs (in CI) — they update `CHANGELOG.md`, bump `package.json` version, sync that version into `pyproject.toml`/`uv.lock`, and are then deleted.
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config@latest/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "fixed": [],
6
+ "linked": [],
7
+ "access": "public",
8
+ "baseBranch": "main",
9
+ "updateInternalDependencies": "patch",
10
+ "privatePackages": {
11
+ "version": true,
12
+ "tag": true
13
+ }
14
+ }
@@ -0,0 +1,20 @@
1
+ root = true
2
+
3
+
4
+ [*]
5
+ indent_style = space
6
+ indent_size = 4
7
+ end_of_line = lf
8
+ charset = utf-8
9
+ trim_trailing_whitespace = true
10
+ insert_final_newline = true
11
+
12
+ [*.{md,markdown}]
13
+ indent_size = 1
14
+ trim_trailing_whitespace = true
15
+
16
+ [*.{yml,yaml,json,toml,js,mjs,cjs,ts}]
17
+ indent_size = 2
18
+
19
+ [Makefile]
20
+ indent_style = tab
@@ -0,0 +1,41 @@
1
+ name: Setup build environment
2
+ description: Setup Bun, Python, install dependencies, and configure caching
3
+
4
+ inputs:
5
+ scope:
6
+ description: '"all" installs everything; "js" skips Python/uv (for jobs that only need JS tooling)'
7
+ required: false
8
+ default: all
9
+
10
+ # Set an empty permissions block to enforce least privilege
11
+ permissions: {}
12
+
13
+ runs:
14
+ using: composite
15
+
16
+ steps:
17
+ - name: Setup Bun
18
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
19
+ with:
20
+ bun-version: 1.3.14
21
+
22
+ - name: Install uv
23
+ if: inputs.scope == 'all'
24
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
25
+ with:
26
+ version: "0.11.23"
27
+ enable-cache: true
28
+
29
+ - name: Install Python
30
+ if: inputs.scope == 'all'
31
+ shell: bash
32
+ run: uv python install
33
+
34
+ - name: Install JS dependencies
35
+ shell: bash
36
+ run: bun install --frozen-lockfile --ignore-scripts
37
+
38
+ - name: Install Python dependencies
39
+ if: inputs.scope == 'all'
40
+ shell: bash
41
+ run: uv sync --frozen
@@ -0,0 +1,71 @@
1
+ name: lint-and-test
2
+
3
+ # Prevent multiple workflows from running simultaneously
4
+ concurrency:
5
+ group: ${{ github.workflow }}-${{ github.ref }}
6
+ cancel-in-progress: true
7
+
8
+ # Set minimal permissions for security
9
+ permissions:
10
+ contents: read
11
+
12
+ on:
13
+ pull_request:
14
+ branches:
15
+ - main
16
+
17
+ jobs:
18
+ lint:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Checkout repository
22
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
23
+
24
+ - name: Setup environment
25
+ uses: ./.github/actions/setup
26
+
27
+ - name: Check code
28
+ run: bun run check
29
+
30
+ test:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - name: Checkout repository
34
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
35
+
36
+ - name: Setup environment
37
+ uses: ./.github/actions/setup
38
+
39
+ - name: Run tests
40
+ run: bun run test
41
+
42
+ build:
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - name: Checkout repository
46
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
47
+
48
+ - name: Setup environment
49
+ uses: ./.github/actions/setup
50
+
51
+ - name: Build wheel and sdist
52
+ run: uv build
53
+
54
+ - name: Check distribution metadata
55
+ run: uvx twine check dist/*
56
+
57
+ check-pr-title:
58
+ runs-on: ubuntu-latest
59
+ steps:
60
+ - name: Checkout repository
61
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
62
+
63
+ - name: Setup environment
64
+ uses: ./.github/actions/setup
65
+ with:
66
+ scope: js
67
+
68
+ - name: Check PR title
69
+ env:
70
+ PR_TITLE: ${{ github.event.pull_request.title }}
71
+ run: echo "$PR_TITLE" | bunx commitlint
@@ -0,0 +1,70 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ concurrency: ${{ github.workflow }}-${{ github.ref }}
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ release:
14
+ runs-on: ubuntu-latest
15
+ # changesets/action needs contents:write (push the version branch) and
16
+ # pull-requests:write (open the "version packages" PR) via the default
17
+ # GITHUB_TOKEN — this works because the repo allows Actions to create PRs.
18
+ permissions:
19
+ contents: write
20
+ pull-requests: write
21
+ outputs:
22
+ published: ${{ steps.changesets.outputs.published }}
23
+ steps:
24
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
25
+
26
+ - uses: ./.github/actions/setup
27
+
28
+ # While changesets are pending, opens/updates the "version packages" PR
29
+ # and does nothing else. Once that PR merges, the changeset files are gone,
30
+ # so this run instead executes `publish` (changeset tag) and reports
31
+ # published=true. NOTE: a routine push to main with no pending changesets
32
+ # ALSO has no changeset files, so it takes the same branch — the publish
33
+ # job below is therefore made idempotent (skip-existing) so it can only
34
+ # ever upload a genuinely new version.
35
+ - id: changesets
36
+ uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
37
+ with:
38
+ version: node scripts/ci-bump-versions.js
39
+ publish: bunx changeset tag
40
+ title: "chore: version packages"
41
+ commit: "chore: version packages"
42
+ env:
43
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
+
45
+ publish:
46
+ needs: release
47
+ if: needs.release.outputs.published == 'true'
48
+ runs-on: ubuntu-latest
49
+ # id-token:write is for PyPI trusted publishing (OIDC); scoped to this job
50
+ # so the changesets job above runs with no token-minting permission.
51
+ permissions:
52
+ contents: read
53
+ id-token: write
54
+ environment: pypi
55
+ steps:
56
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
57
+
58
+ - uses: ./.github/actions/setup
59
+
60
+ - name: Build distribution
61
+ run: uv build
62
+
63
+ # Trusted publishing (OIDC): no API token. Configure a PyPI publisher
64
+ # for this repo + the "pypi" environment first.
65
+ # skip-existing makes this a no-op when the version is already on PyPI, so
66
+ # only a real version bump (from a merged version-packages PR) publishes.
67
+ - name: Publish to PyPI
68
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
69
+ with:
70
+ skip-existing: true
@@ -0,0 +1,30 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Test / coverage artifacts
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ .ty_cache/
16
+ .coverage
17
+ coverage.xml
18
+ htmlcov/
19
+ test-results.xml
20
+ test-report.html
21
+
22
+ # Node / changesets tooling
23
+ node_modules/
24
+
25
+ # OS / editor
26
+ .DS_Store
27
+
28
+ # direnv (local env pinning; may resolve secrets)
29
+ .envrc
30
+ .direnv/
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,157 @@
1
+ # AGENTS.md
2
+
3
+ > Instructions for AI coding assistants working on this project.
4
+ >
5
+ > **Note**: `CLAUDE.md` is a symlink to this file. Edit `AGENTS.md`; both reflect the change.
6
+
7
+ ## Purpose
8
+
9
+ `s3fs-access-grants` is a published library that makes [S3 Access Grants](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-grants.html)
10
+ transparent to [fsspec](https://filesystem-spec.readthedocs.io/) / [s3fs](https://s3fs.readthedocs.io/).
11
+ `register()` installs `ScopedS3FileSystem` for `s3://`; every fsspec consumer
12
+ (pandas, polars, universal-pathlib, `fsspec.open`) then routes each call through
13
+ a credential scoped to the matching grant.
14
+
15
+ All code lives in `src/s3fs_access_grants/`:
16
+
17
+ - `__init__.py` — public API. Exposes exactly three names: `register()` (the only
18
+ entry point — resolves account/region via the private `_resolve_account_id` /
19
+ `_resolve_region`, then registers the handler bound to those values via
20
+ `functools.partial`; returns `None`), `ScopedS3FileSystem` (for advanced/manual
21
+ construction; requires `grants_account_id`/`grants_region`), and
22
+ `CrossScopeCopyError`. **No AWS I/O on import** — registration is explicit.
23
+ - `filesystem.py` — the filesystem itself: `ScopedS3FileSystem` (the s3fs
24
+ subclass), the `enumerate_scopes` helper (module-internal: called by both
25
+ `register()` and the FS constructor; not in `__all__`), longest-prefix routing,
26
+ per-scope auto-refreshing clients, and the cross-scope copy guard. There are no
27
+ override globals: resolved values are bound into the handler at registration
28
+ time, not stashed.
29
+
30
+ Imports are always absolute (`from s3fs_access_grants.filesystem import ...`),
31
+ never relative, and import individual names, not the module.
32
+
33
+ See [README.md](./README.md) for end-to-end behaviour.
34
+
35
+ ## Tech stack
36
+
37
+ - Python 3.11+ (dev interpreter pinned to 3.14 via `.python-version`)
38
+ - uv (package + venv management), bun (task runner + changesets/lefthook tooling)
39
+ - boto3 / aiobotocore (AWS), s3fs + fsspec (filesystem layer)
40
+ - ruff (lint + format), ty (type check), pytest (test)
41
+ - hatchling (build backend), changesets (release/version management)
42
+
43
+ ## Essential commands
44
+
45
+ Use `bun` commands, not raw tool invocations:
46
+
47
+ ```bash
48
+ bun install # uv sync + lefthook install
49
+ bun run check # lefthook pre-commit on all files (ruff, ty, editorconfig)
50
+ bun run test # pytest with inline coverage (term-missing)
51
+ bun run build # uv build (wheel + sdist)
52
+ uv run pytest tests/test_filesystem.py # single test file
53
+ uv build && uvx twine check dist/* # verify the package is publishable
54
+ ```
55
+
56
+ `bun run check` runs lefthook's `pre-commit`, whose per-tool globs operate on
57
+ `{staged_files}`. **Stage your changes (`git add -A`) before running it** — new,
58
+ unstaged files are invisible to ruff/ty/editorconfig and produce a false-green
59
+ that pre-commit will reject at commit time.
60
+
61
+ ## Development workflow
62
+
63
+ 1. **Read** the existing code in `filesystem.py` before changing it. Match the
64
+ style you find there.
65
+ 2. **Implement** the smallest change that does the job. No abstractions for
66
+ hypothetical future needs.
67
+ 3. **Test** — add/adjust tests, run `bun run test`.
68
+ 4. **Verify** — `git add -A`, then `bun run check` → `bun run test`. Fix every
69
+ issue.
70
+ 5. **Changeset** — for any user-facing change, `bun run changeset:add` (`patch`
71
+ for fixes, `minor` for features, `major` for breaking). The summary becomes
72
+ the CHANGELOG entry. Pure chore/CI changes skip this.
73
+
74
+ ## Python conventions
75
+
76
+ ### Style and typing
77
+
78
+ - Modern syntax only: `str | None` not `Optional`; `list[str]`/`dict[str, X]`
79
+ not `List`/`Dict`; f-strings not `%`/`.format()`.
80
+ - Annotate every function's parameters and return type. Return concrete types
81
+ (`list[X]`), accept abstract ones (`Sequence[X]`) only where it broadens usable
82
+ input.
83
+ - Module-level logger: `logger = logging.getLogger(__name__)`. Use `%s`/`%r`
84
+ formatting in log calls, not f-strings, so formatting is deferred.
85
+ - Google-style docstrings on public functions/classes/methods. First line is an
86
+ imperative summary of *what*, not *how*. Skip `Args`/`Returns` that merely echo
87
+ the signature. `_`-prefixed helpers get a one-liner at most. No docstrings in
88
+ test bodies (file-level only).
89
+ - Imports at the top of the file, always.
90
+
91
+ ### Error handling
92
+
93
+ - Hard-fail: raise, don't return error dicts or result objects.
94
+ - Catch specific exception types, not bare `except`. Use `raise ... from e` to
95
+ preserve the chain. Broad suppression is confined to one place: the
96
+ best-effort teardown in `_close_scopes` uses `contextlib.suppress(Exception)`
97
+ so a failing client close can't abort the rest of the cleanup.
98
+
99
+ ### Tooling compliance
100
+
101
+ - `ruff` and `ty` must both pass with zero warnings before commit.
102
+
103
+ ### Documented exceptions (this is a library on top of s3fs internals)
104
+
105
+ The general rule is **no suppressions and no `Any`** — fix the underlying issue.
106
+ This project has a few unavoidable, deliberate exceptions, each carrying an
107
+ inline comment explaining why. Do not remove them, and match the pattern if you
108
+ add genuinely analogous code:
109
+
110
+ - **`# noqa: SLF001`** on the calls into `S3FileSystem._call_s3` / `_iterdir` and
111
+ on `session._credentials`. The entire design is driving s3fs/aiobotocore
112
+ internals; the docstring at the top of `filesystem.py` documents the seam.
113
+ - **`# ty: ignore[invalid-argument-type]`** on the `_ClientOverride(...)`
114
+ arguments. The proxy is a structural stand-in for the filesystem; ty can't see
115
+ the duck-typing.
116
+ - **`Any`** for the per-scope client dict — aiobotocore clients are untyped.
117
+ - **`_SCOPE_CACHE`** — a module-level dict caching the grant routing table per
118
+ `(account, region)`. It's a process-wide perf cache (enumeration is
119
+ identity-global), not state-passing, and the one place module-level mutable
120
+ state is allowed. `register()` binds account/region into the handler via
121
+ `functools.partial`, so there are deliberately **no** override globals.
122
+
123
+ Anything beyond these requires a comment justifying it. Don't reach for a new
124
+ suppression to make a check pass — fix the code first.
125
+
126
+ ### Compatibility seam
127
+
128
+ `ScopedS3FileSystem` depends on `s3fs` internals (`_call_s3` and `_iterdir`
129
+ resolving their client via `get_s3()`). The module docstring records the exact
130
+ versions it was validated against. **On any `s3fs` bump, re-verify that seam**
131
+ and update the docstring + the README compatibility note.
132
+
133
+ ## Testing
134
+
135
+ - Mirror `src/` layout under `tests/`.
136
+ - Prefer inline data and pure-logic tests. The routing logic (`_parse_grant`,
137
+ `_scope_for`, longest-prefix ordering) is testable without AWS — keep it that
138
+ way; use a `SimpleNamespace` stand-in rather than constructing a real
139
+ filesystem (which makes live AWS calls).
140
+ - Mock external services (boto3/aiobotocore); never hit real AWS in tests.
141
+ - Naming: `test_{behavior}` inside `class Test{Unit}:`. Plain `assert`,
142
+ `pytest.raises` for exceptions. One behavior per test. `_make_{object}()` for
143
+ builders.
144
+
145
+ ## Non-negotiable rules
146
+
147
+ - **Use `bun` commands** for check/test/build — not raw tool invocations.
148
+ - **Conventional commits** (commitlint-enforced): `feat:`, `fix:`, `docs:`,
149
+ `refactor:`, `test:`, `chore:`.
150
+ - **No secrets committed**: never commit `.env`, API keys, AWS credentials, or
151
+ account-specific connection strings.
152
+ - **No AI authorship anywhere**: in commit messages, PRs, or any text, never
153
+ refer to yourself as an assistant / Claude / AI, and never add "generated by"
154
+ or "co-authored by AI" lines. Write as a human developer would.
155
+ - **Releases go through changesets**: never hand-edit the version in
156
+ `pyproject.toml` / `package.json`. CI bumps it from changeset files via
157
+ `scripts/ci-bump-versions.js` and publishes to PyPI.
@@ -0,0 +1,21 @@
1
+ # s3fs-access-grants
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f497575: First version: transparent S3 Access Grants for fsspec/s3fs. Call `register()`
8
+ once and every fsspec consumer (pandas, polars, universal-pathlib, `fsspec.open`)
9
+ routes each `s3://` call through a credential scoped to the matching grant, with
10
+ no Access Grants logic in application code.
11
+
12
+ ### Patch Changes
13
+
14
+ - bfa95f3: Widen dependency bounds: `s3fs`/`fsspec` to `>=2026,<2027`, `boto3` to
15
+ `>=1.34,<2`. Add `aiobotocore>=2.22,<4` as a direct dependency (it is imported
16
+ directly) and drop the unused `aioboto3`.
17
+ - 72648d6: Fix `register()` breaking URL-based reads (e.g. `pandas.read_json("s3://...")`).
18
+ It registered a `functools.partial`, but fsspec resolves `s3://` URLs by calling
19
+ classmethods (`_get_kwargs_from_urls`, `_strip_protocol`, ...) on the registered
20
+ object, which a partial lacks. Register a dynamically-created `ScopedS3FileSystem`
21
+ subclass instead, with the resolved account/region as class-level defaults.
@@ -0,0 +1 @@
1
+ AGENTS.md
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Josep Arús Pous
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: s3fs-access-grants
3
+ Version: 0.2.0
4
+ Summary: Transparent S3 Access Grants for fsspec/s3fs — per-grant scoped credentials, no Access Grants logic in application code.
5
+ Project-URL: repository, https://github.com/undeadpixel/s3fs-access-grants
6
+ Author: Josep Arus Pous
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: access-grants,aws,fsspec,s3,s3control,s3fs
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: System :: Filesystems
18
+ Classifier: Typing :: Typed
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: aiobotocore<4,>=2.22
21
+ Requires-Dist: boto3<2,>=1.34
22
+ Requires-Dist: fsspec<2027,>=2026
23
+ Requires-Dist: s3fs<2027,>=2026
24
+ Description-Content-Type: text/markdown
25
+
26
+ # s3fs-access-grants
27
+
28
+ Transparent [S3 Access Grants](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-grants.html)
29
+ for [fsspec](https://filesystem-spec.readthedocs.io/) / [s3fs](https://s3fs.readthedocs.io/).
30
+
31
+ Register the implementation for `s3://` once and every fsspec consumer —
32
+ `pandas`, `polars`, `universal-pathlib`, `fsspec.open`, ... — routes each call
33
+ through a credential scoped to the matching grant. No Access Grants logic leaks
34
+ into application code.
35
+
36
+ ## How it works
37
+
38
+ `ScopedS3FileSystem` subclasses `s3fs.S3FileSystem`. On construction it
39
+ enumerates the caller's grants (`ListCallerAccessGrants`) and builds a
40
+ longest-prefix routing table. Each S3 operation is matched to a grant scope and
41
+ served by a per-scope `aiobotocore` client whose credentials come from
42
+ `GetDataAccess` and auto-refresh on expiry. Calls that match no grant fall
43
+ through to the default IAM client (fail-closed), and cross-scope copies are
44
+ rejected.
45
+
46
+ If the caller has no grants (or no permission to list them), `register()`
47
+ installs nothing and plain `s3fs` handles `s3://` with zero added overhead.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install s3fs-access-grants
53
+ # or
54
+ uv add s3fs-access-grants
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ Registration is explicit — importing the package does no AWS I/O.
60
+
61
+ ```python
62
+ import s3fs_access_grants
63
+
64
+ # Install the s3:// handler for the calling identity's grants.
65
+ s3fs_access_grants.register()
66
+
67
+ import polars as pl
68
+ df = pl.read_parquet("s3://bucket/teamA/data.parquet") # scoped automatically
69
+ ```
70
+
71
+ Pass the grants-instance account/region explicitly (e.g. in a notebook, or when
72
+ the grants instance lives in a different account than your role):
73
+
74
+ ```python
75
+ import s3fs_access_grants
76
+
77
+ s3fs_access_grants.register(account_id="767546672094", region="eu-west-1")
78
+ ```
79
+
80
+ `register()` is the only entry point and returns nothing — it's a one-time setup
81
+ call. It resolves the account/region, then binds them into the handler it
82
+ registers, so a later argless `fsspec.filesystem("s3")` (what pandas / polars use
83
+ under the hood) builds the filesystem with the right values. After calling it,
84
+ just use fsspec / pandas / polars as normal.
85
+
86
+ ### Configuration
87
+
88
+ `register()` resolves the grants-instance account and region in this order:
89
+
90
+ | Setting | Resolution order |
91
+ | ---------- | --------------------------------------------------------------- |
92
+ | Account ID | explicit arg → `S3FS_ACCESS_GRANTS_ACCOUNT_ID` → STS caller |
93
+ | Region | explicit arg → `S3FS_ACCESS_GRANTS_REGION` → session default |
94
+
95
+ The grants instance may live in a different account than the caller's role, so
96
+ the explicit arg / env is authoritative; the STS caller account is only a
97
+ same-account fallback.
98
+
99
+ ### Advanced: manual construction
100
+
101
+ `register()` covers the common case. To build a scoped filesystem by hand —
102
+ e.g. pointing at a different grants account without touching the global s3://
103
+ registration — construct `ScopedS3FileSystem` directly:
104
+
105
+ ```python
106
+ from s3fs_access_grants import ScopedS3FileSystem
107
+
108
+ fs = ScopedS3FileSystem(grants_account_id="767546672094", grants_region="eu-west-1")
109
+ ```
110
+
111
+ ## Compatibility
112
+
113
+ This subclass depends on `s3fs` internals (`_call_s3` and `_iterdir` resolving
114
+ their client via `get_s3()`). Validated against s3fs 2026.6.0 / aiobotocore
115
+ 2.25.1. Re-check on any `s3fs` major bump.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ bun install # installs JS tooling + runs uv sync + lefthook install
121
+ bun run check # lint + typecheck (ruff, ty, editorconfig)
122
+ bun run test # pytest with coverage
123
+ bun run build # uv build (wheel + sdist)
124
+ ```
125
+
126
+ Releases are managed with [changesets](https://github.com/changesets/changesets):
127
+ add one with `bun run changeset:add`, and merging the generated "version
128
+ packages" PR bumps the version, syncs it into `pyproject.toml`, and publishes to
129
+ PyPI.
130
+
131
+ ## License
132
+
133
+ MIT