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.
- s3fs_access_grants-0.2.0/.changeset/README.md +11 -0
- s3fs_access_grants-0.2.0/.changeset/config.json +14 -0
- s3fs_access_grants-0.2.0/.editorconfig +20 -0
- s3fs_access_grants-0.2.0/.github/actions/setup/action.yaml +41 -0
- s3fs_access_grants-0.2.0/.github/workflows/lint-and-test.yaml +71 -0
- s3fs_access_grants-0.2.0/.github/workflows/release.yaml +70 -0
- s3fs_access_grants-0.2.0/.gitignore +30 -0
- s3fs_access_grants-0.2.0/.python-version +1 -0
- s3fs_access_grants-0.2.0/AGENTS.md +157 -0
- s3fs_access_grants-0.2.0/CHANGELOG.md +21 -0
- s3fs_access_grants-0.2.0/CLAUDE.md +1 -0
- s3fs_access_grants-0.2.0/LICENSE +21 -0
- s3fs_access_grants-0.2.0/PKG-INFO +133 -0
- s3fs_access_grants-0.2.0/README.md +108 -0
- s3fs_access_grants-0.2.0/bun.lock +426 -0
- s3fs_access_grants-0.2.0/commitlint.config.cjs +1 -0
- s3fs_access_grants-0.2.0/lefthook.yml +21 -0
- s3fs_access_grants-0.2.0/package.json +23 -0
- s3fs_access_grants-0.2.0/pyproject.toml +128 -0
- s3fs_access_grants-0.2.0/scripts/ci-bump-versions.js +28 -0
- s3fs_access_grants-0.2.0/src/s3fs_access_grants/__init__.py +117 -0
- s3fs_access_grants-0.2.0/src/s3fs_access_grants/filesystem.py +257 -0
- s3fs_access_grants-0.2.0/src/s3fs_access_grants/py.typed +0 -0
- s3fs_access_grants-0.2.0/tests/__init__.py +0 -0
- s3fs_access_grants-0.2.0/tests/conftest.py +12 -0
- s3fs_access_grants-0.2.0/tests/test_about.py +18 -0
- s3fs_access_grants-0.2.0/tests/test_filesystem.py +183 -0
- s3fs_access_grants-0.2.0/tests/test_register.py +63 -0
- 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
|