alloy-sdk 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,7 @@
1
+ Proprietary License
2
+
3
+ Copyright (c) Alloy. All rights reserved.
4
+
5
+ This software and associated documentation files are proprietary to Alloy.
6
+ Use, copying, modification, distribution, or sublicensing is permitted only
7
+ under a separate written agreement with Alloy.
@@ -0,0 +1,502 @@
1
+ Metadata-Version: 2.4
2
+ Name: alloy-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for Alloy hosted SQL and Mesh Storage APIs
5
+ Project-URL: Homepage, https://www.usealloy.ai/
6
+ Project-URL: Documentation, https://docs.usealloy.ai/
7
+ Project-URL: Repository, https://github.com/alloyrobotics/alloy-web
8
+ Author: Alloy
9
+ License: Proprietary License
10
+
11
+ Copyright (c) Alloy. All rights reserved.
12
+
13
+ This software and associated documentation files are proprietary to Alloy.
14
+ Use, copying, modification, distribution, or sublicensing is permitted only
15
+ under a separate written agreement with Alloy.
16
+ License-File: LICENSE
17
+ Keywords: alloy,autonomy,data-lake,mcap,mesh-storage,robotics,sql
18
+ Classifier: Development Status :: 3 - Alpha
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: License :: Other/Proprietary License
21
+ Classifier: Operating System :: MacOS
22
+ Classifier: Operating System :: POSIX :: Linux
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Topic :: Database
27
+ Classifier: Topic :: Scientific/Engineering
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: <3.13,>=3.12
30
+ Requires-Dist: aioboto3>=15.5.0
31
+ Requires-Dist: boto3>=1.40.61
32
+ Requires-Dist: httpx>=0.27.0
33
+ Requires-Dist: pyarrow>=15.0.0
34
+ Requires-Dist: requests>=2.33.0
35
+ Provides-Extra: dataframes
36
+ Requires-Dist: duckdb>=1.0.0; extra == 'dataframes'
37
+ Requires-Dist: pandas<3,>=2.0.0; extra == 'dataframes'
38
+ Requires-Dist: polars>=1.0.0; extra == 'dataframes'
39
+ Provides-Extra: duckdb
40
+ Requires-Dist: duckdb>=1.0.0; extra == 'duckdb'
41
+ Provides-Extra: pandas
42
+ Requires-Dist: pandas<3,>=2.0.0; extra == 'pandas'
43
+ Provides-Extra: polars
44
+ Requires-Dist: polars>=1.0.0; extra == 'polars'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # Alloy Python SDK
48
+
49
+ Public-ready Python SDK package for hosted Alloy APIs.
50
+
51
+ ## Install
52
+
53
+ Requires Python 3.12. The first public SDK release intentionally supports
54
+ Python 3.12 only; the SDK quality workflow tests Linux and macOS on Python 3.12.
55
+
56
+ After the PyPI release is live:
57
+
58
+ ```bash
59
+ pip install alloy-sdk
60
+ ```
61
+
62
+ Before the PyPI release, install from this repository checkout for validation:
63
+
64
+ ```bash
65
+ uv pip install ./packages/python/alloy-sdk
66
+ ```
67
+
68
+ ## Storage
69
+
70
+ Use `alloy.storage` to upload local files into Mesh Storage, list uploaded
71
+ files, and download files by key. The SDK handles the transfer details.
72
+
73
+ ```python
74
+ from alloy import storage
75
+
76
+ with storage.connect() as store:
77
+ upload = store.upload_folder(
78
+ "local/run-001",
79
+ path="flights/run-001",
80
+ overwrite=False,
81
+ )
82
+
83
+ print(upload.prefix)
84
+ # uploads/sdk-uploads/flights/run-001/
85
+ ```
86
+
87
+ Single-file uploads use the same folder-style `path`:
88
+
89
+ ```python
90
+ from alloy import storage
91
+
92
+ with storage.connect() as store:
93
+ store.upload_file("local/run-001/run.mcap", path="flights/run-001", overwrite=False)
94
+ ```
95
+
96
+ `path` is a Mesh folder under `uploads/sdk-uploads/`; do not include
97
+ `uploads/`, `sdk-uploads/`, a bucket name, or a trailing slash. If `path` is
98
+ omitted, Alloy generates a dated folder such as `2026-06-24/<uuid>`.
99
+
100
+ Read helpers accept either an SDK upload `path` or an existing Mesh prefix.
101
+ Downloads use exact Mesh keys:
102
+
103
+ ```python
104
+ from alloy import storage
105
+
106
+ with storage.connect() as store:
107
+ listing = store.list_files(path="flights/run-001")
108
+ for file in listing:
109
+ print(file.key, file.size)
110
+
111
+ store.download_file(
112
+ "uploads/sdk-uploads/flights/run-001/run.mcap",
113
+ "run.mcap",
114
+ )
115
+ ```
116
+
117
+ Async clients mirror the sync methods:
118
+
119
+ ```python
120
+ from alloy import storage
121
+
122
+ async with storage.async_connect() as store:
123
+ await store.upload_folder(
124
+ "local/run-001",
125
+ path="flights/run-001",
126
+ overwrite=False,
127
+ )
128
+ ```
129
+
130
+ The SDK does not expose delete, move, rename, or overwrite operations in v1.
131
+ `overwrite=True` is rejected; delete/replace workflows must go through Mesh
132
+ deletion semantics.
133
+
134
+ ### Storage Security Model
135
+
136
+ Storage helpers request short-lived, path-scoped R2 credentials from the Alloy
137
+ data API and use them only for the transfer. Do not log `UploadSession`,
138
+ `ReadSession`, `UploadResult`, or raw credential values, and do not hand
139
+ temporary credentials to untrusted code. Upload sessions are scoped to
140
+ `uploads/sdk-uploads/<path>/`; raw object-store credentials can still perform
141
+ S3-compatible writes inside that temporary scope until expiry, so the SDK keeps
142
+ `overwrite=False` fixed and does not expose delete, move, rename, or overwrite
143
+ helpers in v1.
144
+
145
+ ## Hosted SQL
146
+
147
+ Hosted SQL v1 has one public transport: HTTPS `POST /mesh/query` returning Apache
148
+ Arrow IPC stream bytes. The SDK keeps the public API small:
149
+
150
+ - `fetch*` for small row-oriented results.
151
+ - `fetch_rows` for capped Python rowsets with columns, CSV output, and
152
+ truncation metadata.
153
+ - `query` for an Arrow-backed result object.
154
+ - `query_arrow`, `query_pandas`, `query_polars`, and `query_df` for direct formats.
155
+ - `stream` for batch iteration.
156
+
157
+ ### Connect
158
+
159
+ The SDK reads `ALLOY_DATA_URL` and `ALLOY_API_KEY`.
160
+
161
+ ```bash
162
+ export ALLOY_DATA_URL="https://data.usealloy.ai"
163
+ export ALLOY_API_KEY="ak_..."
164
+ ```
165
+
166
+ ```python
167
+ from alloy import sql
168
+
169
+ with sql.connect() as db:
170
+ count = db.fetchval("SELECT count(*) FROM alloy.fleet.diagnostics")
171
+ ```
172
+
173
+ Pass explicit values when you do not want environment-based config:
174
+
175
+ ```python
176
+ from alloy import sql
177
+
178
+ with sql.connect(
179
+ base_url="https://data.usealloy.ai",
180
+ api_key="ak_...",
181
+ ) as db:
182
+ rows = db.fetch(
183
+ """
184
+ SELECT topic, count(*) AS n
185
+ FROM alloy.fleet.diagnostics
186
+ GROUP BY topic
187
+ """
188
+ )
189
+ ```
190
+
191
+ Use the data API host provided by Alloy for your environment. Common hosts are
192
+ `https://data.usealloy.ai` for production, `https://data-adnav.usealloy.ai` for
193
+ Adnav production, and `https://data-dev.usealloy.ai` for development testing.
194
+
195
+ ### Small Row Results
196
+
197
+ Use `fetch`, `fetchrow`, and `fetchval` when you want ordinary Python values for
198
+ a bounded result set. This is the most compact shape for scripts, dashboards,
199
+ backend routes, and MCP tools.
200
+
201
+ ```python
202
+ from alloy import sql
203
+
204
+ p = sql.param
205
+
206
+ with sql.connect() as db:
207
+ count = db.fetchval("SELECT count(*) FROM alloy.fleet.diagnostics")
208
+
209
+ latest = db.fetchrow(
210
+ """
211
+ SELECT d.topic, d.created_at
212
+ FROM alloy.fleet.diagnostics AS d
213
+ JOIN alloy.mesh.file_meta AS fm
214
+ ON fm.file_id = d.file_id
215
+ AND fm.key = 'alloy.mission_id'
216
+ WHERE fm.value = $mission_id
217
+ ORDER BY d.created_at DESC
218
+ LIMIT 1
219
+ """,
220
+ params={"mission_id": p.utf8("2U8NUGFHGifdCt7tdcnwTd")},
221
+ )
222
+
223
+ rows = db.fetch(
224
+ """
225
+ SELECT d.topic, count(*) AS n
226
+ FROM alloy.fleet.diagnostics AS d
227
+ JOIN alloy.mesh.file_meta AS fm
228
+ ON fm.file_id = d.file_id
229
+ AND fm.key = 'alloy.mission_id'
230
+ WHERE fm.value = $mission_id
231
+ GROUP BY d.topic
232
+ ORDER BY n DESC
233
+ LIMIT 100
234
+ """,
235
+ params={"mission_id": p.utf8("2U8NUGFHGifdCt7tdcnwTd")},
236
+ )
237
+ ```
238
+
239
+ Use `fetch_rows` when the caller needs result metadata, CSV output, or an
240
+ explicit client-side cap in one object:
241
+
242
+ ```python
243
+ from alloy import sql
244
+
245
+ with sql.connect() as db:
246
+ rowset = db.fetch_rows(
247
+ """
248
+ SELECT topic, created_at, payload
249
+ FROM alloy.fleet.diagnostics
250
+ ORDER BY created_at DESC
251
+ LIMIT 1000
252
+ """,
253
+ max_rows=1000,
254
+ )
255
+
256
+ print(rowset.columns)
257
+ print(rowset.row_count)
258
+ print(rowset.to_csv())
259
+
260
+ if rowset.truncated:
261
+ print("Result was capped locally; add or tighten LIMIT in SQL.")
262
+ ```
263
+
264
+ `max_rows` is a client materialization cap. It avoids building unbounded Python
265
+ row lists, but it does not rewrite the SQL, reduce hosted SQL work, or reduce
266
+ the bytes the v1 endpoint sends. Use SQL `LIMIT` for performance and `max_rows`
267
+ as a final guardrail at the SDK boundary.
268
+
269
+ ### Arrow-Backed Results
270
+
271
+ Use `query` when you want metadata plus flexible conversion. The sync result is
272
+ script-friendly and exposes ClickHouse-style row helpers.
273
+
274
+ ```python
275
+ from alloy import sql
276
+
277
+ with sql.connect() as db:
278
+ result = db.query("SELECT * FROM alloy.fleet.diagnostics LIMIT 100")
279
+
280
+ result.column_names
281
+ result.column_types
282
+ result.row_count
283
+ result.result_rows
284
+ result.first_row
285
+ result.first_item
286
+
287
+ for row in result.named_results():
288
+ print(row["topic"])
289
+
290
+ arrow_table = result.to_arrow()
291
+ pandas_df = result.to_pandas()
292
+ polars_df = result.to_polars()
293
+ duckdb_conn = result.to_duckdb(table_name="diagnostics")
294
+ ```
295
+
296
+ Direct format helpers are shortcuts over the same `query` path:
297
+
298
+ ```python
299
+ with sql.connect() as db:
300
+ table = db.query_arrow("SELECT * FROM alloy.fleet.diagnostics LIMIT 100")
301
+ df = db.query_df("SELECT * FROM alloy.fleet.diagnostics LIMIT 100")
302
+ pandas_df = db.query_pandas("SELECT * FROM alloy.fleet.diagnostics LIMIT 100")
303
+ polars_df = db.query_polars("SELECT * FROM alloy.fleet.diagnostics LIMIT 100")
304
+ ```
305
+
306
+ ### Async Usage
307
+
308
+ The async client mirrors the sync client. Use the same method names with
309
+ `await`. Async HTTP is native, Arrow IPC decode runs off the event loop, and
310
+ expensive result materialization is async-only.
311
+
312
+ ```python
313
+ from alloy import sql
314
+
315
+ async with sql.async_connect() as db:
316
+ count = await db.fetchval("SELECT count(*) FROM alloy.fleet.diagnostics")
317
+
318
+ latest = await db.fetchrow(
319
+ """
320
+ SELECT fm.value AS mission_id, d.topic, d.created_at
321
+ FROM alloy.fleet.diagnostics AS d
322
+ LEFT JOIN alloy.mesh.file_meta AS fm
323
+ ON fm.file_id = d.file_id
324
+ AND fm.key = 'alloy.mission_id'
325
+ ORDER BY d.created_at DESC
326
+ LIMIT 1
327
+ """
328
+ )
329
+
330
+ rows = await db.fetch("SELECT topic FROM alloy.fleet.diagnostics LIMIT 100")
331
+
332
+ rowset = await db.fetch_rows(
333
+ "SELECT topic, created_at FROM alloy.fleet.diagnostics LIMIT 1000",
334
+ max_rows=1000,
335
+ )
336
+ csv_text = await rowset.to_csv()
337
+ if rowset.truncated:
338
+ raise RuntimeError("Hosted SQL result exceeded the local row cap")
339
+
340
+ result = await db.query("SELECT * FROM alloy.fleet.diagnostics LIMIT 100")
341
+ table = result.to_arrow()
342
+ df = await result.to_pandas()
343
+ polars_df = await result.to_polars()
344
+ ```
345
+
346
+ ### Streaming Batches
347
+
348
+ Use `stream` for batch-oriented reads. The sync stream decodes directly from the
349
+ HTTP response. The async stream uses async HTTP and decodes Arrow IPC batches in
350
+ a worker thread; v1 still reads the response body before decoding batches.
351
+
352
+ ```python
353
+ from alloy import sql
354
+
355
+ with sql.connect() as db:
356
+ with db.stream("SELECT * FROM alloy.fleet.diagnostics") as stream:
357
+ for batch in stream:
358
+ print(batch.num_rows)
359
+ ```
360
+
361
+ ```python
362
+ from alloy import sql
363
+
364
+ async with sql.async_connect() as db:
365
+ async with db.stream("SELECT * FROM alloy.fleet.diagnostics") as stream:
366
+ async with stream.reader() as reader:
367
+ print(reader.schema.names)
368
+ async for batch in reader:
369
+ print(batch.num_rows)
370
+ ```
371
+
372
+ ### Params
373
+
374
+ Use named typed params instead of formatting values into SQL strings. Params are
375
+ values only: not table names, column names, identifiers, clauses, or SQL
376
+ fragments. The SDK never interpolates params into SQL client-side.
377
+
378
+ ```python
379
+ from alloy import sql
380
+
381
+ p = sql.param
382
+
383
+ with sql.connect() as db:
384
+ rows = db.fetch(
385
+ """
386
+ SELECT d.created_at, d.payload
387
+ FROM alloy.fleet.diagnostics AS d
388
+ JOIN alloy.mesh.file_meta AS fm
389
+ ON fm.file_id = d.file_id
390
+ AND fm.key = 'alloy.mission_id'
391
+ WHERE fm.value = $mission_id
392
+ AND d.created_at >= $since
393
+ AND d.score >= $min_score
394
+ ORDER BY d.created_at DESC
395
+ LIMIT 100
396
+ """,
397
+ params={
398
+ "mission_id": p.utf8("2U8NUGFHGifdCt7tdcnwTd"),
399
+ "since": p.timestamp_us("2026-06-01T00:00:00Z"),
400
+ "min_score": p.float64(0.8),
401
+ },
402
+ )
403
+ ```
404
+
405
+ Params are named only: SQL uses `$mission_id`, and the params key is
406
+ `"mission_id"`. Positional placeholders like `$1` are not supported in v1.
407
+ Supported types are `utf8`, `bool`, `int64`, `float64`, `date32`,
408
+ `timestamp_us`, and `binary`. `int64` is encoded as a decimal string on the
409
+ wire so values are not rounded by JavaScript gateways; use `sql.param.int64(...)`
410
+ instead of hand-writing that payload.
411
+
412
+ ### Local DuckDB
413
+
414
+ Local DuckDB helpers run over the downloaded Arrow result. They do not push work
415
+ back to hosted SQL.
416
+
417
+ ```python
418
+ with sql.connect() as db:
419
+ result = db.query(
420
+ """
421
+ SELECT topic, count(*) AS n
422
+ FROM alloy.fleet.diagnostics
423
+ GROUP BY topic
424
+ """
425
+ )
426
+ summary = result.local_sql("SELECT sum(n) FROM result")
427
+ ```
428
+
429
+ ```python
430
+ async with sql.async_connect() as db:
431
+ result = await db.query(
432
+ """
433
+ SELECT topic, count(*) AS n
434
+ FROM alloy.fleet.diagnostics
435
+ GROUP BY topic
436
+ """
437
+ )
438
+ summary = await result.local_sql("SELECT sum(n) FROM result")
439
+ ```
440
+
441
+ ## V1 Contract
442
+
443
+ - Supported: HTTPS `POST /mesh/query`, bearer API-key auth, Arrow IPC stream output.
444
+ - Supported: PyArrow-first results, with Pandas, Polars, and DuckDB helpers.
445
+ - Supported: named typed SQL value params via `params={...}`.
446
+ - Not supported in v1: public Flight SQL/gRPC, Postgres wire protocol, ODBC/JDBC
447
+ BI plug-and-play, cursors, sessions, or stream resume.
448
+ - If a stream fails partway through, rerun the query.
449
+ - Pointer-follow is for projected payload fields. Filtering on pointered payload
450
+ fields is not a performance contract.
451
+ - Nested filters are evaluated correctly, but arbitrary nested predicate
452
+ pushdown is separate performance hardening work.
453
+
454
+ ## Release Validation
455
+
456
+ The production release flow is documented in [RELEASE.md](RELEASE.md). Public
457
+ SDK releases are shipped by merging the bot-created `release/sdk-vX.Y.Z` ->
458
+ `release/sdk` release PR. That merge publishes through PyPI Trusted Publishing
459
+ after the protected `pypi` environment is approved.
460
+
461
+ Humans set only the release line in `alloy/_version.py`, such as `0.1`. The
462
+ release PR workflow stamps the generated branch with the next available patch
463
+ version, such as `0.1.0` or `0.1.1`.
464
+
465
+ Before marking a release candidate ready, run the local SDK quality gate from
466
+ `packages/python/alloy-sdk`:
467
+
468
+ ```bash
469
+ uv sync --all-extras --frozen
470
+ uv run ruff format --check .
471
+ uv run ruff check .
472
+ uv run ty check
473
+ uv run pip-audit --local --progress-spinner off
474
+ uv run pytest -q
475
+ rm -rf dist
476
+ uv build --out-dir dist
477
+ uv run python scripts/validate_release.py
478
+ uv run python scripts/validate_release_scope.py --base origin/main --head HEAD
479
+ uv run python scripts/validate_distribution.py dist
480
+ uv run twine check dist/*
481
+ ```
482
+
483
+ Then smoke-test the built wheel in a clean environment:
484
+
485
+ ```bash
486
+ smoke_venv=$(mktemp -d)
487
+ uv venv "$smoke_venv" --python 3.12
488
+ uv pip install --python "$smoke_venv/bin/python" dist/*.whl
489
+ "$smoke_venv/bin/python" scripts/smoke_installed_wheel.py
490
+ rm -rf "$smoke_venv"
491
+ ```
492
+
493
+ For live release validation, set `ALLOY_DATA_URL` and `ALLOY_API_KEY`, then run:
494
+
495
+ ```bash
496
+ uv run python scripts/live_smoke.py
497
+ ```
498
+
499
+ The live smoke runs SQL plus sync and async Storage upload/list/download,
500
+ exact-key conflict, and cross-prefix denial checks. It writes tiny objects under
501
+ `uploads/sdk-uploads/sdk-live-smoke/` and leaves them in place because SDK v1
502
+ intentionally has no delete helper.