sheetdiff 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Umer
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,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: sheetdiff
3
+ Version: 0.1.0
4
+ Summary: Concise spreadsheet diff utility (CSV/XLSX) with an optional GitHub/VS Code-style web UI
5
+ Author-email: Muhammad Umer <muhammadumer0266@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Muhammad Umer
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/muhammadumer0266/xl-diff
29
+ Project-URL: Repository, https://github.com/muhammadumer0266/xl-diff
30
+ Project-URL: Issues, https://github.com/muhammadumer0266/xl-diff/issues
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3 :: Only
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Topic :: Office/Business
36
+ Classifier: Topic :: Utilities
37
+ Requires-Python: >=3.10
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: pandas>=1.5
41
+ Requires-Dist: openpyxl>=3.0
42
+ Provides-Extra: web
43
+ Requires-Dist: flask>=2.0; extra == "web"
44
+ Provides-Extra: remote
45
+ Requires-Dist: fsspec>=2023.1; extra == "remote"
46
+ Requires-Dist: s3fs>=2023.1; extra == "remote"
47
+ Requires-Dist: adlfs>=2023.1; extra == "remote"
48
+ Provides-Extra: all
49
+ Requires-Dist: sheetdiff[remote,web]; extra == "all"
50
+ Dynamic: license-file
51
+
52
+
53
+ # xl-diff (sheetdiff)
54
+
55
+ Minimal, efficient spreadsheet diff tool. Load CSV/XLSX and produce unified +/- row and cell diffs.
56
+
57
+ ## Run locally from a clone (web UI)
58
+
59
+ If you just want to clone the repo and use the web UI on your own machine, without publishing to PyPI:
60
+
61
+ ```bash
62
+ git clone https://github.com/muhammadumer0266/xl-diff.git
63
+ cd xl-diff
64
+
65
+ # Create and activate a virtualenv
66
+ python -m venv .venv
67
+ # macOS/Linux:
68
+ source .venv/bin/activate
69
+ # Windows (PowerShell):
70
+ .venv\Scripts\Activate.ps1
71
+
72
+ # Install the package in editable mode with the web extra
73
+ pip install -e ".[web]"
74
+
75
+ # Start the web server
76
+ xldiff-web
77
+ ```
78
+
79
+ Then open http://localhost:5000, upload two spreadsheets (optionally a key column), and view a GitHub-style row/cell diff (green = added, red = removed, yellow = modified with old value struck through above the new one).
80
+
81
+ The pure-Python (pandas-based) diff engine works out of the box after `pip install -e ".[web]"`. If you also want the faster Rust-backed engine, additionally run `maturin develop --release` (see "Building the Rust extension" below) before starting `xldiff-web`.
82
+
83
+ ## Install from PyPI (distributed as `sheetdiff`)
84
+
85
+ ```
86
+ pip install sheetdiff
87
+ ```
88
+
89
+ Quick CLI
90
+
91
+ ```
92
+ python -m sheetdiff.cli left.xlsx right.xlsx --key id
93
+ ```
94
+
95
+ Web UI (GitHub-style diff viewer)
96
+
97
+ ```
98
+ pip install sheetdiff[web]
99
+ xldiff-web
100
+ ```
101
+
102
+ Open http://localhost:5000, upload two spreadsheets (optionally a key column), and view a GitHub-style row/cell diff (green = added, red = removed, yellow = modified with old value struck through above the new one).
103
+
104
+ Remote storage (S3, MinIO, Cloudflare R2, Garage, Azure Blob)
105
+
106
+ ```
107
+ pip install sheetdiff[remote]
108
+ sheetdiff s3://my-bucket/left.xlsx s3://my-bucket/right.xlsx --key id
109
+ ```
110
+
111
+ Files can also be referenced by URI in the web UI (each row accepts an upload *or* a URI) and from the Python API via `sheetdiff.resolve_source`. Credentials and per-backend endpoints are configured once via environment variables, never through the web form — see [docs/REMOTE_STORAGE.md](docs/REMOTE_STORAGE.md) for the full list, plus how to configure upload/remote size limits (`SHEETDIFF_MAX_UPLOAD_MB`, `SHEETDIFF_MAX_REMOTE_MB`, `SHEETDIFF_MAX_FILES`).
112
+
113
+ Python API
114
+
115
+ ```
116
+ from sheetdiff.core import diff_sheets, format_unified
117
+
118
+ changes = diff_sheets('left.csv','right.csv', key='id')
119
+ print(format_unified(changes))
120
+ ```
121
+
122
+ Functions
123
+
124
+ - `diff_sheets(left, right, key=None, sheet_name=None)` — compare two files; if `key` provided, rows are matched by that column, otherwise by position. Returns list of tuples `(type, row_key_or_index, column, old, new)`.
125
+ - `format_unified(changes)` — render a compact unified +/- text view.
126
+
127
+ Integration notes
128
+
129
+ - For web backends (Django/Celery), call `diff_sheets` or `compare_workbooks` inside a worker and serialize `changes` to JSON for paginated display.
130
+ - The package will prefer a Rust-backed binary extension `xl_diff` when available (faster, memory-efficient). If the Rust extension is not installed, the pure-Python fallback (`pandas` based) is used.
131
+ - For very large files implement the Rust core (see `RUST_INTEGRATION.md`) and build wheels via `maturin` for best performance.
132
+
133
+ # xl-diff
134
+
135
+ [![CI](https://github.com/muhammadumer0266/xl-diff/actions/workflows/CI.yml/badge.svg)](https://github.com/muhammadumer0266/xl-diff/actions/workflows/CI.yml)
136
+ [![crates.io](https://img.shields.io/crates/v/xl_diff.svg)](https://crates.io/crates/xl_diff)
137
+ [![PyPI](https://img.shields.io/pypi/v/sheetdiff.svg)](https://pypi.org/project/sheetdiff/)
138
+ [![Language](https://img.shields.io/badge/Language-Rust%20%2F%20Python-orange.svg)](https://www.rust-lang.org/)
139
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
140
+
141
+ xl-diff is a high-performance, memory-safe Excel comparison engine written in Rust and exposed as a Python extension via PyO3. It is designed to integrate into Django/Celery pipelines and produce cell-level semantic diffs between spreadsheet versions.
142
+
143
+ Features
144
+ - Fast, zero-copy reading of XLSX using `calamine`.
145
+ - Parallel diff computation using `rayon`.
146
+ - Row alignment using an optional key column or positional fallback.
147
+ - Python bindings (PyO3) for seamless integration.
148
+ - Cross-platform packaging with `maturin` and GitHub Actions.
149
+
150
+ ## Building the Rust extension
151
+
152
+ The package prefers a Rust-backed binary extension (`xl_diff`, faster and more memory-efficient) when available, falling back to the pure-Python implementation otherwise. To build it in-place inside your virtualenv:
153
+
154
+ ```bash
155
+ pip install maturin
156
+ maturin develop --release
157
+ ```
158
+
159
+ Python usage
160
+
161
+ ```python
162
+ import xl_diff
163
+
164
+ # List sheets
165
+ sheets = xl_diff.get_sheet_names("/path/to/file.xlsx")
166
+
167
+ # Diff two sheets by key column 0
168
+ deltas = xl_diff.diff_sheets(
169
+ "/path/to/old.xlsx",
170
+ "Sheet1",
171
+ "/path/to/new.xlsx",
172
+ "Sheet1",
173
+ 0,
174
+ )
175
+
176
+ for d in deltas:
177
+ print(d.row_idx_old, d.row_idx_new, d.col_idx, d.status, d.old_value, d.new_value)
178
+ ```
179
+
180
+ Badge & CI
181
+
182
+ This repository includes GitHub Actions workflows to build manylinux/musllinux and wheels for Windows/macOS, along with an sdist job. The CI badge above links to the main workflow.
183
+
184
+ Contributing
185
+
186
+ Please open issues for bugs and feature requests. Pull requests should target the `main` branch; the `issue/traceability-phase2` branch contains traceability and test improvements.
187
+
188
+ License
189
+
190
+ MIT
@@ -0,0 +1,139 @@
1
+
2
+ # xl-diff (sheetdiff)
3
+
4
+ Minimal, efficient spreadsheet diff tool. Load CSV/XLSX and produce unified +/- row and cell diffs.
5
+
6
+ ## Run locally from a clone (web UI)
7
+
8
+ If you just want to clone the repo and use the web UI on your own machine, without publishing to PyPI:
9
+
10
+ ```bash
11
+ git clone https://github.com/muhammadumer0266/xl-diff.git
12
+ cd xl-diff
13
+
14
+ # Create and activate a virtualenv
15
+ python -m venv .venv
16
+ # macOS/Linux:
17
+ source .venv/bin/activate
18
+ # Windows (PowerShell):
19
+ .venv\Scripts\Activate.ps1
20
+
21
+ # Install the package in editable mode with the web extra
22
+ pip install -e ".[web]"
23
+
24
+ # Start the web server
25
+ xldiff-web
26
+ ```
27
+
28
+ Then open http://localhost:5000, upload two spreadsheets (optionally a key column), and view a GitHub-style row/cell diff (green = added, red = removed, yellow = modified with old value struck through above the new one).
29
+
30
+ The pure-Python (pandas-based) diff engine works out of the box after `pip install -e ".[web]"`. If you also want the faster Rust-backed engine, additionally run `maturin develop --release` (see "Building the Rust extension" below) before starting `xldiff-web`.
31
+
32
+ ## Install from PyPI (distributed as `sheetdiff`)
33
+
34
+ ```
35
+ pip install sheetdiff
36
+ ```
37
+
38
+ Quick CLI
39
+
40
+ ```
41
+ python -m sheetdiff.cli left.xlsx right.xlsx --key id
42
+ ```
43
+
44
+ Web UI (GitHub-style diff viewer)
45
+
46
+ ```
47
+ pip install sheetdiff[web]
48
+ xldiff-web
49
+ ```
50
+
51
+ Open http://localhost:5000, upload two spreadsheets (optionally a key column), and view a GitHub-style row/cell diff (green = added, red = removed, yellow = modified with old value struck through above the new one).
52
+
53
+ Remote storage (S3, MinIO, Cloudflare R2, Garage, Azure Blob)
54
+
55
+ ```
56
+ pip install sheetdiff[remote]
57
+ sheetdiff s3://my-bucket/left.xlsx s3://my-bucket/right.xlsx --key id
58
+ ```
59
+
60
+ Files can also be referenced by URI in the web UI (each row accepts an upload *or* a URI) and from the Python API via `sheetdiff.resolve_source`. Credentials and per-backend endpoints are configured once via environment variables, never through the web form — see [docs/REMOTE_STORAGE.md](docs/REMOTE_STORAGE.md) for the full list, plus how to configure upload/remote size limits (`SHEETDIFF_MAX_UPLOAD_MB`, `SHEETDIFF_MAX_REMOTE_MB`, `SHEETDIFF_MAX_FILES`).
61
+
62
+ Python API
63
+
64
+ ```
65
+ from sheetdiff.core import diff_sheets, format_unified
66
+
67
+ changes = diff_sheets('left.csv','right.csv', key='id')
68
+ print(format_unified(changes))
69
+ ```
70
+
71
+ Functions
72
+
73
+ - `diff_sheets(left, right, key=None, sheet_name=None)` — compare two files; if `key` provided, rows are matched by that column, otherwise by position. Returns list of tuples `(type, row_key_or_index, column, old, new)`.
74
+ - `format_unified(changes)` — render a compact unified +/- text view.
75
+
76
+ Integration notes
77
+
78
+ - For web backends (Django/Celery), call `diff_sheets` or `compare_workbooks` inside a worker and serialize `changes` to JSON for paginated display.
79
+ - The package will prefer a Rust-backed binary extension `xl_diff` when available (faster, memory-efficient). If the Rust extension is not installed, the pure-Python fallback (`pandas` based) is used.
80
+ - For very large files implement the Rust core (see `RUST_INTEGRATION.md`) and build wheels via `maturin` for best performance.
81
+
82
+ # xl-diff
83
+
84
+ [![CI](https://github.com/muhammadumer0266/xl-diff/actions/workflows/CI.yml/badge.svg)](https://github.com/muhammadumer0266/xl-diff/actions/workflows/CI.yml)
85
+ [![crates.io](https://img.shields.io/crates/v/xl_diff.svg)](https://crates.io/crates/xl_diff)
86
+ [![PyPI](https://img.shields.io/pypi/v/sheetdiff.svg)](https://pypi.org/project/sheetdiff/)
87
+ [![Language](https://img.shields.io/badge/Language-Rust%20%2F%20Python-orange.svg)](https://www.rust-lang.org/)
88
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
89
+
90
+ xl-diff is a high-performance, memory-safe Excel comparison engine written in Rust and exposed as a Python extension via PyO3. It is designed to integrate into Django/Celery pipelines and produce cell-level semantic diffs between spreadsheet versions.
91
+
92
+ Features
93
+ - Fast, zero-copy reading of XLSX using `calamine`.
94
+ - Parallel diff computation using `rayon`.
95
+ - Row alignment using an optional key column or positional fallback.
96
+ - Python bindings (PyO3) for seamless integration.
97
+ - Cross-platform packaging with `maturin` and GitHub Actions.
98
+
99
+ ## Building the Rust extension
100
+
101
+ The package prefers a Rust-backed binary extension (`xl_diff`, faster and more memory-efficient) when available, falling back to the pure-Python implementation otherwise. To build it in-place inside your virtualenv:
102
+
103
+ ```bash
104
+ pip install maturin
105
+ maturin develop --release
106
+ ```
107
+
108
+ Python usage
109
+
110
+ ```python
111
+ import xl_diff
112
+
113
+ # List sheets
114
+ sheets = xl_diff.get_sheet_names("/path/to/file.xlsx")
115
+
116
+ # Diff two sheets by key column 0
117
+ deltas = xl_diff.diff_sheets(
118
+ "/path/to/old.xlsx",
119
+ "Sheet1",
120
+ "/path/to/new.xlsx",
121
+ "Sheet1",
122
+ 0,
123
+ )
124
+
125
+ for d in deltas:
126
+ print(d.row_idx_old, d.row_idx_new, d.col_idx, d.status, d.old_value, d.new_value)
127
+ ```
128
+
129
+ Badge & CI
130
+
131
+ This repository includes GitHub Actions workflows to build manylinux/musllinux and wheels for Windows/macOS, along with an sdist job. The CI badge above links to the main workflow.
132
+
133
+ Contributing
134
+
135
+ Please open issues for bugs and feature requests. Pull requests should target the `main` branch; the `issue/traceability-phase2` branch contains traceability and test improvements.
136
+
137
+ License
138
+
139
+ MIT
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sheetdiff"
7
+ version = "0.1.0"
8
+ description = "Concise spreadsheet diff utility (CSV/XLSX) with an optional GitHub/VS Code-style web UI"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [ { name = "Muhammad Umer", email = "muhammadumer0266@gmail.com" } ]
12
+ license = { file = "LICENSE" }
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Office/Business",
19
+ "Topic :: Utilities",
20
+ ]
21
+ dependencies = ["pandas>=1.5", "openpyxl>=3.0"]
22
+
23
+ [project.optional-dependencies]
24
+ web = ["flask>=2.0"]
25
+ remote = ["fsspec>=2023.1", "s3fs>=2023.1", "adlfs>=2023.1"]
26
+ all = ["sheetdiff[web,remote]"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/muhammadumer0266/xl-diff"
30
+ Repository = "https://github.com/muhammadumer0266/xl-diff"
31
+ Issues = "https://github.com/muhammadumer0266/xl-diff/issues"
32
+
33
+ [project.scripts]
34
+ xldiff-web = "sheetdiff.webapp:main"
35
+
36
+ [tool.setuptools.packages.find]
37
+ include = ["sheetdiff", "sheetdiff.*"]
38
+
39
+ [tool.setuptools.package-data]
40
+ sheetdiff = ["templates/*.html"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ from .core import diff_sheets, format_unified, diff_rows_full, diff_multi_grid, compare_workbooks
2
+ from . import config
3
+ from .storage import resolve as resolve_source, resolve_many as resolve_sources, StorageError
4
+
5
+ __all__ = [
6
+ "diff_sheets", "format_unified", "diff_rows_full", "diff_multi_grid", "compare_workbooks",
7
+ "config", "resolve_source", "resolve_sources", "StorageError",
8
+ ]
@@ -0,0 +1,42 @@
1
+ import argparse
2
+ import sys
3
+ import tempfile
4
+
5
+ from . import config, storage
6
+ from .core import diff_sheets, format_unified, diff_workbook, format_workbook
7
+
8
+ def main():
9
+ p = argparse.ArgumentParser(prog="sheetdiff")
10
+ p.add_argument("left", help="local path or remote URI (s3://, minio://, r2://, garage://, az://)")
11
+ p.add_argument("right", help="local path or remote URI (s3://, minio://, r2://, garage://, az://)")
12
+ p.add_argument("--key", help="column name to use as row key")
13
+ p.add_argument("--sheet", help="sheet name for Excel files")
14
+ p.add_argument("--all-sheets", action="store_true", help="compare all sheets in workbooks")
15
+ p.add_argument(
16
+ "--max-remote-mb", type=int, default=None,
17
+ help=f"override the remote download size cap (default: {config.MAX_REMOTE_MB} MB, "
18
+ "also settable via SHEETDIFF_MAX_REMOTE_MB)",
19
+ )
20
+ args = p.parse_args()
21
+
22
+ max_bytes = args.max_remote_mb * 1024 * 1024 if args.max_remote_mb else None
23
+
24
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
25
+ try:
26
+ left_path = storage.resolve(args.left, tmp, max_bytes=max_bytes)
27
+ right_path = storage.resolve(args.right, tmp, max_bytes=max_bytes)
28
+ except storage.StorageError as e:
29
+ print(f"error: {e}", file=sys.stderr)
30
+ raise SystemExit(1)
31
+
32
+ if args.all_sheets:
33
+ from .core import compare_workbooks
34
+ wb = compare_workbooks(left_path, right_path, key=args.key)
35
+ print(format_workbook(wb))
36
+ else:
37
+ changes = diff_sheets(left_path, right_path, key=args.key, sheet_name=args.sheet)
38
+ print(format_unified(changes))
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
@@ -0,0 +1,127 @@
1
+ """Central, environment-driven configuration for sheetdiff.
2
+
3
+ Every knob here can be overridden with an environment variable so a
4
+ deployment can tune limits and remote-storage credentials without touching
5
+ code. Nothing here reads a hardcoded secret — credentials only ever come
6
+ from the environment (or a `.env` loaded by the process before start-up),
7
+ never from user-submitted form fields, so a web UI cannot be used to smuggle
8
+ credentials for a backend the operator didn't configure.
9
+ """
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from typing import Dict, Optional
13
+
14
+
15
+ def _env_int(name: str, default: int) -> int:
16
+ raw = os.environ.get(name)
17
+ if raw is None or raw.strip() == "":
18
+ return default
19
+ try:
20
+ value = int(raw)
21
+ except ValueError:
22
+ raise ValueError(f"{name}={raw!r} is not a valid integer") from None
23
+ if value <= 0:
24
+ raise ValueError(f"{name}={raw!r} must be a positive integer")
25
+ return value
26
+
27
+
28
+ def _env_list(name: str, default: str) -> list:
29
+ raw = os.environ.get(name, default)
30
+ return [x.strip().lower() for x in raw.split(",") if x.strip()]
31
+
32
+
33
+ # --- Upload / download size limits -----------------------------------------
34
+ # All expressed in MB for readability at the env-var layer; converted to
35
+ # bytes for use. Change per-deployment via env vars, e.g.:
36
+ # SHEETDIFF_MAX_UPLOAD_MB=250
37
+ # SHEETDIFF_MAX_REMOTE_MB=500
38
+ # SHEETDIFF_MAX_FILES=6
39
+ MAX_UPLOAD_MB = _env_int("SHEETDIFF_MAX_UPLOAD_MB", 100)
40
+ MAX_REMOTE_MB = _env_int("SHEETDIFF_MAX_REMOTE_MB", MAX_UPLOAD_MB)
41
+ MAX_UPLOAD_BYTES = MAX_UPLOAD_MB * 1024 * 1024
42
+ MAX_REMOTE_BYTES = MAX_REMOTE_MB * 1024 * 1024
43
+
44
+ # Hard ceiling on how many files can be compared in one request (web UI and
45
+ # CLI --all-sheets multi-file mode), to bound memory/CPU on shared servers.
46
+ MAX_FILES = _env_int("SHEETDIFF_MAX_FILES", 8)
47
+
48
+ # Network timeouts for remote storage reads (seconds).
49
+ REMOTE_CONNECT_TIMEOUT = _env_int("SHEETDIFF_REMOTE_CONNECT_TIMEOUT", 10)
50
+ REMOTE_READ_TIMEOUT = _env_int("SHEETDIFF_REMOTE_READ_TIMEOUT", 60)
51
+
52
+ # Which remote URI schemes are enabled at all. Empty by default in the sense
53
+ # that only schemes listed here are ever dispatched to fsspec; anything else
54
+ # (including http/https, to avoid turning the diff endpoint into an SSRF
55
+ # proxy) is rejected outright.
56
+ ALLOWED_REMOTE_SCHEMES = set(
57
+ _env_list("SHEETDIFF_ALLOWED_SCHEMES", "s3,minio,r2,garage,az,azure,gs")
58
+ )
59
+
60
+
61
+ @dataclass
62
+ class BackendConfig:
63
+ protocol: str # fsspec protocol, e.g. "s3" or "az"
64
+ storage_options: Dict[str, str] = field(default_factory=dict)
65
+
66
+
67
+ def _s3_backend(scheme: str) -> BackendConfig:
68
+ prefix = f"SHEETDIFF_STORAGE_{scheme.upper()}_"
69
+ opts: Dict[str, str] = {}
70
+ endpoint = os.environ.get(prefix + "ENDPOINT_URL")
71
+ if endpoint:
72
+ opts["client_kwargs"] = {"endpoint_url": endpoint}
73
+ key = os.environ.get(prefix + "KEY") or os.environ.get("AWS_ACCESS_KEY_ID")
74
+ secret = os.environ.get(prefix + "SECRET") or os.environ.get("AWS_SECRET_ACCESS_KEY")
75
+ region = os.environ.get(prefix + "REGION") or os.environ.get("AWS_DEFAULT_REGION")
76
+ if key:
77
+ opts["key"] = key
78
+ if secret:
79
+ opts["secret"] = secret
80
+ if region:
81
+ opts.setdefault("client_kwargs", {})["region_name"] = region
82
+ return BackendConfig(protocol="s3", storage_options=opts)
83
+
84
+
85
+ def _azure_backend() -> BackendConfig:
86
+ prefix = "SHEETDIFF_STORAGE_AZ_"
87
+ opts: Dict[str, str] = {}
88
+ conn_str = os.environ.get(prefix + "CONNECTION_STRING")
89
+ if conn_str:
90
+ opts["connection_string"] = conn_str
91
+ else:
92
+ account = os.environ.get(prefix + "ACCOUNT_NAME")
93
+ account_key = os.environ.get(prefix + "ACCOUNT_KEY")
94
+ sas_token = os.environ.get(prefix + "SAS_TOKEN")
95
+ if account:
96
+ opts["account_name"] = account
97
+ if account_key:
98
+ opts["account_key"] = account_key
99
+ if sas_token:
100
+ opts["sas_token"] = sas_token
101
+ return BackendConfig(protocol="az", storage_options=opts)
102
+
103
+
104
+ def _gcs_backend() -> BackendConfig:
105
+ opts: Dict[str, str] = {}
106
+ creds = os.environ.get("SHEETDIFF_STORAGE_GS_TOKEN")
107
+ if creds:
108
+ opts["token"] = creds
109
+ return BackendConfig(protocol="gcs", storage_options=opts)
110
+
111
+
112
+ def backend_for_scheme(scheme: str) -> Optional[BackendConfig]:
113
+ """Map a URI scheme (e.g. 'minio', 'r2', 'az') to an fsspec backend.
114
+
115
+ Returns None if the scheme isn't recognized/enabled, so callers can
116
+ reject it instead of silently falling through to some default.
117
+ """
118
+ scheme = scheme.lower()
119
+ if scheme not in ALLOWED_REMOTE_SCHEMES:
120
+ return None
121
+ if scheme in ("s3", "minio", "r2", "garage"):
122
+ return _s3_backend(scheme)
123
+ if scheme in ("az", "azure"):
124
+ return _azure_backend()
125
+ if scheme == "gs":
126
+ return _gcs_backend()
127
+ return None