pqbench 0.0.2__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 (160) hide show
  1. pqbench-0.0.2/Cargo.toml +42 -0
  2. pqbench-0.0.2/PKG-INFO +188 -0
  3. pqbench-0.0.2/README.md +169 -0
  4. pqbench-0.0.2/crates/pqbench/Cargo.toml +53 -0
  5. pqbench-0.0.2/crates/pqbench/src/bench.rs +112 -0
  6. pqbench-0.0.2/crates/pqbench/src/bytemass/aggregate.rs +137 -0
  7. pqbench-0.0.2/crates/pqbench/src/bytemass/analytics.rs +121 -0
  8. pqbench-0.0.2/crates/pqbench/src/bytemass/api.rs +207 -0
  9. pqbench-0.0.2/crates/pqbench/src/bytemass/collection.rs +213 -0
  10. pqbench-0.0.2/crates/pqbench/src/bytemass/json.rs +61 -0
  11. pqbench-0.0.2/crates/pqbench/src/bytemass/mod.rs +30 -0
  12. pqbench-0.0.2/crates/pqbench/src/bytemass/pages.rs +89 -0
  13. pqbench-0.0.2/crates/pqbench/src/bytemass/raw.rs +75 -0
  14. pqbench-0.0.2/crates/pqbench/src/bytemass/remote.rs +143 -0
  15. pqbench-0.0.2/crates/pqbench/src/bytemass/text.rs +90 -0
  16. pqbench-0.0.2/crates/pqbench/src/capability.rs +63 -0
  17. pqbench-0.0.2/crates/pqbench/src/catalog/get/api.rs +38 -0
  18. pqbench-0.0.2/crates/pqbench/src/catalog/get/impl.rs +58 -0
  19. pqbench-0.0.2/crates/pqbench/src/catalog/get/mod.rs +10 -0
  20. pqbench-0.0.2/crates/pqbench/src/catalog/list/api.rs +37 -0
  21. pqbench-0.0.2/crates/pqbench/src/catalog/list/impl.rs +78 -0
  22. pqbench-0.0.2/crates/pqbench/src/catalog/list/mod.rs +10 -0
  23. pqbench-0.0.2/crates/pqbench/src/catalog/mod.rs +7 -0
  24. pqbench-0.0.2/crates/pqbench/src/codecs.rs +475 -0
  25. pqbench-0.0.2/crates/pqbench/src/compression/analytics.rs +90 -0
  26. pqbench-0.0.2/crates/pqbench/src/compression/api.rs +59 -0
  27. pqbench-0.0.2/crates/pqbench/src/compression/json.rs +13 -0
  28. pqbench-0.0.2/crates/pqbench/src/compression/mod.rs +106 -0
  29. pqbench-0.0.2/crates/pqbench/src/compression/raw.rs +104 -0
  30. pqbench-0.0.2/crates/pqbench/src/compression/text.rs +52 -0
  31. pqbench-0.0.2/crates/pqbench/src/config.rs +89 -0
  32. pqbench-0.0.2/crates/pqbench/src/credentials/check/api.rs +96 -0
  33. pqbench-0.0.2/crates/pqbench/src/credentials/check/impl.rs +140 -0
  34. pqbench-0.0.2/crates/pqbench/src/credentials/check/mod.rs +6 -0
  35. pqbench-0.0.2/crates/pqbench/src/credentials/get/api.rs +52 -0
  36. pqbench-0.0.2/crates/pqbench/src/credentials/get/impl.rs +292 -0
  37. pqbench-0.0.2/crates/pqbench/src/credentials/get/mod.rs +6 -0
  38. pqbench-0.0.2/crates/pqbench/src/credentials/mod.rs +6 -0
  39. pqbench-0.0.2/crates/pqbench/src/dialect.rs +88 -0
  40. pqbench-0.0.2/crates/pqbench/src/diff.rs +110 -0
  41. pqbench-0.0.2/crates/pqbench/src/experiment.rs +1113 -0
  42. pqbench-0.0.2/crates/pqbench/src/file/list/api.rs +54 -0
  43. pqbench-0.0.2/crates/pqbench/src/file/list/impl.rs +29 -0
  44. pqbench-0.0.2/crates/pqbench/src/file/list/mod.rs +9 -0
  45. pqbench-0.0.2/crates/pqbench/src/file/mod.rs +7 -0
  46. pqbench-0.0.2/crates/pqbench/src/lib.rs +32 -0
  47. pqbench-0.0.2/crates/pqbench/src/lz/analytics.rs +25 -0
  48. pqbench-0.0.2/crates/pqbench/src/lz/api.rs +46 -0
  49. pqbench-0.0.2/crates/pqbench/src/lz/json.rs +13 -0
  50. pqbench-0.0.2/crates/pqbench/src/lz/mod.rs +121 -0
  51. pqbench-0.0.2/crates/pqbench/src/lz/raw.rs +42 -0
  52. pqbench-0.0.2/crates/pqbench/src/lz/text.rs +12 -0
  53. pqbench-0.0.2/crates/pqbench/src/metastore/get/api.rs +37 -0
  54. pqbench-0.0.2/crates/pqbench/src/metastore/get/impl.rs +64 -0
  55. pqbench-0.0.2/crates/pqbench/src/metastore/get/mod.rs +10 -0
  56. pqbench-0.0.2/crates/pqbench/src/metastore/mod.rs +5 -0
  57. pqbench-0.0.2/crates/pqbench/src/partition/list/api.rs +61 -0
  58. pqbench-0.0.2/crates/pqbench/src/partition/list/impl.rs +143 -0
  59. pqbench-0.0.2/crates/pqbench/src/partition/list/mod.rs +10 -0
  60. pqbench-0.0.2/crates/pqbench/src/partition/mod.rs +8 -0
  61. pqbench-0.0.2/crates/pqbench/src/profile.rs +538 -0
  62. pqbench-0.0.2/crates/pqbench/src/ratelimit.rs +75 -0
  63. pqbench-0.0.2/crates/pqbench/src/report.rs +136 -0
  64. pqbench-0.0.2/crates/pqbench/src/schema/get/api.rs +39 -0
  65. pqbench-0.0.2/crates/pqbench/src/schema/get/impl.rs +104 -0
  66. pqbench-0.0.2/crates/pqbench/src/schema/get/mod.rs +11 -0
  67. pqbench-0.0.2/crates/pqbench/src/schema/list/api.rs +39 -0
  68. pqbench-0.0.2/crates/pqbench/src/schema/list/impl.rs +140 -0
  69. pqbench-0.0.2/crates/pqbench/src/schema/list/mod.rs +12 -0
  70. pqbench-0.0.2/crates/pqbench/src/schema/mod.rs +9 -0
  71. pqbench-0.0.2/crates/pqbench/src/skill.rs +129 -0
  72. pqbench-0.0.2/crates/pqbench/src/stats.rs +217 -0
  73. pqbench-0.0.2/crates/pqbench/src/table/get/api.rs +57 -0
  74. pqbench-0.0.2/crates/pqbench/src/table/get/impl.rs +312 -0
  75. pqbench-0.0.2/crates/pqbench/src/table/get/mod.rs +11 -0
  76. pqbench-0.0.2/crates/pqbench/src/table/list/api.rs +42 -0
  77. pqbench-0.0.2/crates/pqbench/src/table/list/impl.rs +170 -0
  78. pqbench-0.0.2/crates/pqbench/src/table/list/mod.rs +11 -0
  79. pqbench-0.0.2/crates/pqbench/src/table/mod.rs +689 -0
  80. pqbench-0.0.2/crates/pqbench/src/table/selection.rs +88 -0
  81. pqbench-0.0.2/crates/pqbench/src/text.rs +34 -0
  82. pqbench-0.0.2/crates/pqbench/src/third_party/avro/api.rs +41 -0
  83. pqbench-0.0.2/crates/pqbench/src/third_party/avro/impl.rs +38 -0
  84. pqbench-0.0.2/crates/pqbench/src/third_party/avro/mod.rs +10 -0
  85. pqbench-0.0.2/crates/pqbench/src/third_party/delta/api.rs +49 -0
  86. pqbench-0.0.2/crates/pqbench/src/third_party/delta/impl/load.rs +671 -0
  87. pqbench-0.0.2/crates/pqbench/src/third_party/delta/impl.rs +77 -0
  88. pqbench-0.0.2/crates/pqbench/src/third_party/delta/mod.rs +9 -0
  89. pqbench-0.0.2/crates/pqbench/src/third_party/flate2/api.rs +32 -0
  90. pqbench-0.0.2/crates/pqbench/src/third_party/flate2/impl.rs +29 -0
  91. pqbench-0.0.2/crates/pqbench/src/third_party/flate2/mod.rs +9 -0
  92. pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/api.rs +51 -0
  93. pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/impl/load.rs +689 -0
  94. pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/impl.rs +77 -0
  95. pqbench-0.0.2/crates/pqbench/src/third_party/iceberg/mod.rs +10 -0
  96. pqbench-0.0.2/crates/pqbench/src/third_party/lz4/api.rs +32 -0
  97. pqbench-0.0.2/crates/pqbench/src/third_party/lz4/impl.rs +17 -0
  98. pqbench-0.0.2/crates/pqbench/src/third_party/lz4/mod.rs +9 -0
  99. pqbench-0.0.2/crates/pqbench/src/third_party/mod.rs +15 -0
  100. pqbench-0.0.2/crates/pqbench/src/third_party/object_store/api.rs +339 -0
  101. pqbench-0.0.2/crates/pqbench/src/third_party/object_store/impl/remote.rs +542 -0
  102. pqbench-0.0.2/crates/pqbench/src/third_party/object_store/impl.rs +77 -0
  103. pqbench-0.0.2/crates/pqbench/src/third_party/object_store/mod.rs +10 -0
  104. pqbench-0.0.2/crates/pqbench/src/third_party/parquet/api.rs +533 -0
  105. pqbench-0.0.2/crates/pqbench/src/third_party/parquet/impl.rs +807 -0
  106. pqbench-0.0.2/crates/pqbench/src/third_party/parquet/mod.rs +9 -0
  107. pqbench-0.0.2/crates/pqbench/src/third_party/reqwest/api.rs +96 -0
  108. pqbench-0.0.2/crates/pqbench/src/third_party/reqwest/impl.rs +76 -0
  109. pqbench-0.0.2/crates/pqbench/src/third_party/reqwest/mod.rs +9 -0
  110. pqbench-0.0.2/crates/pqbench/src/third_party/snappy/api.rs +32 -0
  111. pqbench-0.0.2/crates/pqbench/src/third_party/snappy/impl.rs +45 -0
  112. pqbench-0.0.2/crates/pqbench/src/third_party/snappy/mod.rs +9 -0
  113. pqbench-0.0.2/crates/pqbench/src/third_party/zstd/api.rs +32 -0
  114. pqbench-0.0.2/crates/pqbench/src/third_party/zstd/impl.rs +16 -0
  115. pqbench-0.0.2/crates/pqbench/src/third_party/zstd/mod.rs +9 -0
  116. pqbench-0.0.2/crates/pqbench/src/v1.rs +863 -0
  117. pqbench-0.0.2/crates/pqbench/src/viz/html.rs +165 -0
  118. pqbench-0.0.2/crates/pqbench/src/viz/mod.rs +158 -0
  119. pqbench-0.0.2/crates/pqbench/tests/bytemass_e2e.rs +184 -0
  120. pqbench-0.0.2/crates/pqbench/tests/common/mod.rs +15 -0
  121. pqbench-0.0.2/crates/pqbench/tests/compression_e2e.rs +108 -0
  122. pqbench-0.0.2/crates/pqbench/tests/delta.rs +697 -0
  123. pqbench-0.0.2/crates/pqbench/tests/diff.rs +51 -0
  124. pqbench-0.0.2/crates/pqbench/tests/file_metadata.rs +88 -0
  125. pqbench-0.0.2/crates/pqbench/tests/file_selection.rs +51 -0
  126. pqbench-0.0.2/crates/pqbench/tests/fixtures/small_reddit_none.parquet +3 -0
  127. pqbench-0.0.2/crates/pqbench/tests/fixtures/small_snappy.parquet +3 -0
  128. pqbench-0.0.2/crates/pqbench/tests/iceberg.rs +438 -0
  129. pqbench-0.0.2/crates/pqbench/tests/lz_e2e.rs +104 -0
  130. pqbench-0.0.2/crates/pqbench/tests/object_patterns.rs +23 -0
  131. pqbench-0.0.2/crates/pqbench/tests/page_headers.rs +108 -0
  132. pqbench-0.0.2/crates/pqbench/tests/redset_e2e.rs +37 -0
  133. pqbench-0.0.2/crates/pqbench/tests/support/mod.rs +126 -0
  134. pqbench-0.0.2/pqbench/__init__.py +79 -0
  135. pqbench-0.0.2/pqbench/native.pyi +390 -0
  136. pqbench-0.0.2/pqbench/py.typed +0 -0
  137. pqbench-0.0.2/pqbench/walk.py +194 -0
  138. pqbench-0.0.2/pyproject.toml +36 -0
  139. pqbench-0.0.2/python/Cargo.lock +4690 -0
  140. pqbench-0.0.2/python/Cargo.toml +48 -0
  141. pqbench-0.0.2/python/README.md +169 -0
  142. pqbench-0.0.2/python/samples/walk.py +37 -0
  143. pqbench-0.0.2/python/src/bytemass.rs +32 -0
  144. pqbench-0.0.2/python/src/catalog.rs +50 -0
  145. pqbench-0.0.2/python/src/credentials.rs +55 -0
  146. pqbench-0.0.2/python/src/file.rs +33 -0
  147. pqbench-0.0.2/python/src/handles.rs +170 -0
  148. pqbench-0.0.2/python/src/lib.rs +89 -0
  149. pqbench-0.0.2/python/src/marshal.rs +160 -0
  150. pqbench-0.0.2/python/src/metastore.rs +25 -0
  151. pqbench-0.0.2/python/src/partition.rs +37 -0
  152. pqbench-0.0.2/python/src/prelude.rs +23 -0
  153. pqbench-0.0.2/python/src/ratelimit.rs +28 -0
  154. pqbench-0.0.2/python/src/records.rs +200 -0
  155. pqbench-0.0.2/python/src/requests.rs +50 -0
  156. pqbench-0.0.2/python/src/schema.rs +54 -0
  157. pqbench-0.0.2/python/src/table.rs +65 -0
  158. pqbench-0.0.2/python/tests/test_bindings.py +173 -0
  159. pqbench-0.0.2/python/tests/test_dbx_e2e.py +141 -0
  160. pqbench-0.0.2/python/tests/test_native_stub.py +258 -0
@@ -0,0 +1,42 @@
1
+ [workspace]
2
+ resolver = "2"
3
+ members = [
4
+ "crates/aipnaming",
5
+ "crates/aipnaming-cli",
6
+ "crates/docscheck",
7
+ "crates/docscheck-cli",
8
+ "crates/pqbench",
9
+ "crates/pqbench-cli",
10
+ ]
11
+ exclude = ["python"]
12
+ default-members = [
13
+ "crates/aipnaming",
14
+ "crates/aipnaming-cli",
15
+ "crates/docscheck",
16
+ "crates/docscheck-cli",
17
+ "crates/pqbench",
18
+ "crates/pqbench-cli",
19
+ ]
20
+
21
+ [workspace.package]
22
+ version = "0.0.2"
23
+ edition = "2021"
24
+ rust-version = "1.91.1"
25
+ license = "MIT OR Apache-2.0"
26
+ authors = ["pqbench contributors"]
27
+ repository = "https://github.com/pqbench/pqbench"
28
+
29
+ [workspace.dependencies]
30
+ aipnaming = { version = "0.0.2", path = "crates/aipnaming" }
31
+ docscheck = { version = "0.0.2", path = "crates/docscheck" }
32
+ pqbench = { version = "0.0.2", path = "crates/pqbench" }
33
+
34
+ [profile.dev]
35
+ opt-level = 0
36
+ debug = 1
37
+ incremental = true
38
+
39
+ # The profile that 'dist' will build with
40
+ [profile.dist]
41
+ inherits = "release"
42
+ lto = "thin"
pqbench-0.0.2/PKG-INFO ADDED
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: pqbench
3
+ Version: 0.0.2
4
+ Classifier: Programming Language :: Python :: 3
5
+ Classifier: Programming Language :: Rust
6
+ Classifier: Programming Language :: Python :: Implementation :: CPython
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Summary: Python bindings for the pqbench command line.
10
+ Keywords: parquet,compression,benchmark
11
+ Author: pqbench contributors
12
+ License-Expression: MIT OR Apache-2.0
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
15
+ Project-URL: Documentation, https://github.com/pqbench/pqbench/tree/main/python
16
+ Project-URL: Homepage, https://github.com/pqbench/pqbench
17
+ Project-URL: Repository, https://github.com/pqbench/pqbench
18
+
19
+ # pqbench
20
+
21
+ PyO3 bindings for [pqbench](https://github.com/pqbench/pqbench). `pip install`
22
+ builds a native wheel; a separate `pqbench` binary is not required. Two
23
+ surfaces, one wheel:
24
+
25
+ * **`pqbench.native`** — the commands: one function per library path, with the
26
+ same arguments. Every name is the Rust function of the same path.
27
+ * **`pqbench`** — the ergonomic surface over them: the client handles every
28
+ command takes (`Config`, `Dialect`, `Lake`, `Layer`, `Workspace`) and `walk`,
29
+ the whole-metastore walk composed once.
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ pip install pqbench
35
+ ```
36
+
37
+ The wheel carries every storage backend (`delta-s3`, `iceberg-s3`), so `s3://`
38
+ reads and Delta/Iceberg tables work out of the box. A storage read
39
+ authenticates with the `credentials=` argument (a `Credentials` record) or,
40
+ for `bytemass`, with `env` on its request — or with the process environment.
41
+
42
+ From a checkout:
43
+
44
+ ```sh
45
+ pip install maturin
46
+ maturin develop --manifest-path python/Cargo.toml
47
+ ```
48
+
49
+ ## The commands
50
+
51
+ Each command is the library function of the same path, under `pqbench.native`,
52
+ and takes the handles and the resource's name like the Rust signature: the name
53
+ is the AIP resource name a parent record carries, so a record feeds the next
54
+ command as it is. `credentials.get` takes a table's name and returns its lease,
55
+ the `Credentials` record a storage read takes, and a record is its model's
56
+ class: `record["uri"]`, `record.uri` and `record.asdict()` agree.
57
+
58
+ They are the walk's own pipeline — metastore, catalog, schema, table,
59
+ credentials, partition, file, bytemass — and they stop at `bytemass`: the
60
+ commands downstream of a byte-mass stream (`viz`, `diff`, `profile`,
61
+ `experiment`), the standalone codec benchmarks (`lz`, `compression`) and
62
+ `skill` are separate tools, and are not bound.
63
+
64
+ ```python
65
+ import asyncio
66
+ import pqbench
67
+ import pqbench.native
68
+
69
+ async def main():
70
+ workspace = pqbench.Workspace(pqbench.Config("https://…", token="…"))
71
+ lake = pqbench.Lake(workspace)
72
+
73
+ # The walk, one function per resource. The I/O verbs are awaitable.
74
+ await pqbench.native.metastore.get.get(workspace)
75
+ await pqbench.native.catalog.get.get(workspace, "catalogs/dbx_samples")
76
+ await pqbench.native.catalog.list.list(workspace)
77
+ await pqbench.native.schema.list.list(workspace, "catalogs/dbx_samples")
78
+ refs = await pqbench.native.table.list.list(
79
+ workspace, "catalogs/dbx_samples/schemas/nyctaxi"
80
+ )
81
+ await pqbench.native.credentials.check.check(
82
+ workspace, "catalogs/dbx_samples/schemas/nyctaxi/tables/trips"
83
+ )
84
+ # `credentials.get.get` is name -> lease: the lease is what a storage read
85
+ # takes, from the table's name alone.
86
+ credentials = await pqbench.native.credentials.get.get(workspace, refs[0]["name"])
87
+ table = await pqbench.native.table.get.get(
88
+ workspace,
89
+ lake,
90
+ refs[0]["name"], refs[0].get("uri"), credentials,
91
+ )
92
+
93
+ # `partition.list.list` and `file.list.list` stream (async iterators); the
94
+ # single-value verbs are awaitable.
95
+ async for partition in pqbench.native.partition.list.list(
96
+ lake, table["name"], table["uri"], table.get("credentials"), 86_400_000
97
+ ):
98
+ async for file in pqbench.native.file.list.list(
99
+ lake, partition["uri"], partition.get("credentials"), partition["commit_versions"]
100
+ ):
101
+ # The pipeline stops here: `bytemass` measures the file's bytes.
102
+ rows = await pqbench.native.bytemass.bytemass(
103
+ lake, pqbench.native.BytemassRequest(inputs=[file["uri"]])
104
+ )
105
+ print(len(rows), file["uri"])
106
+
107
+ asyncio.run(main())
108
+ ```
109
+
110
+ ### The walk
111
+
112
+ `walk` is the one composition rather than a binding: it spans the catalog
113
+ levels and descends every table the principal may read outside compute, with
114
+ the tables in flight together (bounded by `concurrency`, 32 by default), and
115
+ yields each table file.
116
+
117
+ ```python
118
+ async for file in pqbench.walk(workspace):
119
+ print(file["uri"])
120
+ ```
121
+
122
+ `catalog=` descends one catalog instead of every readable one. A table that
123
+ cannot be read is skipped (logged at `debug`), so a mixed metastore still
124
+ walks; records stream, so the walk holds no more than a bounded queue of them.
125
+
126
+ The Unity Catalog API limits requests per endpoint and per workspace, so its
127
+ calls are paced at `requests_per_second` — 15, one bucket per endpoint, and `0`
128
+ turns pacing off. The storage reads are not paced: object storage has no such
129
+ limit and retries its own. Anything skipped is counted in a `warning` when the
130
+ walk ends, so a rate limit shows up as a count rather than as silently missing
131
+ files.
132
+
133
+ `bytemass` is the last command: it reads what `file.list.list` yields — a
134
+ path or URI per file — and returns one `MassRow` per column chunk.
135
+
136
+ ```python
137
+ rows = await pqbench.native.bytemass.bytemass(
138
+ lake, pqbench.native.BytemassRequest(inputs=["s3://…/part-00000.parquet"])
139
+ )
140
+ ```
141
+
142
+ [`samples/walk.py`](samples/walk.py) is a runnable walk over the **whole
143
+ metastore**: it calls `walk`, spans every catalog and schema, descends the one
144
+ catalog the principal may read outside compute, and prints each table file.
145
+
146
+ ```sh
147
+ PQB_ENDPOINT=https://<workspace>.cloud.databricks.com PQB_TOKEN=<bearer> \
148
+ AWS_REGION=us-east-2 python samples/walk.py
149
+ ```
150
+
151
+ ## Configuration
152
+
153
+ `Config(endpoint, token=None, dialect=Dialect.UNITY)` carries the endpoint, the
154
+ bearer, and the dialect (`UNITY` or `ICEBERG`); `Layer(endpoint=None,
155
+ token=None, dialect=None).resolve()` combines configuration layers into a
156
+ `Config`.
157
+
158
+ `credentials` is a `Credentials` record — what `credentials.get` returns — or
159
+ the mapping of the same fields; a read given none falls back to the process
160
+ environment. A request's `env` is the library's `BTreeMap<String, String>`,
161
+ passed straight through. A `BytemassRequest` names `inputs` (paths or glob
162
+ masks), `env` (the `AWS_*` options a remote read uses) and `indexes` (load the
163
+ parquet page indexes too, one extra range per file).
164
+
165
+ ## Publish to PyPI
166
+
167
+ [`.github/workflows/publish-pypi.yml`](../.github/workflows/publish-pypi.yml)
168
+ builds manylinux, macOS, and Windows wheels with
169
+ [maturin](https://www.maturin.rs/) and uploads them with
170
+ [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) — no token
171
+ is stored. It has its own dispatch, after the matching tag is released:
172
+ `gh workflow run publish-pypi.yml`, or **Publish Python package** from the
173
+ Actions tab. Publishing is separate because PyPI names the workflow file that
174
+ may upload, and its trusted publisher does not accept a job defined in a
175
+ workflow that another workflow calls.
176
+
177
+ Its version is the workspace's: `scripts/version.sh sync` writes
178
+ `[workspace.package] version` from `Cargo.toml` into `python/Cargo.toml` before
179
+ maturin reads it, and `make check-version` fails when the two disagree —
180
+ `python/` is its own crate and cannot inherit it.
181
+
182
+ Before the first upload:
183
+
184
+ 1. On PyPI, add a pending publisher for this repository: owner `pqbench`,
185
+ project `pqbench`, workflow `publish-pypi.yml`, environment `pypi`.
186
+ 2. In the GitHub repository settings, create an environment named `pypi`.
187
+ 3. Run the **Publish Python package** workflow.
188
+
@@ -0,0 +1,169 @@
1
+ # pqbench
2
+
3
+ PyO3 bindings for [pqbench](https://github.com/pqbench/pqbench). `pip install`
4
+ builds a native wheel; a separate `pqbench` binary is not required. Two
5
+ surfaces, one wheel:
6
+
7
+ * **`pqbench.native`** — the commands: one function per library path, with the
8
+ same arguments. Every name is the Rust function of the same path.
9
+ * **`pqbench`** — the ergonomic surface over them: the client handles every
10
+ command takes (`Config`, `Dialect`, `Lake`, `Layer`, `Workspace`) and `walk`,
11
+ the whole-metastore walk composed once.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pip install pqbench
17
+ ```
18
+
19
+ The wheel carries every storage backend (`delta-s3`, `iceberg-s3`), so `s3://`
20
+ reads and Delta/Iceberg tables work out of the box. A storage read
21
+ authenticates with the `credentials=` argument (a `Credentials` record) or,
22
+ for `bytemass`, with `env` on its request — or with the process environment.
23
+
24
+ From a checkout:
25
+
26
+ ```sh
27
+ pip install maturin
28
+ maturin develop --manifest-path python/Cargo.toml
29
+ ```
30
+
31
+ ## The commands
32
+
33
+ Each command is the library function of the same path, under `pqbench.native`,
34
+ and takes the handles and the resource's name like the Rust signature: the name
35
+ is the AIP resource name a parent record carries, so a record feeds the next
36
+ command as it is. `credentials.get` takes a table's name and returns its lease,
37
+ the `Credentials` record a storage read takes, and a record is its model's
38
+ class: `record["uri"]`, `record.uri` and `record.asdict()` agree.
39
+
40
+ They are the walk's own pipeline — metastore, catalog, schema, table,
41
+ credentials, partition, file, bytemass — and they stop at `bytemass`: the
42
+ commands downstream of a byte-mass stream (`viz`, `diff`, `profile`,
43
+ `experiment`), the standalone codec benchmarks (`lz`, `compression`) and
44
+ `skill` are separate tools, and are not bound.
45
+
46
+ ```python
47
+ import asyncio
48
+ import pqbench
49
+ import pqbench.native
50
+
51
+ async def main():
52
+ workspace = pqbench.Workspace(pqbench.Config("https://…", token="…"))
53
+ lake = pqbench.Lake(workspace)
54
+
55
+ # The walk, one function per resource. The I/O verbs are awaitable.
56
+ await pqbench.native.metastore.get.get(workspace)
57
+ await pqbench.native.catalog.get.get(workspace, "catalogs/dbx_samples")
58
+ await pqbench.native.catalog.list.list(workspace)
59
+ await pqbench.native.schema.list.list(workspace, "catalogs/dbx_samples")
60
+ refs = await pqbench.native.table.list.list(
61
+ workspace, "catalogs/dbx_samples/schemas/nyctaxi"
62
+ )
63
+ await pqbench.native.credentials.check.check(
64
+ workspace, "catalogs/dbx_samples/schemas/nyctaxi/tables/trips"
65
+ )
66
+ # `credentials.get.get` is name -> lease: the lease is what a storage read
67
+ # takes, from the table's name alone.
68
+ credentials = await pqbench.native.credentials.get.get(workspace, refs[0]["name"])
69
+ table = await pqbench.native.table.get.get(
70
+ workspace,
71
+ lake,
72
+ refs[0]["name"], refs[0].get("uri"), credentials,
73
+ )
74
+
75
+ # `partition.list.list` and `file.list.list` stream (async iterators); the
76
+ # single-value verbs are awaitable.
77
+ async for partition in pqbench.native.partition.list.list(
78
+ lake, table["name"], table["uri"], table.get("credentials"), 86_400_000
79
+ ):
80
+ async for file in pqbench.native.file.list.list(
81
+ lake, partition["uri"], partition.get("credentials"), partition["commit_versions"]
82
+ ):
83
+ # The pipeline stops here: `bytemass` measures the file's bytes.
84
+ rows = await pqbench.native.bytemass.bytemass(
85
+ lake, pqbench.native.BytemassRequest(inputs=[file["uri"]])
86
+ )
87
+ print(len(rows), file["uri"])
88
+
89
+ asyncio.run(main())
90
+ ```
91
+
92
+ ### The walk
93
+
94
+ `walk` is the one composition rather than a binding: it spans the catalog
95
+ levels and descends every table the principal may read outside compute, with
96
+ the tables in flight together (bounded by `concurrency`, 32 by default), and
97
+ yields each table file.
98
+
99
+ ```python
100
+ async for file in pqbench.walk(workspace):
101
+ print(file["uri"])
102
+ ```
103
+
104
+ `catalog=` descends one catalog instead of every readable one. A table that
105
+ cannot be read is skipped (logged at `debug`), so a mixed metastore still
106
+ walks; records stream, so the walk holds no more than a bounded queue of them.
107
+
108
+ The Unity Catalog API limits requests per endpoint and per workspace, so its
109
+ calls are paced at `requests_per_second` — 15, one bucket per endpoint, and `0`
110
+ turns pacing off. The storage reads are not paced: object storage has no such
111
+ limit and retries its own. Anything skipped is counted in a `warning` when the
112
+ walk ends, so a rate limit shows up as a count rather than as silently missing
113
+ files.
114
+
115
+ `bytemass` is the last command: it reads what `file.list.list` yields — a
116
+ path or URI per file — and returns one `MassRow` per column chunk.
117
+
118
+ ```python
119
+ rows = await pqbench.native.bytemass.bytemass(
120
+ lake, pqbench.native.BytemassRequest(inputs=["s3://…/part-00000.parquet"])
121
+ )
122
+ ```
123
+
124
+ [`samples/walk.py`](samples/walk.py) is a runnable walk over the **whole
125
+ metastore**: it calls `walk`, spans every catalog and schema, descends the one
126
+ catalog the principal may read outside compute, and prints each table file.
127
+
128
+ ```sh
129
+ PQB_ENDPOINT=https://<workspace>.cloud.databricks.com PQB_TOKEN=<bearer> \
130
+ AWS_REGION=us-east-2 python samples/walk.py
131
+ ```
132
+
133
+ ## Configuration
134
+
135
+ `Config(endpoint, token=None, dialect=Dialect.UNITY)` carries the endpoint, the
136
+ bearer, and the dialect (`UNITY` or `ICEBERG`); `Layer(endpoint=None,
137
+ token=None, dialect=None).resolve()` combines configuration layers into a
138
+ `Config`.
139
+
140
+ `credentials` is a `Credentials` record — what `credentials.get` returns — or
141
+ the mapping of the same fields; a read given none falls back to the process
142
+ environment. A request's `env` is the library's `BTreeMap<String, String>`,
143
+ passed straight through. A `BytemassRequest` names `inputs` (paths or glob
144
+ masks), `env` (the `AWS_*` options a remote read uses) and `indexes` (load the
145
+ parquet page indexes too, one extra range per file).
146
+
147
+ ## Publish to PyPI
148
+
149
+ [`.github/workflows/publish-pypi.yml`](../.github/workflows/publish-pypi.yml)
150
+ builds manylinux, macOS, and Windows wheels with
151
+ [maturin](https://www.maturin.rs/) and uploads them with
152
+ [PyPI trusted publishing](https://docs.pypi.org/trusted-publishers/) — no token
153
+ is stored. It has its own dispatch, after the matching tag is released:
154
+ `gh workflow run publish-pypi.yml`, or **Publish Python package** from the
155
+ Actions tab. Publishing is separate because PyPI names the workflow file that
156
+ may upload, and its trusted publisher does not accept a job defined in a
157
+ workflow that another workflow calls.
158
+
159
+ Its version is the workspace's: `scripts/version.sh sync` writes
160
+ `[workspace.package] version` from `Cargo.toml` into `python/Cargo.toml` before
161
+ maturin reads it, and `make check-version` fails when the two disagree —
162
+ `python/` is its own crate and cannot inherit it.
163
+
164
+ Before the first upload:
165
+
166
+ 1. On PyPI, add a pending publisher for this repository: owner `pqbench`,
167
+ project `pqbench`, workflow `publish-pypi.yml`, environment `pypi`.
168
+ 2. In the GitHub repository settings, create an environment named `pypi`.
169
+ 3. Run the **Publish Python package** workflow.
@@ -0,0 +1,53 @@
1
+ [package]
2
+ name = "pqbench"
3
+ version.workspace = true
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+ authors.workspace = true
8
+ repository.workspace = true
9
+ description = "A benchmarking library for Parquet files (in-memory read/write throughput, compression ratio)."
10
+
11
+ [features]
12
+ default = []
13
+ # S3 object reads. Without it, `s3://` fails at runtime, not at compile time.
14
+ # reqwest signs a HEAD to read `x-amz-storage-class`, which object_store drops.
15
+ aws = ["dep:object_store", "object_store/aws", "dep:async-trait"]
16
+ # Delta snapshot resolution requires Rust 1.94.1 or newer.
17
+ delta = ["dep:deltalake", "dep:futures"]
18
+ delta-s3 = ["delta", "deltalake/s3", "aws"]
19
+ # Iceberg snapshot planning reads metadata JSON and Avro manifests.
20
+ iceberg = ["dep:apache-avro"]
21
+ iceberg-s3 = ["iceberg", "aws"]
22
+
23
+ [dependencies]
24
+ snappy_src = "0.2.5"
25
+ zstd = "0.13"
26
+ lz4 = "1"
27
+ flate2 = { version = "1", default-features = false, features = ["zlib"] }
28
+ glob = "0.3"
29
+ # Codecs are enabled so `read_sample` can decode compressed files; without
30
+ # them parquet-rs fails at runtime (`Disabled feature at compile time`).
31
+ parquet = { version = "59", default-features = false, features = ["snap", "brotli", "lz4", "zstd", "flate2-rust_backend"] }
32
+ # Native roots only: the OS certificate store, where internal/private CAs live.
33
+ # `rustls-tls-native-roots` also brings the rustls backend and the ring
34
+ # provider, so the client still has TLS without `aws` (object_store is what
35
+ # otherwise enables them).
36
+ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "json"] }
37
+ async-trait = { version = "0.1", optional = true }
38
+ bytes = "1"
39
+ parquet-format-safe = "0.2.4"
40
+ serde = { version = "1.0.229", features = ["derive"] }
41
+ serde_json = "1.0.151"
42
+ deltalake = { version = "1.0", default-features = false, features = ["rustls"], optional = true }
43
+ futures = { version = "0.3", optional = true }
44
+ futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
45
+ url = "2"
46
+ # Must match delta-rs's ObjectStore trait version: stores cross that internal
47
+ # boundary. Keep the pin until delta-rs upgrades its dependency.
48
+ object_store = { version = "=0.13.2", optional = true }
49
+ apache-avro = { version = "0.17", optional = true }
50
+
51
+ [dev-dependencies]
52
+ tempfile = "3"
53
+ tokio = { version = "1", features = ["rt", "macros"] }
@@ -0,0 +1,112 @@
1
+ //! Shared codec-sweep mechanism for the `lz` and `compression` commands.
2
+ //!
3
+ //! The two commands own their own request types ([`crate::lz::LzRequest`],
4
+ //! [`crate::compression::CompressionRequest`]); this module holds only the
5
+ //! resolution they both need: parsing `codec@level` specs, the analytics
6
+ //! config, and the raw pass count. It deliberately carries no request type of
7
+ //! its own, so neither command depends on the other. Its [`Error`] is the error
8
+ //! type both sweeps and their renderers return.
9
+
10
+ use crate::codecs::{Codec, CodecImpl, Error as CodecError};
11
+ use crate::stats;
12
+
13
+ /// The measurement decisions a request resolves to: the codec×level set, the
14
+ /// analytics config, and the raw pass count the upstream wants.
15
+ pub(crate) struct Sweep {
16
+ pub(crate) codec_configs: Vec<(Codec, u8)>,
17
+ pub(crate) stats_config: stats::Config,
18
+ pub(crate) passes: u32,
19
+ }
20
+
21
+ /// Resolve the shared sweep decisions from a command's arguments.
22
+ pub(crate) fn resolve(
23
+ codec_specs: &[String],
24
+ samples: u32,
25
+ warmup_iterations: u32,
26
+ mode: stats::Mode,
27
+ ) -> Result<Sweep, Error> {
28
+ Ok(Sweep {
29
+ codec_configs: parse_codec_configs(codec_specs)?,
30
+ stats_config: stats::Config {
31
+ warmup_iterations: warmup_iterations as usize,
32
+ mode,
33
+ },
34
+ passes: samples + warmup_iterations,
35
+ })
36
+ }
37
+
38
+ /// Errors from a codec sweep: spec parsing, the codec/parquet layers, or
39
+ /// rendering.
40
+ #[derive(Debug)]
41
+ #[non_exhaustive]
42
+ pub enum Error {
43
+ /// A `codec@level` spec could not be parsed.
44
+ Spec(String),
45
+ /// The codec layer failed.
46
+ Codec(CodecError),
47
+ /// The parquet layer failed.
48
+ Parquet(crate::third_party::parquet::api::Error),
49
+ /// A renderer could not serialize its output.
50
+ Json(serde_json::Error),
51
+ }
52
+
53
+ impl std::fmt::Display for Error {
54
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55
+ match self {
56
+ Error::Spec(e) => write!(f, "{e}"),
57
+ Error::Codec(e) => write!(f, "{e}"),
58
+ Error::Parquet(e) => write!(f, "{e}"),
59
+ Error::Json(e) => write!(f, "json: {e}"),
60
+ }
61
+ }
62
+ }
63
+
64
+ impl std::error::Error for Error {}
65
+
66
+ impl From<CodecError> for Error {
67
+ fn from(e: CodecError) -> Self {
68
+ Error::Codec(e)
69
+ }
70
+ }
71
+
72
+ impl From<crate::third_party::parquet::api::Error> for Error {
73
+ fn from(e: crate::third_party::parquet::api::Error) -> Self {
74
+ Error::Parquet(e)
75
+ }
76
+ }
77
+
78
+ impl From<serde_json::Error> for Error {
79
+ fn from(e: serde_json::Error) -> Self {
80
+ Error::Json(e)
81
+ }
82
+ }
83
+
84
+ /// Parse the `codec@level` specs; empty selects every wired codec at its first
85
+ /// level.
86
+ fn parse_codec_configs(specs: &[String]) -> Result<Vec<(Codec, u8)>, Error> {
87
+ if specs.is_empty() {
88
+ return Ok(Codec::all().map(|c| (c, default_level(c))).collect());
89
+ }
90
+ specs.iter().map(|spec| parse_spec(spec)).collect()
91
+ }
92
+
93
+ /// One `codec@level` spec. The `@level` part is optional and defaults to the
94
+ /// codec's lowest level.
95
+ fn parse_spec(spec: &str) -> Result<(Codec, u8), Error> {
96
+ let Some((name, level)) = spec.split_once('@') else {
97
+ let codec = parse_codec(spec)?;
98
+ return Ok((codec, default_level(codec)));
99
+ };
100
+ let level = level
101
+ .parse::<u8>()
102
+ .map_err(|_| Error::Spec(format!("bad level in {spec}")))?;
103
+ Ok((parse_codec(name)?, level))
104
+ }
105
+
106
+ fn parse_codec(name: &str) -> Result<Codec, Error> {
107
+ Codec::from_name(name).ok_or_else(|| Error::Spec(format!("unknown codec: {name}")))
108
+ }
109
+
110
+ fn default_level(codec: Codec) -> u8 {
111
+ codec.level_range().first_level as u8
112
+ }