arrows 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 (45) hide show
  1. arrows-0.2.0/LICENSE +21 -0
  2. arrows-0.2.0/PKG-INFO +629 -0
  3. arrows-0.2.0/README.md +592 -0
  4. arrows-0.2.0/examples/README.md +87 -0
  5. arrows-0.2.0/examples/bootstrap_aws.py +154 -0
  6. arrows-0.2.0/pyproject.toml +112 -0
  7. arrows-0.2.0/pyproject.toml.orig +106 -0
  8. arrows-0.2.0/src/arrows/__init__.py +159 -0
  9. arrows-0.2.0/src/arrows/__main__.py +6 -0
  10. arrows-0.2.0/src/arrows/auth.py +39 -0
  11. arrows-0.2.0/src/arrows/cli.py +102 -0
  12. arrows-0.2.0/src/arrows/components/__init__.py +69 -0
  13. arrows-0.2.0/src/arrows/components/aws.py +164 -0
  14. arrows-0.2.0/src/arrows/components/gmail.py +33 -0
  15. arrows-0.2.0/src/arrows/components/google.py +115 -0
  16. arrows-0.2.0/src/arrows/components/google_sheets.py +47 -0
  17. arrows-0.2.0/src/arrows/components/redshift.py +137 -0
  18. arrows-0.2.0/src/arrows/components/s3.py +61 -0
  19. arrows-0.2.0/src/arrows/components/sqlite.py +60 -0
  20. arrows-0.2.0/src/arrows/core/__init__.py +62 -0
  21. arrows-0.2.0/src/arrows/core/component.py +147 -0
  22. arrows-0.2.0/src/arrows/core/config.py +70 -0
  23. arrows-0.2.0/src/arrows/core/engine.py +58 -0
  24. arrows-0.2.0/src/arrows/core/errors.py +73 -0
  25. arrows-0.2.0/src/arrows/core/registry.py +90 -0
  26. arrows-0.2.0/src/arrows/core/secrets.py +467 -0
  27. arrows-0.2.0/src/arrows/core/session.py +424 -0
  28. arrows-0.2.0/src/arrows/gmail.py +104 -0
  29. arrows-0.2.0/src/arrows/google_sheets.py +364 -0
  30. arrows-0.2.0/src/arrows/mysql.py +0 -0
  31. arrows-0.2.0/src/arrows/py.typed +0 -0
  32. arrows-0.2.0/src/arrows/redis.py +0 -0
  33. arrows-0.2.0/src/arrows/redshift.py +201 -0
  34. arrows-0.2.0/src/arrows/s3.py +211 -0
  35. arrows-0.2.0/src/arrows/spark.py +0 -0
  36. arrows-0.2.0/src/arrows/sqlite.py +80 -0
  37. arrows-0.2.0/src/arrows/template_renderer.py +22 -0
  38. arrows-0.2.0/src/arrows/utils.py +43 -0
  39. arrows-0.2.0/tests/conftest.py +16 -0
  40. arrows-0.2.0/tests/test_login.py +190 -0
  41. arrows-0.2.0/tests/test_packaging.py +60 -0
  42. arrows-0.2.0/tests/test_public_api.py +36 -0
  43. arrows-0.2.0/tests/test_registry_and_session.py +221 -0
  44. arrows-0.2.0/tests/test_secrets.py +112 -0
  45. arrows-0.2.0/tests/test_sqlite_component.py +32 -0
arrows-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 George Jiang
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.
arrows-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,629 @@
1
+ Metadata-Version: 2.4
2
+ Name: arrows
3
+ Version: 0.2.0
4
+ Summary: Universal data wrangling tool built around Apache Arrow, DuckDB, Polars, S3 for efficiently transferring and transforming data between different data sources with pluggable, independently loadable components
5
+ Keywords: arrow,duckdb,polars,etl,s3,redshift,data-engineering
6
+ Author: George Jiang
7
+ Author-email: George Jiang <huifengjiang@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Database
17
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: duckdb>=1.5.5
20
+ Requires-Dist: jinja2>=3.1.6
21
+ Requires-Dist: pyarrow>=25.0.1
22
+ Requires-Dist: adbc-driver-postgresql==1.11.0
23
+ Requires-Dist: awswrangler>=3.17.1
24
+ Requires-Dist: boto3>=1.35
25
+ Requires-Dist: psycopg2-binary>=2.9.13
26
+ Requires-Dist: google-api-python-client>=2.200.0
27
+ Requires-Dist: google-auth>=2.58.0
28
+ Requires-Dist: pandas>=2.2
29
+ Requires-Dist: polars>=1.44.2
30
+ Requires-Dist: keyring>=25.0
31
+ Requires-Python: >=3.11
32
+ Project-URL: Homepage, https://github.com/George-Jiang/arrows
33
+ Project-URL: Repository, https://github.com/George-Jiang/arrows
34
+ Project-URL: Issues, https://github.com/George-Jiang/arrows/issues
35
+ Project-URL: Changelog, https://github.com/George-Jiang/arrows/blob/main/CHANGELOG.md
36
+ Description-Content-Type: text/markdown
37
+
38
+ # arrows
39
+
40
+ arrows is an **universal data wrangling tool** built around Apache Arrow, DuckDB, Polars, S3 for efficiently transferring and transforming data between different data sources.
41
+
42
+ Every integration is a **component**: an independently installable, independently
43
+ loadable unit that owns its own dependencies and its own credentials. A job that
44
+ only sends email never imports boto3, and never needs a Redshift password to exist.
45
+
46
+ ## Features
47
+
48
+ - 🧩 **Pluggable components**: load only what a job needs; add new ones without touching the core
49
+ - 🔄 **Data format conversion**: Apache Arrow, Pandas, Polars, DuckDB
50
+ - 📊 **Google Sheets**: read and write, with SQL query support
51
+ - 🗄️ **Amazon Redshift**: query, UNLOAD to S3, COPY from S3
52
+ - ☁️ **AWS S3**: Parquet datasets, queryable in place
53
+ - 📧 **Gmail**: send templated email
54
+ - 🔐 **Layered secret resolution**: env → .env → local files → OS keychain → AWS Secrets Manager
55
+ - 📝 **SQL template rendering** with Jinja2
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install arrows # every component
61
+ uv add arrows # with uv
62
+ ```
63
+
64
+ That is the whole installation. There are no extras to choose between: every
65
+ component's libraries are ordinary dependencies.
66
+
67
+ Installing everything does **not** mean loading everything. `import arrows` pulls
68
+ in none of it — boto3, psycopg2, the Google client and Polars are imported the
69
+ first time a component that needs them is loaded, so start-up stays in the tens
70
+ of milliseconds whatever a job actually uses.
71
+
72
+ ## Components
73
+
74
+ ```bash
75
+ arrows components # what exists, what it needs, what is loaded
76
+ arrows secrets redshift # which secrets resolve, and from where (values redacted)
77
+ arrows doctor s3 redshift # load them and run a real health check
78
+ arrows login redshift # type the missing secrets, then verify
79
+ ```
80
+
81
+ | Component | Depends on | Third-party libraries | Purpose |
82
+ |---|---|---|---|
83
+ | `aws` | – | boto3 | boto3 session + DuckDB S3 credentials |
84
+ | `s3` | `aws` | pyarrow | Parquet datasets in S3 |
85
+ | `redshift` | `aws` | psycopg2, awswrangler, ADBC | Query, UNLOAD, COPY |
86
+ | `google` | – | google-auth | Shared Google OAuth credentials |
87
+ | `google_sheets` | `google` | google-api-python-client | Read/write Sheets |
88
+ | `gmail` | `google` | google-api-python-client | Send email |
89
+ | `sqlite` | – | duckdb | Local SQLite files (reference component) |
90
+
91
+ ### Loading
92
+
93
+ Nothing is loaded on import. Ask for what the job needs:
94
+
95
+ ```python
96
+ import arrows
97
+
98
+ arrows.load('s3', 'redshift') # authenticates both, fails fast if a secret is missing
99
+ arrows.load('gmail') # pulls in 'google' automatically
100
+ ```
101
+
102
+ Loading is how a job states its requirements up front, so a missing credential
103
+ surfaces at start-up instead of an hour into a run. In production, turn off
104
+ implicit loading so that is enforced:
105
+
106
+ ```python
107
+ arrows.configure(autoload=False) # or ARROWS_AUTOLOAD=0
108
+ ```
109
+
110
+ With `autoload` left on (the default), using a data API loads its component on
111
+ first call, which is what you want in a notebook.
112
+
113
+ Other entry points:
114
+
115
+ ```python
116
+ arrows.list_components() # names, dependencies, load state
117
+ arrows.health() # per-component health checks
118
+ arrows.unload('redshift') # close connections, drop registered secrets
119
+ arrows.close() # release everything
120
+ ```
121
+
122
+ `ARROWS_COMPONENTS=s3,redshift` supplies the default set, so `arrows.load()` with
123
+ no arguments loads it — useful for containers and scheduled jobs. Creating the
124
+ session itself never touches a credential, so `arrows.configure(secrets=...)` can
125
+ install a different secret source first.
126
+
127
+ ### Isolated sessions
128
+
129
+ A `Session` owns its components and its secret store, so one process can talk to
130
+ two environments at once:
131
+
132
+ ```python
133
+ from arrows.core import Session, SecretStore
134
+ from arrows.core.secrets import AwsSecretsManagerProvider
135
+
136
+ prod = Session(secrets=SecretStore([AwsSecretsManagerProvider('prod/arrows')]))
137
+ with prod.activate(): # module-level APIs now use this session
138
+ prod.load('redshift')
139
+ arrow = arrows.redshift.fetch_arrow('select 1')
140
+ ```
141
+
142
+ ### Adding a component
143
+
144
+ Copy [`src/arrows/components/sqlite.py`](src/arrows/components/sqlite.py) — the
145
+ reference implementation — and declare what you need:
146
+
147
+ ```python
148
+ from arrows.core.component import Component, HealthStatus
149
+
150
+
151
+ class ClickhouseComponent(Component):
152
+ name = 'clickhouse'
153
+ requires = ('CLICKHOUSE_URL', 'CLICKHOUSE_PASSWORD') # resolved before setup runs
154
+ optional = ('CLICKHOUSE_DATABASE',)
155
+ depends_on = () # other components
156
+
157
+ def setup(self, secrets):
158
+ # A hint is only needed for a component distributed separately from
159
+ # arrows; a built-in leaves it out, since its libraries always ship.
160
+ driver = self.import_module('clickhouse_connect', hint='pip install arrows-clickhouse')
161
+ self.client = driver.get_client(
162
+ host=self.require_secret('CLICKHOUSE_URL'),
163
+ password=self.require_secret('CLICKHOUSE_PASSWORD'),
164
+ )
165
+
166
+ def health_check(self):
167
+ return HealthStatus(self.name, bool(self.client.ping()))
168
+
169
+ def close(self):
170
+ self.client.close()
171
+
172
+
173
+ COMPONENT = ClickhouseComponent
174
+ ```
175
+
176
+ Register it in [`src/arrows/components/__init__.py`](src/arrows/components/__init__.py)
177
+ for a built-in, or ship it from **your own package** with no change to arrows at all:
178
+
179
+ ```toml
180
+ [project.entry-points."arrows.components"]
181
+ clickhouse = "arrows_clickhouse:SPEC"
182
+ ```
183
+
184
+ ```python
185
+ SPEC = ComponentSpec(
186
+ name='clickhouse',
187
+ module='arrows_clickhouse.component', # imported only when the component is loaded
188
+ summary='ClickHouse over the native protocol',
189
+ install_hint='pip install arrows-clickhouse',
190
+ )
191
+ ```
192
+
193
+ The component's data API is an ordinary module; it reaches its component with
194
+ `arrows.core.session.get('clickhouse')`.
195
+
196
+ ## Secrets
197
+
198
+ Components never read files or environment variables directly. They ask a
199
+ `SecretStore` for a **key**; where that key comes from is a deployment decision.
200
+ The default chain, first hit wins:
201
+
202
+ | Order | Provider | Intended for |
203
+ |---|---|---|
204
+ | 1 | `EnvProvider` | Containers, CI, 12-factor deployments |
205
+ | 2 | `DotEnvProvider('.env')` | Local development |
206
+ | 3 | `SecretFileProvider(~/.credentials)` | One file per secret; also Docker/K8s `/run/secrets` |
207
+ | 4 | `JsonFileProvider(...)` | The legacy `~/.credentials/*.json` layout |
208
+ | 5 | `KeyringProvider` | macOS Keychain / libsecret / Windows Credential Manager |
209
+ | 6 | `PromptProvider` | Notebooks and one-off scripts, opt-in — see below |
210
+
211
+ `AwsSecretsManagerProvider` is available for production, and is the recommended
212
+ source once more than one machine is involved:
213
+
214
+ ```python
215
+ import arrows
216
+ from arrows.core import SecretStore
217
+ from arrows.core.secrets import AwsSecretsManagerProvider, EnvProvider
218
+
219
+ arrows.configure(secrets=SecretStore([EnvProvider(), AwsSecretsManagerProvider('prod/arrows')]))
220
+ arrows.load('redshift')
221
+ ```
222
+
223
+ Guarantees worth knowing:
224
+
225
+ - **Values are not copied into `os.environ`.** The environment is inherited by
226
+ every subprocess and appears in crash dumps. Use `store.export([...])` only for
227
+ a library that insists on reading the environment itself.
228
+ - **Values cannot leak into a log by accident.** `Secret.__str__` and
229
+ `__repr__` render `***`; the plaintext requires an explicit `.reveal()`.
230
+ - **Credential files are permission-checked.** A file readable by group or others
231
+ triggers a warning telling you to `chmod 600`.
232
+ - **No secret is ever written to disk by arrows.**
233
+
234
+ ### Notebooks: supplying secrets by hand
235
+
236
+ Two ways, mixable. **In code**, with named arguments per component:
237
+
238
+ ```python
239
+ import arrows
240
+
241
+ arrows.redshift.login(host='my-cluster...amazonaws.com', database='dev', user='analyst', password='...')
242
+
243
+ arrows.s3.login(profile='analytics', bucket='my-staging-bucket')
244
+ arrows.google_sheets.login(token_json=open('~/token.json').read())
245
+ ```
246
+
247
+ **By prompt**, for anything not supplied — masked, and only for what is actually
248
+ missing:
249
+
250
+ ```python
251
+ arrows.login('redshift')
252
+ # redshift: enter 2 secret(s), blank to skip
253
+ # REDSHIFT_HOST: ········
254
+ # REDSHIFT_PASSWORD: ········
255
+
256
+ arrows.redshift.login(user='analyst') # asks only for host, database, password
257
+ ```
258
+
259
+ The precedence rule differs between the two on purpose:
260
+
261
+ | Source | Position in the chain | Why |
262
+ |---|---|---|
263
+ | Passed in code | **first** — outranks everything | You wrote it down; it is an instruction, not a fallback |
264
+ | Typed at a prompt | **last** — fills gaps only | A value typed in a notebook must not shadow real configuration |
265
+
266
+ Calling `login()` again with different credentials rebuilds the affected
267
+ components (and anything depending on them), so rotating a password mid-session
268
+ takes effect immediately.
269
+
270
+ Keyword names come from each component's `login_args`; a raw `UPPER_CASE` secret
271
+ key always works too, which is the escape hatch for keys a component has not
272
+ named:
273
+
274
+ ```python
275
+ arrows.login('redshift', REDSHIFT_IAM_DURATION_SECONDS='900')
276
+ ```
277
+
278
+ To stop retyping on every kernel restart, save to the OS keychain — needs
279
+ `arrows[keyring]`:
280
+
281
+ ```python
282
+ arrows.redshift.login(password='...', save=True) # found by KeyringProvider next time
283
+ arrows.login('redshift', save=True) # same, for prompted answers
284
+ ```
285
+
286
+ Same thing from a terminal: `arrows login redshift --save`.
287
+
288
+ Prompting is inert outside a terminal or an IPython kernel, and
289
+ `ARROWS_NON_INTERACTIVE=1` disables it everywhere — a scheduled job raises a
290
+ missing-secret error naming the key, instead of hanging forever on a prompt
291
+ nobody will answer. `prompt=False` forces that behaviour, `prompt=True` requires
292
+ a human. So the same call works in both places:
293
+
294
+ ```python
295
+ arrows.redshift.login(host=..., user=..., password=...) # script: no prompt, nothing missing
296
+ arrows.redshift.login() # notebook: asks for all of it
297
+ ```
298
+
299
+ A key ending in `_JSON` or `_FILE` accepts a path at the prompt, which is the
300
+ sane way to supply a Google token:
301
+
302
+ ```python
303
+ arrows.login('google')
304
+ # GOOGLE_TOKEN_JSON: ~/Downloads/token.json ← read from the file
305
+ ```
306
+
307
+ ### Keys by component
308
+
309
+ | Component | Required | Optional |
310
+ |---|---|---|
311
+ | `aws` | – (falls back to the standard AWS chain) | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `AWS_PROFILE`, `AWS_REGION` |
312
+ | `s3` | – | `ARROWS_DEFAULT_BUCKET` |
313
+ | `redshift` | `REDSHIFT_HOST`, `REDSHIFT_DATABASE`, `REDSHIFT_USER` | `REDSHIFT_PASSWORD` **or** `REDSHIFT_CLUSTER_IDENTIFIER`, `REDSHIFT_PORT` |
314
+ | `google` | `GOOGLE_TOKEN_JSON` | `GOOGLE_SCOPES` |
315
+ | `sqlite` | – | `SQLITE_DATABASE` |
316
+
317
+ Two credentials deserve a note:
318
+
319
+ - **AWS**: prefer *no* credentials in arrows at all. With nothing configured,
320
+ boto3 resolves its own chain — `AWS_PROFILE`, SSO, EC2/ECS/EKS instance roles —
321
+ all of which rotate automatically. Explicit keys are supported for laptops but
322
+ are the weakest option, and they are the only case where key material has to be
323
+ handed to DuckDB as a literal secret.
324
+ - **Redshift**: set `REDSHIFT_CLUSTER_IDENTIFIER` and omit `REDSHIFT_PASSWORD`.
325
+ arrows then mints a short-lived password through `redshift:GetClusterCredentials`
326
+ using the AWS identity already loaded, so no database password exists on disk.
327
+
328
+ ### Migrating from `load_credentials()`
329
+
330
+ `arrows.load_credentials()` and the `arrows.auth.load_*` functions still work and
331
+ now delegate to the component loader, with a `DeprecationWarning`:
332
+
333
+ | Before | Now |
334
+ |---|---|
335
+ | `arrows.load_credentials()` | `arrows.load('s3', 'redshift', 'google')` |
336
+ | `auth.load_aws_credentials()` | `arrows.load('s3')` |
337
+ | `auth.load_redshift_credentials()` | `arrows.load('redshift')` |
338
+ | `auth.load_google_credentials()` | `arrows.load('google')` |
339
+
340
+ The legacy `~/.credentials/` files are still read, so no file needs to move.
341
+
342
+ ## Usage
343
+
344
+ ### Amazon Redshift
345
+
346
+ #### Querying Data
347
+
348
+ ```python
349
+ import arrows
350
+ from arrows import redshift
351
+
352
+ arrows.load('redshift')
353
+
354
+ # Query using S3 Unload engine (default, suitable for large data)
355
+ arrow = redshift.fetch_arrow(sql='SELECT * FROM my_table', engine='s3')
356
+
357
+ # Query using ADBC engine (fast for smaller datasets)
358
+ arrow = redshift.fetch_arrow(sql='SELECT * FROM my_table WHERE date > %(date)s', engine='adbc', date='2024-01-01')
359
+
360
+ # Get Pandas DataFrame (defaults to ADBC engine)
361
+ df = redshift.fetch_dataframe(
362
+ sql='SELECT * FROM my_table',
363
+ engine='adbc',
364
+ dtype_backend='numpy', # or 'pyarrow'
365
+ )
366
+ ```
367
+
368
+ #### Importing Data to Redshift
369
+
370
+ ```python
371
+ from arrows import redshift
372
+
373
+ # Import Arrow data to Redshift
374
+ redshift.arrow_to_redshift(
375
+ arrow=arrow,
376
+ table_name='schema.table_name',
377
+ mode='append', # or 'overwrite'
378
+ )
379
+
380
+ # Copy from S3 to Redshift
381
+ redshift.copy(table_name='schema.table_name', s3_path='s3://bucket/path/', mode='append')
382
+ ```
383
+
384
+ #### Exporting Data to S3
385
+
386
+ ```python
387
+ from arrows import redshift
388
+
389
+ # Export Redshift query results to S3
390
+ dataset = redshift.unload(sql='SELECT * FROM my_table', s3_path='s3://bucket/path/')
391
+ ```
392
+
393
+ #### Executing SQL
394
+
395
+ ```python
396
+ from arrows import redshift
397
+
398
+ # Execute SQL statement
399
+ redshift.execute_sql(sql='CREATE TABLE IF NOT EXISTS my_table (id INT, name VARCHAR)')
400
+
401
+ # Execute SQL file (supports Jinja2 templates)
402
+ redshift.execute_sql_file(sql_script_path='scripts/create_table.sql', table_name='my_table')
403
+ ```
404
+
405
+ ### AWS S3
406
+
407
+ #### Storing and Reading Data
408
+
409
+ ```python
410
+ from arrows import s3
411
+
412
+ # Store Arrow data to S3
413
+ dataset = s3.arrow_to_s3(
414
+ arrow=arrow,
415
+ s3_path='s3://bucket/path/',
416
+ bucket='my-bucket', # Optional
417
+ engine='duckdb', # or 'pyarrow'
418
+ )
419
+
420
+ # Store Polars DataFrame to S3
421
+ dataset = s3.polars_to_s3(df=df, s3_path='s3://bucket/path/')
422
+
423
+ # Read data from S3
424
+ dataset = s3.get_dataset('s3://bucket/path/')
425
+ arrow = dataset.to_arrow()
426
+
427
+ # Convert to Pandas
428
+ df = dataset.to_duckdb().df()
429
+
430
+ # Convert to Polars
431
+ df = dataset.to_polars(lazy=False)
432
+ ```
433
+
434
+ #### S3Dataset Operations
435
+
436
+ ```python
437
+ from arrows import s3
438
+
439
+
440
+ # Get dataset
441
+ dataset = s3.get_dataset('s3://bucket/path/')
442
+
443
+ dataset = s3.S3Dataset(s3_path='s3://bucket/path/')
444
+
445
+
446
+ # Create dataset
447
+ dataset = s3.create_dataset(s3_path='s3://bucket/path/')
448
+
449
+ # Write from Arrow
450
+ dataset.from_arrow(arrow)
451
+
452
+ # Write from Polars
453
+ dataset.from_polars(df)
454
+
455
+ # Import from Redshift
456
+ dataset.from_redshift(sql='SELECT * FROM my_table')
457
+
458
+ # Export to Redshift
459
+ dataset.to_redshift('schema.table_name', mode='append')
460
+
461
+ # Query S3 data with SQL
462
+ result = dataset.query('SELECT * FROM self WHERE id > 100')
463
+
464
+ # Delete dataset
465
+ dataset.delete()
466
+
467
+ # Clear dataset contents
468
+ dataset.clear_contents()
469
+ ```
470
+
471
+ ### Google Sheets
472
+
473
+ #### Reading from Google Sheets
474
+
475
+ ```python
476
+ from arrows import google_sheets
477
+
478
+ # Read data from Google Sheet as Arrow format
479
+ arrow = google_sheets.fetch_arrow(
480
+ spreadsheet_id='your_spreadsheet_id',
481
+ sheet_name='Sheet1',
482
+ sheet_range='A1:D100', # Optional
483
+ all_varchar=False, # Optional: Treat all columns as VARCHAR
484
+ )
485
+
486
+ spreadsheet = google_sheets.get_spreadsheet(spreadsheet_id)
487
+ sheet = spreadsheet.get_sheet(sheet_name)
488
+ # or
489
+ sheet = google_sheets.get_sheet(spreadsheet_id, sheet_name)
490
+
491
+ arrow = sheet.to_arrow(self, sheet_range=None, all_varchar=False, sql=None)
492
+ # or
493
+ df = sheet.to_polars(self, sheet_range=None, all_varchar=False, sql=None)
494
+ # or
495
+ df = sheet.to_pandas(self, sheet_range=None, all_varchar=False, sql=None)
496
+ # or
497
+ duckdb_relation = sheet.to_duckdb(self, sheet_range=None, all_varchar=False, sql=None)
498
+
499
+
500
+ # Use SQL query
501
+ arrow = google_sheets.fetch_arrow(
502
+ spreadsheet_id='your_spreadsheet_id',
503
+ sheet_name='Sheet1',
504
+ sql="""
505
+ SELECT
506
+ *
507
+ FROM
508
+ self
509
+ WHERE column1 > 100
510
+ """,
511
+ )
512
+ ```
513
+
514
+ #### Writing to Google Sheets
515
+
516
+ ```python
517
+ from arrows import google_sheets
518
+
519
+ # Write Arrow data to Google Sheet
520
+ sheet = google_sheets.arrow_to_googlesheet(arrow=arrow, spreadsheet_id='your_spreadsheet_id', sheet_name='Sheet1')
521
+ ```
522
+
523
+ #### Managing Spreadsheets and Sheets
524
+
525
+ ```python
526
+ from arrows import google_sheets
527
+
528
+ # Create a new Spreadsheet
529
+ spreadsheet = google_sheets.create_spreadsheet(
530
+ spreadsheet_name='My Spreadsheet',
531
+ parent_id='parent_id', # Optional
532
+ )
533
+
534
+ # Get a Spreadsheet
535
+ spreadsheet = google_sheets.get_spreadsheet('spreadsheet_id')
536
+
537
+ # Create a new Sheet
538
+ sheet = spreadsheet.create_sheet('New Sheet')
539
+
540
+ # Get a Sheet
541
+ sheet = spreadsheet.get_sheet('Sheet1')
542
+
543
+ # Share Spreadsheet
544
+ spreadsheet.share(email='user@example.com', role='writer')
545
+
546
+ # Delete a Sheet
547
+ spreadsheet.delete_sheet('Sheet1')
548
+ ```
549
+
550
+ ### Gmail
551
+
552
+ #### Sending Emails
553
+
554
+ ```python
555
+ from arrows import gmail
556
+
557
+ # Simple email
558
+ gmail.send_email(to=['user@example.com'], subject='Report', content='<h1>Hello</h1>', cc=['manager@example.com'])
559
+
560
+ # Advanced usage with Email class
561
+ email = gmail.Email(subject='Monthly Report', to=['user@example.com'], sender='Data Team')
562
+
563
+ # Set content from template
564
+ email.from_template('path/to/template.html', variable='value')
565
+
566
+ # Send email
567
+ email.send()
568
+ ```
569
+
570
+ ### SQL Template Rendering
571
+
572
+ ```python
573
+ from arrows.template_renderer import render_template
574
+
575
+ # Render SQL template
576
+ sql = render_template('path/to/template.sql', table_name='my_table', date='2024-01-01')
577
+ ```
578
+
579
+ ## Core API
580
+
581
+ ### Google Sheets
582
+
583
+ - `fetch_arrow()` - Read data from Google Sheet as Arrow format
584
+ - `arrow_to_googlesheet()` - Write Arrow data to Google Sheet
585
+ - `get_sheet()` - Get Sheet object
586
+ - `get_spreadsheet()` - Get Spreadsheet object
587
+ - `create_spreadsheet()` - Create new Spreadsheet
588
+
589
+ ### Redshift
590
+
591
+ - `fetch_arrow()` - Query data from Redshift as Arrow format
592
+ - `fetch_dataframe()` - Query data from Redshift as DataFrame
593
+ - `arrow_to_redshift()` - Import Arrow data to Redshift
594
+ - `unload()` - Export Redshift query results to S3
595
+ - `copy()` - Copy data from S3 to Redshift
596
+ - `execute_sql()` - Execute SQL on Redshift
597
+ - `execute_sql_file()` - Execute SQL file
598
+
599
+ ### S3
600
+
601
+ - `arrow_to_s3()` - Store Arrow data to S3
602
+ - `polars_to_s3()` - Store Polars DataFrame to S3
603
+ - `get_dataset()` - Get S3 dataset
604
+ - `create_dataset()` - Create new S3 dataset
605
+
606
+ ### Gmail
607
+
608
+ - `send_email()` - Send an email
609
+ - `Email` - Class for constructing and sending emails
610
+
611
+
612
+ ## Development
613
+
614
+ ```bash
615
+ uv sync # install with every component
616
+ uv run pytest # unit tests, no credentials needed
617
+ uv run ruff check src tests # lint
618
+ uv run arrows doctor # check real connectivity for what is configured
619
+ ```
620
+
621
+ ## Notes
622
+
623
+ 1. **Load what you need**: `arrows.load(...)` fails fast on a missing credential; `arrows doctor` checks a real round-trip
624
+ 2. **Google Sheets permissions**: the OAuth token needs the `spreadsheets` and `drive` scopes; Gmail needs `gmail.send`
625
+ 3. **S3 permissions**: Redshift UNLOAD/COPY needs the cluster's IAM role to reach the same bucket your session does
626
+ 4. **Data formats**: everything is Apache Arrow in the middle; check type compatibility at the edges
627
+ 5. **Breaking change in 0.2**: `redshift.fetch_arrow(..., engine='s3')` now renders its SQL with Jinja2 (`{{ name }}`)
628
+ like every other entry point, instead of `str.format` (`{name}`)
629
+