rete-graph 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.
- rete_graph-0.1.0/Cargo.toml +43 -0
- rete_graph-0.1.0/PKG-INFO +153 -0
- rete_graph-0.1.0/README.md +124 -0
- rete_graph-0.1.0/clients/python/.gitignore +6 -0
- rete_graph-0.1.0/clients/python/Cargo.lock +1274 -0
- rete_graph-0.1.0/clients/python/Cargo.toml +29 -0
- rete_graph-0.1.0/clients/python/README.md +124 -0
- rete_graph-0.1.0/clients/python/examples/tutorial.ipynb +531 -0
- rete_graph-0.1.0/clients/python/src/lib.rs +430 -0
- rete_graph-0.1.0/clients/python/src/readers.rs +281 -0
- rete_graph-0.1.0/clients/python/tests/conftest.py +88 -0
- rete_graph-0.1.0/clients/python/tests/test_rete_graph.py +271 -0
- rete_graph-0.1.0/crates/rete-core/Cargo.toml +67 -0
- rete_graph-0.1.0/crates/rete-core/LICENSE +201 -0
- rete_graph-0.1.0/crates/rete-core/README.md +53 -0
- rete_graph-0.1.0/crates/rete-core/examples/coherence_bench.rs +271 -0
- rete_graph-0.1.0/crates/rete-core/examples/parallel_bench.rs +238 -0
- rete_graph-0.1.0/crates/rete-core/examples/qbench.rs +250 -0
- rete_graph-0.1.0/crates/rete-core/src/bgp.rs +1285 -0
- rete_graph-0.1.0/crates/rete-core/src/block_cache.rs +424 -0
- rete_graph-0.1.0/crates/rete-core/src/dict.rs +778 -0
- rete_graph-0.1.0/crates/rete-core/src/dictionary.rs +424 -0
- rete_graph-0.1.0/crates/rete-core/src/file.rs +4380 -0
- rete_graph-0.1.0/crates/rete-core/src/geo.rs +974 -0
- rete_graph-0.1.0/crates/rete-core/src/header.rs +594 -0
- rete_graph-0.1.0/crates/rete-core/src/index.rs +1011 -0
- rete_graph-0.1.0/crates/rete-core/src/ingest.rs +854 -0
- rete_graph-0.1.0/crates/rete-core/src/lib.rs +187 -0
- rete_graph-0.1.0/crates/rete-core/src/meta.rs +1463 -0
- rete_graph-0.1.0/crates/rete-core/src/parallel.rs +207 -0
- rete_graph-0.1.0/crates/rete-core/src/pyramid.rs +452 -0
- rete_graph-0.1.0/crates/rete-core/src/reach.rs +130 -0
- rete_graph-0.1.0/crates/rete-core/src/reader.rs +158 -0
- rete_graph-0.1.0/crates/rete-core/src/reason.rs +598 -0
- rete_graph-0.1.0/crates/rete-core/src/results.rs +209 -0
- rete_graph-0.1.0/crates/rete-core/src/row.rs +506 -0
- rete_graph-0.1.0/crates/rete-core/src/schema_pyramid.rs +1149 -0
- rete_graph-0.1.0/crates/rete-core/src/service.rs +259 -0
- rete_graph-0.1.0/crates/rete-core/src/shacl.rs +1722 -0
- rete_graph-0.1.0/crates/rete-core/src/sparql/aggregate.rs +371 -0
- rete_graph-0.1.0/crates/rete-core/src/sparql/eval.rs +1664 -0
- rete_graph-0.1.0/crates/rete-core/src/sparql/expr.rs +1256 -0
- rete_graph-0.1.0/crates/rete-core/src/sparql/lower.rs +1251 -0
- rete_graph-0.1.0/crates/rete-core/src/sparql/path.rs +553 -0
- rete_graph-0.1.0/crates/rete-core/src/sparql/ql.rs +601 -0
- rete_graph-0.1.0/crates/rete-core/src/sparql.rs +1995 -0
- rete_graph-0.1.0/crates/rete-core/src/terms.rs +338 -0
- rete_graph-0.1.0/crates/rete-core/src/text_index.rs +343 -0
- rete_graph-0.1.0/crates/rete-core/src/tiling.rs +212 -0
- rete_graph-0.1.0/crates/rete-core/src/triples.rs +586 -0
- rete_graph-0.1.0/crates/rete-core/src/varint.rs +56 -0
- rete_graph-0.1.0/crates/rete-core/tests/community_eval.rs +151 -0
- rete_graph-0.1.0/crates/rete-core/tests/compatibility_v1.rs +14 -0
- rete_graph-0.1.0/crates/rete-core/tests/fixtures/v1/minimal.rete +0 -0
- rete_graph-0.1.0/crates/rete-core/tests/fixtures/v1/source.nq +3 -0
- rete_graph-0.1.0/crates/rete-core/tests/properties.rs +194 -0
- rete_graph-0.1.0/crates/rete-core/tests/public_api.rs +41 -0
- rete_graph-0.1.0/crates/rete-core/tests/ranged.rs +679 -0
- rete_graph-0.1.0/crates/rete-core/tests/robustness.rs +175 -0
- rete_graph-0.1.0/crates/rete-core/tests/roundtrip.rs +185 -0
- rete_graph-0.1.0/crates/rete-core/tests/service_federation.rs +164 -0
- rete_graph-0.1.0/crates/rete-core/tests/shacl_core.rs +285 -0
- rete_graph-0.1.0/crates/rete-core/tests/sparql_integration.rs +1085 -0
- rete_graph-0.1.0/crates/rete-core/tests/sparql_nondeterministic.rs +78 -0
- rete_graph-0.1.0/pyproject.toml +43 -0
- rete_graph-0.1.0/python/rete_graph/__init__.py +594 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[workspace]
|
|
2
|
+
resolver = "2"
|
|
3
|
+
members = [
|
|
4
|
+
"crates/rete-core",
|
|
5
|
+
"crates/rete-cli",
|
|
6
|
+
"crates/rete-wasm",
|
|
7
|
+
"crates/docgen",
|
|
8
|
+
"crates/bench",
|
|
9
|
+
]
|
|
10
|
+
# The cargo-fuzz harness is its own (nightly, instrumented) build — never part of
|
|
11
|
+
# the normal workspace build/test. See fuzz/Cargo.toml.
|
|
12
|
+
# Language clients under clients/ likewise own their toolchains: the Python
|
|
13
|
+
# package (PyO3 + maturin) builds and locks independently so the workspace
|
|
14
|
+
# release gates (--locked checks, feature matrix, coverage floors) stay
|
|
15
|
+
# untouched by binding-only changes. Future clients (R, Go) follow the same
|
|
16
|
+
# pattern — clients/ consumes crates/, never the other way around.
|
|
17
|
+
exclude = ["fuzz", "clients/python"]
|
|
18
|
+
|
|
19
|
+
[workspace.package]
|
|
20
|
+
version = "1.0.0-rc.1"
|
|
21
|
+
edition = "2021"
|
|
22
|
+
license = "Apache-2.0"
|
|
23
|
+
repository = "https://github.com/caviri/rete"
|
|
24
|
+
homepage = "https://caviri.github.io/rete/"
|
|
25
|
+
readme = "README.md"
|
|
26
|
+
rust-version = "1.87"
|
|
27
|
+
|
|
28
|
+
[workspace.dependencies]
|
|
29
|
+
# RDF model + SPARQL, reused rather than reinvented (see docs/SPEC.md §2).
|
|
30
|
+
# `rdf-star` enables quoted triples (`<< s p o >>`) in the parsers and SPARQL.
|
|
31
|
+
oxrdf = { version = "0.2", features = ["rdf-star"] }
|
|
32
|
+
oxttl = { version = "0.1", features = ["rdf-star"] }
|
|
33
|
+
oxrdfxml = "0.1"
|
|
34
|
+
spargebra = { version = "0.3", features = ["rdf-star"] }
|
|
35
|
+
serde_json = "1"
|
|
36
|
+
# Range reads from S3/GCS/HTTP and content hashing / compression.
|
|
37
|
+
object_store = "0.11"
|
|
38
|
+
blake3 = "1"
|
|
39
|
+
zstd = "0.13"
|
|
40
|
+
# Errors + CLI.
|
|
41
|
+
anyhow = "1"
|
|
42
|
+
thiserror = "2"
|
|
43
|
+
clap = { version = "4", features = ["derive"] }
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rete-graph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 4 - Beta
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: Intended Audience :: Science/Research
|
|
7
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Rust
|
|
12
|
+
Classifier: Topic :: Database
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
14
|
+
Requires-Dist: pandas>=1.5 ; extra == 'pandas'
|
|
15
|
+
Requires-Dist: rdflib>=6 ; extra == 'rdflib'
|
|
16
|
+
Provides-Extra: pandas
|
|
17
|
+
Provides-Extra: rdflib
|
|
18
|
+
Summary: Query local and remote .rete graph files with SPARQL — Python bindings for the rete engine.
|
|
19
|
+
Keywords: rdf,sparql,graph,database,linked-data,range-requests
|
|
20
|
+
Home-Page: https://caviri.github.io/rete/
|
|
21
|
+
Author: Carlos Vivar Rios
|
|
22
|
+
License: Apache-2.0
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
25
|
+
Project-URL: Homepage, https://caviri.github.io/rete/
|
|
26
|
+
Project-URL: Playground, https://caviri.github.io/rete/playground.html
|
|
27
|
+
Project-URL: Repository, https://github.com/caviri/rete
|
|
28
|
+
|
|
29
|
+
# rete-graph — Python client for `.rete` files
|
|
30
|
+
|
|
31
|
+
Query **local and remote `.rete` graph files with SPARQL** from Python. A
|
|
32
|
+
`.rete` file is a single, immutable, range-queryable RDF graph file
|
|
33
|
+
([rete](https://github.com/caviri/rete)): drop it on any HTTP host that
|
|
34
|
+
supports `Range` requests and query it in place — the client fetches only the
|
|
35
|
+
byte ranges a query touches, never the whole file.
|
|
36
|
+
|
|
37
|
+
These are native bindings (PyO3) to the same Rust engine that powers the rete
|
|
38
|
+
CLI and the [browser playground](https://caviri.github.io/rete/playground.html).
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
uv pip install rete-graph # or: pip install rete-graph
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
A runnable tour with captured outputs lives in
|
|
47
|
+
[`examples/tutorial.ipynb`](examples/tutorial.ipynb).
|
|
48
|
+
|
|
49
|
+
## Use
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import rete_graph as rete
|
|
53
|
+
|
|
54
|
+
# Remote: lazy HTTP range reads — a selective query over a multi-GB file
|
|
55
|
+
# fetches KBs. Any S3/R2/CDN/GitHub URL with Range + CORS works.
|
|
56
|
+
g = rete.open("https://data.graphplaza.com/boe/boe.rete")
|
|
57
|
+
|
|
58
|
+
rows = g.query("""
|
|
59
|
+
SELECT ?s ?label WHERE {
|
|
60
|
+
?s <http://www.w3.org/2000/01/rdf-schema#label> ?label
|
|
61
|
+
} LIMIT 10
|
|
62
|
+
""")
|
|
63
|
+
for row in rows:
|
|
64
|
+
print(row["s"].value, "→", row["label"].to_python())
|
|
65
|
+
|
|
66
|
+
print(g.stats()) # {'fileLength': ..., 'bytes': ..., 'requests': ...}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`query()` returns `{variable: Term}` rows for SELECT, a `bool` for ASK, and
|
|
70
|
+
`(s, p, o)` triples for CONSTRUCT/DESCRIBE. A `Term` carries `.kind` / `.value`
|
|
71
|
+
/ `.datatype` / `.lang`, plus `.to_python()` (int/float/bool for common XSD
|
|
72
|
+
types) and `.n3`.
|
|
73
|
+
|
|
74
|
+
More:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
g = rete.open("data/example.rete") # local file, opened lazily too
|
|
78
|
+
g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl")
|
|
79
|
+
g = rete.open(rete.build(rdflib_graph)) # or straight from an rdflib Graph/Dataset
|
|
80
|
+
g = rete.open(url, headers={"Authorization": "Bearer ..."}) # authed hosts
|
|
81
|
+
|
|
82
|
+
g.examples() # SPARQL examples embedded in the file — run as-is
|
|
83
|
+
g.query(q, reason=True) # OWL 2 QL entailment (query rewriting)
|
|
84
|
+
g.query_df("SELECT ...") # pandas DataFrame (pip install rete-graph[pandas])
|
|
85
|
+
g.schema() # class/predicate profile
|
|
86
|
+
g.prefix_search("Berl") # label autocomplete
|
|
87
|
+
g.text_search("volcano eruption") # full-text (files built with --text-index)
|
|
88
|
+
g.info(), g.quads, g.content_hash()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
SPARQL 1.1 `SERVICE` federation works out of the box — join a `.rete` file
|
|
92
|
+
against any public SPARQL endpoint in one query.
|
|
93
|
+
|
|
94
|
+
### Prepare a `.rete` step by step
|
|
95
|
+
|
|
96
|
+
The lazy `Builder` configures a build — sources, embedded Dataset Card,
|
|
97
|
+
pyramid, full-text index — then `run()` / `export()`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
builder = (
|
|
101
|
+
rete.Builder()
|
|
102
|
+
.add_file("people.ttl") # or .add(text) / .add(rdflib_graph)
|
|
103
|
+
.card(title="People", license="CC0-1.0", created="2026-07-16")
|
|
104
|
+
.pyramid(algo="louvain") # or "types", or .pyramid(False)
|
|
105
|
+
.text_index()
|
|
106
|
+
)
|
|
107
|
+
builder.run() # bytes; counts in builder.stats
|
|
108
|
+
builder.export("people.rete") # immutable, host-anywhere file
|
|
109
|
+
builder.graph().card() # read the embedded card back
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See the "Python: build a .rete" tutorial in the docs for the full walkthrough.
|
|
113
|
+
|
|
114
|
+
### Custom storage (fsspec, S3, GCS, …)
|
|
115
|
+
|
|
116
|
+
Anything that can serve byte ranges can back a graph — pass an object with
|
|
117
|
+
`read_at(offset, length) -> bytes` and `len()`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import fsspec, rete_graph as rete
|
|
121
|
+
|
|
122
|
+
class FsspecReader:
|
|
123
|
+
def __init__(self, url, **kw):
|
|
124
|
+
self.f = fsspec.open(url, "rb", **kw).open()
|
|
125
|
+
self.size = self.f.size
|
|
126
|
+
def len(self):
|
|
127
|
+
return self.size
|
|
128
|
+
def read_at(self, offset, length):
|
|
129
|
+
self.f.seek(offset)
|
|
130
|
+
return self.f.read(length)
|
|
131
|
+
|
|
132
|
+
g = rete.open(reader=FsspecReader("s3://my-bucket/data.rete", anon=False))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Building from source
|
|
136
|
+
|
|
137
|
+
The package is a [maturin](https://maturin.rs) project; the repo convention is
|
|
138
|
+
to build in Docker (nothing on the host):
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
# from the repository root
|
|
142
|
+
docker run --rm -v "$PWD":/io ghcr.io/pyo3/maturin build \
|
|
143
|
+
--release -m clients/python/Cargo.toml --out clients/python/dist
|
|
144
|
+
|
|
145
|
+
uv pip install clients/python/dist/*.whl
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Tests: `uv pip install pytest && pytest clients/python/tests`.
|
|
149
|
+
|
|
150
|
+
Wheels are abi3 (`>= 3.9`): one wheel per platform covers every CPython.
|
|
151
|
+
Releases are published to PyPI by
|
|
152
|
+
`.github/workflows/python-client-publish.yml` on a `py-v*` tag.
|
|
153
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# rete-graph — Python client for `.rete` files
|
|
2
|
+
|
|
3
|
+
Query **local and remote `.rete` graph files with SPARQL** from Python. A
|
|
4
|
+
`.rete` file is a single, immutable, range-queryable RDF graph file
|
|
5
|
+
([rete](https://github.com/caviri/rete)): drop it on any HTTP host that
|
|
6
|
+
supports `Range` requests and query it in place — the client fetches only the
|
|
7
|
+
byte ranges a query touches, never the whole file.
|
|
8
|
+
|
|
9
|
+
These are native bindings (PyO3) to the same Rust engine that powers the rete
|
|
10
|
+
CLI and the [browser playground](https://caviri.github.io/rete/playground.html).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
uv pip install rete-graph # or: pip install rete-graph
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A runnable tour with captured outputs lives in
|
|
19
|
+
[`examples/tutorial.ipynb`](examples/tutorial.ipynb).
|
|
20
|
+
|
|
21
|
+
## Use
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import rete_graph as rete
|
|
25
|
+
|
|
26
|
+
# Remote: lazy HTTP range reads — a selective query over a multi-GB file
|
|
27
|
+
# fetches KBs. Any S3/R2/CDN/GitHub URL with Range + CORS works.
|
|
28
|
+
g = rete.open("https://data.graphplaza.com/boe/boe.rete")
|
|
29
|
+
|
|
30
|
+
rows = g.query("""
|
|
31
|
+
SELECT ?s ?label WHERE {
|
|
32
|
+
?s <http://www.w3.org/2000/01/rdf-schema#label> ?label
|
|
33
|
+
} LIMIT 10
|
|
34
|
+
""")
|
|
35
|
+
for row in rows:
|
|
36
|
+
print(row["s"].value, "→", row["label"].to_python())
|
|
37
|
+
|
|
38
|
+
print(g.stats()) # {'fileLength': ..., 'bytes': ..., 'requests': ...}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`query()` returns `{variable: Term}` rows for SELECT, a `bool` for ASK, and
|
|
42
|
+
`(s, p, o)` triples for CONSTRUCT/DESCRIBE. A `Term` carries `.kind` / `.value`
|
|
43
|
+
/ `.datatype` / `.lang`, plus `.to_python()` (int/float/bool for common XSD
|
|
44
|
+
types) and `.n3`.
|
|
45
|
+
|
|
46
|
+
More:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
g = rete.open("data/example.rete") # local file, opened lazily too
|
|
50
|
+
g = rete.open(rete.build(nt_text)) # build a graph in memory ("nt"/"nq"/"ttl")
|
|
51
|
+
g = rete.open(rete.build(rdflib_graph)) # or straight from an rdflib Graph/Dataset
|
|
52
|
+
g = rete.open(url, headers={"Authorization": "Bearer ..."}) # authed hosts
|
|
53
|
+
|
|
54
|
+
g.examples() # SPARQL examples embedded in the file — run as-is
|
|
55
|
+
g.query(q, reason=True) # OWL 2 QL entailment (query rewriting)
|
|
56
|
+
g.query_df("SELECT ...") # pandas DataFrame (pip install rete-graph[pandas])
|
|
57
|
+
g.schema() # class/predicate profile
|
|
58
|
+
g.prefix_search("Berl") # label autocomplete
|
|
59
|
+
g.text_search("volcano eruption") # full-text (files built with --text-index)
|
|
60
|
+
g.info(), g.quads, g.content_hash()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
SPARQL 1.1 `SERVICE` federation works out of the box — join a `.rete` file
|
|
64
|
+
against any public SPARQL endpoint in one query.
|
|
65
|
+
|
|
66
|
+
### Prepare a `.rete` step by step
|
|
67
|
+
|
|
68
|
+
The lazy `Builder` configures a build — sources, embedded Dataset Card,
|
|
69
|
+
pyramid, full-text index — then `run()` / `export()`:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
builder = (
|
|
73
|
+
rete.Builder()
|
|
74
|
+
.add_file("people.ttl") # or .add(text) / .add(rdflib_graph)
|
|
75
|
+
.card(title="People", license="CC0-1.0", created="2026-07-16")
|
|
76
|
+
.pyramid(algo="louvain") # or "types", or .pyramid(False)
|
|
77
|
+
.text_index()
|
|
78
|
+
)
|
|
79
|
+
builder.run() # bytes; counts in builder.stats
|
|
80
|
+
builder.export("people.rete") # immutable, host-anywhere file
|
|
81
|
+
builder.graph().card() # read the embedded card back
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
See the "Python: build a .rete" tutorial in the docs for the full walkthrough.
|
|
85
|
+
|
|
86
|
+
### Custom storage (fsspec, S3, GCS, …)
|
|
87
|
+
|
|
88
|
+
Anything that can serve byte ranges can back a graph — pass an object with
|
|
89
|
+
`read_at(offset, length) -> bytes` and `len()`:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import fsspec, rete_graph as rete
|
|
93
|
+
|
|
94
|
+
class FsspecReader:
|
|
95
|
+
def __init__(self, url, **kw):
|
|
96
|
+
self.f = fsspec.open(url, "rb", **kw).open()
|
|
97
|
+
self.size = self.f.size
|
|
98
|
+
def len(self):
|
|
99
|
+
return self.size
|
|
100
|
+
def read_at(self, offset, length):
|
|
101
|
+
self.f.seek(offset)
|
|
102
|
+
return self.f.read(length)
|
|
103
|
+
|
|
104
|
+
g = rete.open(reader=FsspecReader("s3://my-bucket/data.rete", anon=False))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Building from source
|
|
108
|
+
|
|
109
|
+
The package is a [maturin](https://maturin.rs) project; the repo convention is
|
|
110
|
+
to build in Docker (nothing on the host):
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
# from the repository root
|
|
114
|
+
docker run --rm -v "$PWD":/io ghcr.io/pyo3/maturin build \
|
|
115
|
+
--release -m clients/python/Cargo.toml --out clients/python/dist
|
|
116
|
+
|
|
117
|
+
uv pip install clients/python/dist/*.whl
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Tests: `uv pip install pytest && pytest clients/python/tests`.
|
|
121
|
+
|
|
122
|
+
Wheels are abi3 (`>= 3.9`): one wheel per platform covers every CPython.
|
|
123
|
+
Releases are published to PyPI by
|
|
124
|
+
`.github/workflows/python-client-publish.yml` on a `py-v*` tag.
|