haystack-py 0.1.3__py3-none-any.whl
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.
- haystack_py-0.1.3.dist-info/METADATA +446 -0
- haystack_py-0.1.3.dist-info/RECORD +49 -0
- haystack_py-0.1.3.dist-info/WHEEL +4 -0
- haystack_py-0.1.3.dist-info/licenses/LICENSE +21 -0
- hs_py/__init__.py +160 -0
- hs_py/__main__.py +119 -0
- hs_py/_scram_core.py +309 -0
- hs_py/auth.py +406 -0
- hs_py/auth_types.py +107 -0
- hs_py/client.py +581 -0
- hs_py/content_negotiation.py +144 -0
- hs_py/convert.py +83 -0
- hs_py/encoding/__init__.py +37 -0
- hs_py/encoding/csv.py +103 -0
- hs_py/encoding/json.py +667 -0
- hs_py/encoding/scanner.py +638 -0
- hs_py/encoding/trio.py +292 -0
- hs_py/encoding/zinc.py +337 -0
- hs_py/errors.py +57 -0
- hs_py/fastapi_server.py +477 -0
- hs_py/filter/__init__.py +34 -0
- hs_py/filter/ast.py +121 -0
- hs_py/filter/eval.py +178 -0
- hs_py/filter/lexer.py +316 -0
- hs_py/filter/parser.py +168 -0
- hs_py/grid.py +212 -0
- hs_py/kinds.py +302 -0
- hs_py/metrics.py +68 -0
- hs_py/ontology/__init__.py +62 -0
- hs_py/ontology/defs.py +138 -0
- hs_py/ontology/namespace.py +261 -0
- hs_py/ontology/normalize.py +153 -0
- hs_py/ontology/rdf.py +131 -0
- hs_py/ontology/reflect.py +109 -0
- hs_py/ontology/taxonomy.py +114 -0
- hs_py/ops.py +423 -0
- hs_py/py.typed +0 -0
- hs_py/redis_ops.py +127 -0
- hs_py/storage/__init__.py +34 -0
- hs_py/storage/memory.py +338 -0
- hs_py/storage/protocol.py +178 -0
- hs_py/storage/redis.py +789 -0
- hs_py/storage/timescale.py +867 -0
- hs_py/tls.py +311 -0
- hs_py/watch.py +191 -0
- hs_py/ws.py +399 -0
- hs_py/ws_client.py +1432 -0
- hs_py/ws_codec.py +147 -0
- hs_py/ws_server.py +477 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: haystack-py
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Async Project Haystack client and ontology library for Python
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: aiohttp>=3.10
|
|
9
|
+
Requires-Dist: cryptography>=42.0
|
|
10
|
+
Requires-Dist: orjson>=3.10
|
|
11
|
+
Requires-Dist: rdflib>=7.0
|
|
12
|
+
Requires-Dist: websockets>=14.0
|
|
13
|
+
Provides-Extra: all
|
|
14
|
+
Requires-Dist: asyncpg>=0.30; extra == 'all'
|
|
15
|
+
Requires-Dist: fastapi>=0.115; extra == 'all'
|
|
16
|
+
Requires-Dist: redis[hiredis]>=5.2; extra == 'all'
|
|
17
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == 'all'
|
|
18
|
+
Provides-Extra: server
|
|
19
|
+
Requires-Dist: fastapi>=0.115; extra == 'server'
|
|
20
|
+
Requires-Dist: redis[hiredis]>=5.2; extra == 'server'
|
|
21
|
+
Requires-Dist: uvicorn[standard]>=0.32; extra == 'server'
|
|
22
|
+
Provides-Extra: timescale
|
|
23
|
+
Requires-Dist: asyncpg>=0.30; extra == 'timescale'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# haystack-py
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/haystack-py/)
|
|
29
|
+
[](https://pypi.org/project/haystack-py/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
[](https://github.com/jscott3201/hs-py/actions/workflows/ci.yml)
|
|
32
|
+
|
|
33
|
+
Asynchronous [Project Haystack](https://project-haystack.org/) client and server library for Python 3.13+. HTTP and WebSocket transports, four wire formats, SCRAM-SHA-256 and mTLS authentication, pluggable storage backends (Redis, TimescaleDB), and full ontology support. Built on native `asyncio`.
|
|
34
|
+
|
|
35
|
+
[Documentation](https://jscott3201.github.io/hs-py/) | [Getting Started](https://jscott3201.github.io/hs-py/getting-started.html) | [API Reference](https://jscott3201.github.io/hs-py/api/index.html) | [Changelog](CHANGELOG.md)
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from hs_py import Client
|
|
39
|
+
|
|
40
|
+
async with Client("http://server/api", "admin", "secret") as c:
|
|
41
|
+
about = await c.about()
|
|
42
|
+
points = await c.read("point and temp and sensor")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Table of Contents
|
|
46
|
+
|
|
47
|
+
- [Features](#features)
|
|
48
|
+
- [Installation](#installation)
|
|
49
|
+
- [Quick Start](#quick-start)
|
|
50
|
+
- [Storage Backends](#storage-backends)
|
|
51
|
+
- [Architecture](#architecture)
|
|
52
|
+
- [Configuration](#configuration)
|
|
53
|
+
- [Testing](#testing)
|
|
54
|
+
- [Requirements](#requirements)
|
|
55
|
+
- [License](#license)
|
|
56
|
+
|
|
57
|
+
## Features
|
|
58
|
+
|
|
59
|
+
| Category | Highlights |
|
|
60
|
+
|----------|-----------|
|
|
61
|
+
| **Transports** | HTTP with aiohttp, WebSocket with `websockets` sans-I/O, persistent connections, per-message deflate compression |
|
|
62
|
+
| **Client** | `Client` (HTTP) and `WebSocketClient` with all 13 standard ops, batch requests, watch subscriptions, auto-auth |
|
|
63
|
+
| **Server** | FastAPI application factory, SCRAM-SHA-256 middleware, content negotiation (JSON/Zinc/Trio/CSV), standalone `WebSocketServer` |
|
|
64
|
+
| **Wire Formats** | JSON v3/v4 (`orjson`), Zinc (text), Trio (tagged records), CSV (export-only) |
|
|
65
|
+
| **Authentication** | SCRAM-SHA-256 over HTTP and WebSocket, PLAINTEXT fallback, token-based WebSocket auth, mTLS with `CertAuthenticator` |
|
|
66
|
+
| **TLS** | TLS 1.3 enforced, mutual authentication, test certificate generation (EC P-256), `TLSConfig` dataclass |
|
|
67
|
+
| **Storage** | Pluggable `StorageAdapter` protocol with Redis (RediSearch + RedisTimeSeries), TimescaleDB (asyncpg + JSONB), and in-memory backends |
|
|
68
|
+
| **Data Model** | All Haystack value types as frozen dataclasses, `Grid` / `GridBuilder` as universal message format |
|
|
69
|
+
| **Filters** | Recursive descent parser, AST representation, evaluation against dicts and grids, SQL pushdown for JSONB/RediSearch |
|
|
70
|
+
| **Ontology** | Def/Lib/Namespace model, taxonomy queries, tag normalization, dict-to-def reflection |
|
|
71
|
+
| **WebSocket Extras** | `ReconnectingWebSocketClient` with backoff, `WebSocketPool` / `ChannelClient` multiplexing, binary frame codec |
|
|
72
|
+
| **Observability** | `MetricsHooks` for connection, message, request, and error callbacks |
|
|
73
|
+
| **Watch** | Server-side `WatchState` delta encoding, client-side `WatchAccumulator` delta merging |
|
|
74
|
+
| **Quality** | 1,200+ tests, 69 end-to-end integration tests, 122 TimescaleDB tests, mypy strict, ruff linting, frozen dataclasses throughout |
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install haystack-py
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Optional extras:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install haystack-py[server] # FastAPI + Redis backend (server-side)
|
|
86
|
+
pip install haystack-py[timescale] # TimescaleDB/PostgreSQL backend
|
|
87
|
+
pip install haystack-py[rdf] # RDF ontology import (rdflib)
|
|
88
|
+
pip install haystack-py[all] # All optional dependencies
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
git clone https://github.com/jscott3201/hs-py.git
|
|
95
|
+
cd hs-py
|
|
96
|
+
uv sync --group dev
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Quick Start
|
|
100
|
+
|
|
101
|
+
### HTTP Client
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
import asyncio
|
|
105
|
+
from hs_py import Client, Ref
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def main():
|
|
109
|
+
async with Client("http://server/api", "admin", "secret") as c:
|
|
110
|
+
# Server info
|
|
111
|
+
about = await c.about()
|
|
112
|
+
print(about[0]["serverName"])
|
|
113
|
+
|
|
114
|
+
# Filter-based read
|
|
115
|
+
sites = await c.read("site")
|
|
116
|
+
for row in sites:
|
|
117
|
+
print(row.get("dis"), row.get("id"))
|
|
118
|
+
|
|
119
|
+
# ID-based read
|
|
120
|
+
entities = await c.read_by_ids([Ref("site-1"), Ref("equip-2")])
|
|
121
|
+
|
|
122
|
+
# Navigation
|
|
123
|
+
nav = await c.nav() # root sites
|
|
124
|
+
children = await c.nav(Ref("site-1")) # site's equips
|
|
125
|
+
|
|
126
|
+
# History
|
|
127
|
+
history = await c.his_read(Ref("point-1"), "yesterday")
|
|
128
|
+
for row in history:
|
|
129
|
+
print(row["ts"], row["val"])
|
|
130
|
+
|
|
131
|
+
# Write a point value
|
|
132
|
+
await c.point_write(Ref("point-1"), level=8, val=72.5)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
asyncio.run(main())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### WebSocket Client
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import asyncio
|
|
142
|
+
from hs_py import WebSocketClient, Grid, Ref
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def main():
|
|
146
|
+
async with WebSocketClient("ws://server/api/ws", auth_token="token") as ws:
|
|
147
|
+
about = await ws.about()
|
|
148
|
+
|
|
149
|
+
# Batch: multiple ops in one round-trip
|
|
150
|
+
results = await ws.batch(
|
|
151
|
+
("about", Grid.make_empty()),
|
|
152
|
+
("read", Grid.make_rows([{"filter": "site"}])),
|
|
153
|
+
("read", Grid.make_rows([{"filter": "point and temp"}])),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Watch: subscribe to entity changes
|
|
157
|
+
watch = await ws.watch_sub([Ref("p-1"), Ref("p-2")], "my-watch")
|
|
158
|
+
watch_id = watch.meta["watchId"]
|
|
159
|
+
poll = await ws.watch_poll(watch_id)
|
|
160
|
+
await ws.watch_close(watch_id)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
asyncio.run(main())
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Server
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import asyncio
|
|
170
|
+
from hs_py.ops import HaystackOps
|
|
171
|
+
from hs_py.storage.memory import MemoryAdapter
|
|
172
|
+
from hs_py.auth_types import SimpleAuthenticator
|
|
173
|
+
from hs_py.fastapi_server import create_app
|
|
174
|
+
import uvicorn
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def main():
|
|
178
|
+
storage = MemoryAdapter()
|
|
179
|
+
await storage.start()
|
|
180
|
+
await storage.load_entities([
|
|
181
|
+
{"id": Ref("s1"), "site": MARKER, "dis": "My Building"},
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
ops = HaystackOps(storage=storage)
|
|
185
|
+
auth = SimpleAuthenticator({"admin": "secret"})
|
|
186
|
+
app = create_app(ops, authenticator=auth)
|
|
187
|
+
config = uvicorn.Config(app, host="0.0.0.0", port=8080)
|
|
188
|
+
server = uvicorn.Server(config)
|
|
189
|
+
await server.serve()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
asyncio.run(main())
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Wire Formats
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from hs_py.encoding import json, zinc, trio, csv
|
|
199
|
+
from hs_py.encoding.json import JsonVersion
|
|
200
|
+
|
|
201
|
+
# JSON v4
|
|
202
|
+
grid = json.decode_grid(data, version=JsonVersion.V4)
|
|
203
|
+
json_bytes = json.encode_grid(grid, version=JsonVersion.V4)
|
|
204
|
+
|
|
205
|
+
# Zinc
|
|
206
|
+
zinc_text = zinc.encode_grid(grid)
|
|
207
|
+
grid = zinc.decode_grid(zinc_text)
|
|
208
|
+
|
|
209
|
+
# Trio records
|
|
210
|
+
records = trio.parse_trio(trio_text)
|
|
211
|
+
trio_text = trio.encode_trio(records)
|
|
212
|
+
|
|
213
|
+
# CSV (encode-only)
|
|
214
|
+
csv_text = csv.encode_grid(grid)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Filters
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from hs_py import MARKER, parse, evaluate, evaluate_grid
|
|
221
|
+
|
|
222
|
+
# Parse filter to AST
|
|
223
|
+
ast = parse("point and temp and sensor")
|
|
224
|
+
|
|
225
|
+
# Evaluate against a dict
|
|
226
|
+
entity = {"point": MARKER, "temp": MARKER, "sensor": MARKER}
|
|
227
|
+
assert evaluate(ast, entity) is True
|
|
228
|
+
|
|
229
|
+
# Filter a grid
|
|
230
|
+
matching = evaluate_grid(ast, grid)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Ontology
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from hs_py.ontology.namespace import Namespace, load_lib_from_trio
|
|
237
|
+
from hs_py.ontology.reflect import reflect
|
|
238
|
+
|
|
239
|
+
lib = load_lib_from_trio(trio_text)
|
|
240
|
+
ns = Namespace([lib])
|
|
241
|
+
|
|
242
|
+
# Taxonomy queries
|
|
243
|
+
assert ns.is_subtype("ahu", "equip")
|
|
244
|
+
subtypes = ns.subtypes("equip") # [ahu, vav, ...]
|
|
245
|
+
|
|
246
|
+
# Reflect entities against definitions
|
|
247
|
+
defs = reflect(ns, entity_dict)
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Storage Backends
|
|
251
|
+
|
|
252
|
+
haystack-py defines a `StorageAdapter` protocol that decouples server operations from data storage. Three implementations are provided:
|
|
253
|
+
|
|
254
|
+
| Backend | Module | Best For |
|
|
255
|
+
|---------|--------|----------|
|
|
256
|
+
| **Memory** | `storage.memory` | Testing, prototyping, small datasets |
|
|
257
|
+
| **Redis** | `storage.redis` | Production with RediSearch full-text and RedisTimeSeries |
|
|
258
|
+
| **TimescaleDB** | `storage.timescale` | Production with PostgreSQL JSONB and time-series hypertables |
|
|
259
|
+
|
|
260
|
+
### Redis
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
from hs_py.storage.redis import RedisAdapter, create_redis_client
|
|
264
|
+
|
|
265
|
+
r = await create_redis_client("redis://localhost:6379")
|
|
266
|
+
adapter = RedisAdapter(r)
|
|
267
|
+
await adapter.start()
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Features: RediSearch indexes for filter queries, RedisTimeSeries for history, pub/sub for watch notifications.
|
|
271
|
+
|
|
272
|
+
### TimescaleDB
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from hs_py.storage.timescale import TimescaleAdapter, create_timescale_pool
|
|
276
|
+
|
|
277
|
+
pool = await create_timescale_pool("postgresql://localhost/haystack")
|
|
278
|
+
adapter = TimescaleAdapter(pool)
|
|
279
|
+
await adapter.start() # Creates schema + hypertable
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Features: JSONB entity storage with GIN indexes, filter AST → SQL pushdown, hypertable time-series, COPY-based bulk loading.
|
|
283
|
+
|
|
284
|
+
## Architecture
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
src/hs_py/
|
|
288
|
+
kinds.py Haystack value types (Marker, Number, Ref, Coord, etc.)
|
|
289
|
+
grid.py Grid, Col, GridBuilder -- universal message format
|
|
290
|
+
errors.py Exception hierarchy (HaystackError, CallError, AuthError)
|
|
291
|
+
auth.py SCRAM-SHA-256 / PLAINTEXT client auth handshake
|
|
292
|
+
auth_types.py Authenticator protocol, SimpleAuthenticator, CertAuthenticator
|
|
293
|
+
client.py Async HTTP client with all standard ops
|
|
294
|
+
ops.py HaystackOps base class with storage-backed op dispatch
|
|
295
|
+
fastapi_server.py FastAPI application factory, SCRAM middleware, WebSocket endpoint
|
|
296
|
+
metrics.py MetricsHooks for transport-level observability
|
|
297
|
+
tls.py TLSConfig, SSL context builders, certificate generation
|
|
298
|
+
security.py Security hardening utilities
|
|
299
|
+
watch.py WatchState (server delta), WatchAccumulator (client merge)
|
|
300
|
+
ws.py Sans-I/O WebSocket wrapper (websockets library)
|
|
301
|
+
ws_client.py WebSocketClient, ReconnectingWebSocketClient, WebSocketPool
|
|
302
|
+
ws_server.py Standalone WebSocket server with SCRAM auth and batch dispatch
|
|
303
|
+
ws_codec.py Binary frame codec (4-byte header + JSON payload)
|
|
304
|
+
encoding/
|
|
305
|
+
json.py JSON v3/v4 encode/decode via orjson
|
|
306
|
+
zinc.py Zinc text format encode/decode
|
|
307
|
+
trio.py Trio tagged record format
|
|
308
|
+
csv.py CSV export (encode-only, lossy)
|
|
309
|
+
scanner.py Shared Zinc value scanning
|
|
310
|
+
filter/
|
|
311
|
+
ast.py Filter AST nodes (Has, Missing, Cmp, And, Or, Path)
|
|
312
|
+
lexer.py Filter expression tokenizer
|
|
313
|
+
parser.py Recursive descent parser
|
|
314
|
+
eval.py Filter evaluation against dicts/grids
|
|
315
|
+
storage/
|
|
316
|
+
protocol.py StorageAdapter protocol (11 async methods)
|
|
317
|
+
memory.py In-memory adapter for testing
|
|
318
|
+
redis.py Redis + RediSearch + RedisTimeSeries adapter
|
|
319
|
+
timescale.py PostgreSQL/TimescaleDB adapter via asyncpg
|
|
320
|
+
ontology/
|
|
321
|
+
defs.py Def and Lib frozen dataclasses
|
|
322
|
+
namespace.py Namespace container, symbol resolution
|
|
323
|
+
taxonomy.py Subtype tree, tag inheritance
|
|
324
|
+
normalize.py Normalization pipeline
|
|
325
|
+
reflect.py Dict-to-def reflection engine
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Key Classes
|
|
329
|
+
|
|
330
|
+
| Class | Module | Purpose |
|
|
331
|
+
|-------|--------|---------|
|
|
332
|
+
| `Client` | `client` | Async HTTP client with SCRAM auth and all Haystack ops |
|
|
333
|
+
| `WebSocketClient` | `ws_client` | Persistent WebSocket client with batch and watch support |
|
|
334
|
+
| `ReconnectingWebSocketClient` | `ws_client` | Auto-reconnecting WebSocket client with exponential backoff |
|
|
335
|
+
| `WebSocketPool` | `ws_client` | Multiplexed channels over a single WebSocket connection |
|
|
336
|
+
| `HaystackOps` | `ops` | Storage-backed server operation handler for all 13 ops |
|
|
337
|
+
| `WebSocketServer` | `ws_server` | Standalone WebSocket server with SCRAM auth and push |
|
|
338
|
+
| `Grid` | `grid` | Universal Haystack message format (immutable) |
|
|
339
|
+
| `GridBuilder` | `grid` | Fluent builder for constructing grids |
|
|
340
|
+
| `StorageAdapter` | `storage.protocol` | Protocol for pluggable storage backends |
|
|
341
|
+
| `TLSConfig` | `tls` | TLS certificate configuration |
|
|
342
|
+
| `MetricsHooks` | `metrics` | Optional observability callbacks |
|
|
343
|
+
| `WatchState` | `watch` | Server-side watch delta computation |
|
|
344
|
+
| `WatchAccumulator` | `watch` | Client-side watch delta merging |
|
|
345
|
+
| `Namespace` | `ontology.namespace` | Resolved ontology with taxonomy queries |
|
|
346
|
+
|
|
347
|
+
### Error Handling
|
|
348
|
+
|
|
349
|
+
All client methods raise from a common exception hierarchy:
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
from hs_py import HaystackError, CallError, AuthError, NetworkError
|
|
353
|
+
|
|
354
|
+
# HaystackError Base for all haystack-py errors
|
|
355
|
+
# CallError Server returned an error grid
|
|
356
|
+
# AuthError Authentication failure
|
|
357
|
+
# NetworkError Transport-level failure
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Configuration
|
|
361
|
+
|
|
362
|
+
### Docker Compose
|
|
363
|
+
|
|
364
|
+
The included `docker/docker-compose.yml` provides a complete development stack:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
docker compose -f docker/docker-compose.yml up -d
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Services:
|
|
371
|
+
|
|
372
|
+
| Service | Port | Description |
|
|
373
|
+
|---------|------|-------------|
|
|
374
|
+
| `server` | 8080 | FastAPI Haystack server with SCRAM auth |
|
|
375
|
+
| `redis` | 6379 | Redis with RediSearch and RedisTimeSeries |
|
|
376
|
+
| `timescaledb` | 5432 | TimescaleDB (PostgreSQL 16) |
|
|
377
|
+
| `redis-tls` | 6380 | Redis with mTLS for TLS integration tests |
|
|
378
|
+
|
|
379
|
+
Environment variables for the server:
|
|
380
|
+
|
|
381
|
+
| Variable | Default | Description |
|
|
382
|
+
|----------|---------|-------------|
|
|
383
|
+
| `REDIS_URL` | `redis://redis:6379` | Redis connection URL |
|
|
384
|
+
| `HAYSTACK_USER` | `admin` | SCRAM username |
|
|
385
|
+
| `HAYSTACK_PASS` | `secret` | SCRAM password |
|
|
386
|
+
|
|
387
|
+
### Seed Data
|
|
388
|
+
|
|
389
|
+
The `_data/` directory contains Project Haystack example building datasets in JSON v4 format:
|
|
390
|
+
|
|
391
|
+
- **Alpha** — 2,032 entities (1 site, 184 equips, 1,846 points)
|
|
392
|
+
- **Bravo** — 1,077 entities (1 site, 149 equips, 918 points)
|
|
393
|
+
|
|
394
|
+
These are loaded automatically by the Docker server on startup.
|
|
395
|
+
|
|
396
|
+
## Testing
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
make test # 1,200+ unit tests
|
|
400
|
+
make lint # ruff check + format verification
|
|
401
|
+
make typecheck # mypy strict
|
|
402
|
+
make check # lint + typecheck + test (all of the above)
|
|
403
|
+
make coverage # tests with coverage report
|
|
404
|
+
make fix # auto-fix lint/format issues
|
|
405
|
+
make docs # sphinx-build
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Docker Integration Tests
|
|
409
|
+
|
|
410
|
+
End-to-end testing against real services with full SCRAM authentication:
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
make docker-server # Start Redis + FastAPI server stack
|
|
414
|
+
make docker-test-e2e # 69 end-to-end tests (HTTP, WebSocket, auth, history, watch)
|
|
415
|
+
make docker-server-clean # Tear down server stack
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### Storage Backend Tests
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
make docker-test # Redis adapter integration tests
|
|
422
|
+
make docker-test-tls # Redis mTLS integration tests
|
|
423
|
+
make docker-test-timescale # 122 TimescaleDB integration tests
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Cleanup
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
make docker-clean # Remove Redis containers
|
|
430
|
+
make docker-clean-timescale # Remove TimescaleDB containers
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Requirements
|
|
434
|
+
|
|
435
|
+
- Python >= 3.13
|
|
436
|
+
- [aiohttp](https://docs.aiohttp.org/) >= 3.10
|
|
437
|
+
- [orjson](https://github.com/ijl/orjson) >= 3.10
|
|
438
|
+
- [cryptography](https://cryptography.io/) >= 42.0
|
|
439
|
+
- [websockets](https://websockets.readthedocs.io/) >= 14.0
|
|
440
|
+
- Optional: [FastAPI](https://fastapi.tiangolo.com/) + [Redis](https://redis.io/) for server mode
|
|
441
|
+
- Optional: [asyncpg](https://magicstack.github.io/asyncpg/) for TimescaleDB backend
|
|
442
|
+
- Docker and Docker Compose for integration tests
|
|
443
|
+
|
|
444
|
+
## License
|
|
445
|
+
|
|
446
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
hs_py/__init__.py,sha256=1a1NH5CbNGKTeDbwx4SdqobDoR_-eh-0rRmf0d-AgXw,3782
|
|
2
|
+
hs_py/__main__.py,sha256=MQtHV1A3od6WtqwGPYd3Ya9m_KJrnPVBks0TpAvjY6o,3793
|
|
3
|
+
hs_py/_scram_core.py,sha256=UquYTt96USmSn85fuwwq-5YY4PTD-tygkDqFpiFc65Q,9845
|
|
4
|
+
hs_py/auth.py,sha256=3aVD_tnjsu3nar4yTUB9M9w4lDA7YCXFbxlsQ4tsiak,13776
|
|
5
|
+
hs_py/auth_types.py,sha256=fBTJFokvodz4ar2645SeYHOa07_47biJUyrOzRYNwkc,3660
|
|
6
|
+
hs_py/client.py,sha256=Fr9wrAQBW3lQ1hHykl7sjNs62dW4-3xoi5n37FNhz0w,22654
|
|
7
|
+
hs_py/content_negotiation.py,sha256=UGULngWqCcHxL2YLO5iQN5XNFRhjd6jZ30_jLPQVMlE,4577
|
|
8
|
+
hs_py/convert.py,sha256=RTkINlzsKF8rHYgbip78MvbGH3zJcd9hmVBmeazUP64,2911
|
|
9
|
+
hs_py/errors.py,sha256=hKW_rWe_RK8_JdR5Qe7gbmEO8WLGv1GDHX9Dp22HoYU,1330
|
|
10
|
+
hs_py/fastapi_server.py,sha256=aRysIRNUhu3ZCBIRuUFQU6F2DIEIazjeNAftqjIcDtA,18273
|
|
11
|
+
hs_py/grid.py,sha256=VDHqB43uA1kcZJzqbFyMoNDgZ9SRjeMllW8j7ZV41ME,6153
|
|
12
|
+
hs_py/kinds.py,sha256=sbI5ziUKakktb39W9xSA15npaK7RHE0sF9hku1jtcZQ,7451
|
|
13
|
+
hs_py/metrics.py,sha256=_tHSrAqbYUls_D2AOHvLR78IfLqmDiV5FDlnspRhjFw,2518
|
|
14
|
+
hs_py/ops.py,sha256=cb7JtvECcbbf5q9OLeJEbrEWXLm7MHaO6shvnx9Rlsw,15300
|
|
15
|
+
hs_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
hs_py/redis_ops.py,sha256=XUJCo3gUeuesT4qoba-iJHJAYl0S6VTH9SyWs-Wk2Y0,4469
|
|
17
|
+
hs_py/tls.py,sha256=Yk6-UR1jPigNBq4mKDSstSu_CjflyAc-tvzbsENjO10,11056
|
|
18
|
+
hs_py/watch.py,sha256=8n0qVxtv5GMpuHcKEiZbzyH29oJ7P9FgUi00n-gJ7SI,6501
|
|
19
|
+
hs_py/ws.py,sha256=CswvUPClez3h72Fuy8OzP0x0Al3B_VslHmeEi0MiKSo,14107
|
|
20
|
+
hs_py/ws_client.py,sha256=N7QeU0BonL-YJuMU9J-P8YO1RPcPvT4s4PUauhZyN70,54310
|
|
21
|
+
hs_py/ws_codec.py,sha256=rSQ5fODumoP2m3B5pxuFVbX7X5m5oIDcetY6WBsqUpE,4281
|
|
22
|
+
hs_py/ws_server.py,sha256=hMNGNETIsojYapDZEguR2iTXdDhEJyMzYgne5B3CpXo,19670
|
|
23
|
+
hs_py/encoding/__init__.py,sha256=xd40h674PlWQ-86_VDwrnYyOvDGkBfIvZJp0Vkx1tLk,1046
|
|
24
|
+
hs_py/encoding/csv.py,sha256=8_JNj7-s8EFTEO-z3tdUXvGfuQFoN0poJQGJO2dbczU,2929
|
|
25
|
+
hs_py/encoding/json.py,sha256=Y0qu52AQKvfrQpbmUTSIsj3gECws8KQFqgNkjE-I2YM,20421
|
|
26
|
+
hs_py/encoding/scanner.py,sha256=Qy7n6V7M7T26qOa0e7KlD8-ttBeVFiexCHnkm-o0BOA,18771
|
|
27
|
+
hs_py/encoding/trio.py,sha256=J2Z_C3jo3Q_FcL1a6ogWFK-e3iBF4CRDyhV-J4LdDwU,9062
|
|
28
|
+
hs_py/encoding/zinc.py,sha256=WkZsm4lvkjLXRNHExSAeIXVWjEkzP_50P-hXWb4ogJ8,8606
|
|
29
|
+
hs_py/filter/__init__.py,sha256=hEfsw8y67NDeBVwBBXU7N-vl6WDUWhvEMRXgIg5A1po,738
|
|
30
|
+
hs_py/filter/ast.py,sha256=TQv17hDvMNELiiG942qB8mL6DjGqrOsMaQcqzyroU9I,2339
|
|
31
|
+
hs_py/filter/eval.py,sha256=HJ0OktQEmUxGBV2EcwCqumzVsSbF2etEq3n0n-OZ-Ro,5662
|
|
32
|
+
hs_py/filter/lexer.py,sha256=FclM5FWJUdCz1-usDSVLDMJOUOo-btL7xFz8yi0bh-8,10045
|
|
33
|
+
hs_py/filter/parser.py,sha256=5tUABoOGL-rdKSoCet8Io0ZUYivvBrnYJHOlLlTcyRo,4889
|
|
34
|
+
hs_py/ontology/__init__.py,sha256=iDU8t6wTj8lbyXyoSepLHngW8sw79GDHyVt9nwnw6qQ,1514
|
|
35
|
+
hs_py/ontology/defs.py,sha256=VPbo1F6VYmbUkmycFJj2BrMA7bwFaCuT7vWKLXhP8aQ,4214
|
|
36
|
+
hs_py/ontology/namespace.py,sha256=VwMdUB8YbYBHyn1ORSTp7iNZjoJUZPi_Cc70POcvleQ,8673
|
|
37
|
+
hs_py/ontology/normalize.py,sha256=ks3y60UAHbjxpMYD32Llz6LexyrM8Biz5TTFovwI0nM,4803
|
|
38
|
+
hs_py/ontology/rdf.py,sha256=3Ii3ABSF5T6p0SZwLM3HYBlU9wB7Gor11C_rWDWt4DU,3975
|
|
39
|
+
hs_py/ontology/reflect.py,sha256=n20mtULp8hrq4nMEnGY5GFkm47-VcvJ7niGgNAF6wuE,3424
|
|
40
|
+
hs_py/ontology/taxonomy.py,sha256=jLlFy6ELKZNg4AVFxqXPvvmjdEYk7k37Ms9uopEbI40,3307
|
|
41
|
+
hs_py/storage/__init__.py,sha256=4h4lVreYnOND_YyyDQHR1OEqITzBAj9qrMUCgzlRIiA,942
|
|
42
|
+
hs_py/storage/memory.py,sha256=3DLOaQaXDCMgNxca_iZrXTQZ_pfVlgNri2Ch5G_Iq6k,11937
|
|
43
|
+
hs_py/storage/protocol.py,sha256=axFXSoAqjvEOOOqYB2XApNdfIilwqVmVXJ3avRBMnMU,5874
|
|
44
|
+
hs_py/storage/redis.py,sha256=hERp5OI6dPq6_eKqkxdl5QGs77GdmWqx8Y8vYF6AqPg,28289
|
|
45
|
+
hs_py/storage/timescale.py,sha256=EZx_XvOZkMkHiUCaps6yoXLgTm1BBJmK8ZYpWAnMePk,29827
|
|
46
|
+
haystack_py-0.1.3.dist-info/METADATA,sha256=YkMiw1tRnounuxeNUx8Efb8FRKhZ90-EcXEt0GkICr0,15792
|
|
47
|
+
haystack_py-0.1.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
48
|
+
haystack_py-0.1.3.dist-info/licenses/LICENSE,sha256=3GO_-MGsYcCdBLOP5qFVRUCgfAKrPLNEG7zN8JaauXc,1069
|
|
49
|
+
haystack_py-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Justin Scott
|
|
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.
|
hs_py/__init__.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""hs-py — Async Project Haystack client library for Python."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.3"
|
|
4
|
+
|
|
5
|
+
from hs_py.auth_types import (
|
|
6
|
+
Authenticator,
|
|
7
|
+
CertAuthenticator,
|
|
8
|
+
ScramCredentials,
|
|
9
|
+
SimpleAuthenticator,
|
|
10
|
+
)
|
|
11
|
+
from hs_py.client import Client
|
|
12
|
+
from hs_py.convert import grid_to_pythonic
|
|
13
|
+
from hs_py.encoding import JsonVersion
|
|
14
|
+
from hs_py.errors import AuthError, CallError, HaystackError, NetworkError
|
|
15
|
+
from hs_py.filter import ParseError, evaluate, evaluate_grid, parse
|
|
16
|
+
from hs_py.grid import Col, Grid, GridBuilder
|
|
17
|
+
from hs_py.kinds import (
|
|
18
|
+
MARKER,
|
|
19
|
+
NA,
|
|
20
|
+
REMOVE,
|
|
21
|
+
Coord,
|
|
22
|
+
Marker,
|
|
23
|
+
Na,
|
|
24
|
+
Number,
|
|
25
|
+
Ref,
|
|
26
|
+
Remove,
|
|
27
|
+
Symbol,
|
|
28
|
+
Uri,
|
|
29
|
+
XStr,
|
|
30
|
+
)
|
|
31
|
+
from hs_py.metrics import MetricsHooks
|
|
32
|
+
from hs_py.ontology.rdf import export_jsonld, export_turtle
|
|
33
|
+
from hs_py.ops import HaystackOps
|
|
34
|
+
from hs_py.tls import (
|
|
35
|
+
TLSConfig,
|
|
36
|
+
build_client_ssl_context,
|
|
37
|
+
build_server_ssl_context,
|
|
38
|
+
extract_peer_cn,
|
|
39
|
+
extract_peer_sans,
|
|
40
|
+
generate_test_certificates,
|
|
41
|
+
)
|
|
42
|
+
from hs_py.watch import WatchAccumulator, WatchState
|
|
43
|
+
from hs_py.ws import HaystackWebSocket
|
|
44
|
+
from hs_py.ws_client import (
|
|
45
|
+
ChannelClient,
|
|
46
|
+
ReconnectingWebSocketClient,
|
|
47
|
+
WebSocketClient,
|
|
48
|
+
WebSocketPool,
|
|
49
|
+
)
|
|
50
|
+
from hs_py.ws_codec import (
|
|
51
|
+
OP_CODES,
|
|
52
|
+
decode_binary_frame,
|
|
53
|
+
encode_binary_push,
|
|
54
|
+
encode_binary_request,
|
|
55
|
+
encode_binary_response,
|
|
56
|
+
)
|
|
57
|
+
from hs_py.ws_server import WebSocketServer
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __getattr__(name: str) -> object:
|
|
61
|
+
if name == "RedisOps":
|
|
62
|
+
from hs_py.redis_ops import RedisOps
|
|
63
|
+
|
|
64
|
+
return RedisOps
|
|
65
|
+
if name == "RedisAdapter":
|
|
66
|
+
from hs_py.storage.redis import RedisAdapter
|
|
67
|
+
|
|
68
|
+
return RedisAdapter
|
|
69
|
+
if name == "create_redis_client":
|
|
70
|
+
from hs_py.redis_ops import create_redis_client
|
|
71
|
+
|
|
72
|
+
return create_redis_client
|
|
73
|
+
if name == "create_fastapi_app":
|
|
74
|
+
from hs_py.fastapi_server import create_fastapi_app
|
|
75
|
+
|
|
76
|
+
return create_fastapi_app
|
|
77
|
+
if name == "StorageAdapter":
|
|
78
|
+
from hs_py.storage.protocol import StorageAdapter
|
|
79
|
+
|
|
80
|
+
return StorageAdapter
|
|
81
|
+
if name == "InMemoryAdapter":
|
|
82
|
+
from hs_py.storage.memory import InMemoryAdapter
|
|
83
|
+
|
|
84
|
+
return InMemoryAdapter
|
|
85
|
+
if name == "TimescaleAdapter":
|
|
86
|
+
from hs_py.storage.timescale import TimescaleAdapter
|
|
87
|
+
|
|
88
|
+
return TimescaleAdapter
|
|
89
|
+
if name == "create_timescale_pool":
|
|
90
|
+
from hs_py.storage.timescale import create_timescale_pool
|
|
91
|
+
|
|
92
|
+
return create_timescale_pool
|
|
93
|
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
|
94
|
+
raise AttributeError(msg)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = [
|
|
98
|
+
"MARKER",
|
|
99
|
+
"NA",
|
|
100
|
+
"OP_CODES",
|
|
101
|
+
"REMOVE",
|
|
102
|
+
"AuthError",
|
|
103
|
+
"Authenticator",
|
|
104
|
+
"CallError",
|
|
105
|
+
"CertAuthenticator",
|
|
106
|
+
"ChannelClient",
|
|
107
|
+
"Client",
|
|
108
|
+
"Col",
|
|
109
|
+
"Coord",
|
|
110
|
+
"Grid",
|
|
111
|
+
"GridBuilder",
|
|
112
|
+
"HaystackError",
|
|
113
|
+
"HaystackOps",
|
|
114
|
+
"HaystackWebSocket",
|
|
115
|
+
"InMemoryAdapter",
|
|
116
|
+
"JsonVersion",
|
|
117
|
+
"Marker",
|
|
118
|
+
"MetricsHooks",
|
|
119
|
+
"Na",
|
|
120
|
+
"NetworkError",
|
|
121
|
+
"Number",
|
|
122
|
+
"ParseError",
|
|
123
|
+
"ReconnectingWebSocketClient",
|
|
124
|
+
"RedisAdapter",
|
|
125
|
+
"RedisOps",
|
|
126
|
+
"Ref",
|
|
127
|
+
"Remove",
|
|
128
|
+
"ScramCredentials",
|
|
129
|
+
"SimpleAuthenticator",
|
|
130
|
+
"StorageAdapter",
|
|
131
|
+
"Symbol",
|
|
132
|
+
"TLSConfig",
|
|
133
|
+
"TimescaleAdapter",
|
|
134
|
+
"Uri",
|
|
135
|
+
"WatchAccumulator",
|
|
136
|
+
"WatchState",
|
|
137
|
+
"WebSocketClient",
|
|
138
|
+
"WebSocketPool",
|
|
139
|
+
"WebSocketServer",
|
|
140
|
+
"XStr",
|
|
141
|
+
"__version__",
|
|
142
|
+
"build_client_ssl_context",
|
|
143
|
+
"build_server_ssl_context",
|
|
144
|
+
"create_fastapi_app",
|
|
145
|
+
"create_redis_client",
|
|
146
|
+
"create_timescale_pool",
|
|
147
|
+
"decode_binary_frame",
|
|
148
|
+
"encode_binary_push",
|
|
149
|
+
"encode_binary_request",
|
|
150
|
+
"encode_binary_response",
|
|
151
|
+
"evaluate",
|
|
152
|
+
"evaluate_grid",
|
|
153
|
+
"export_jsonld",
|
|
154
|
+
"export_turtle",
|
|
155
|
+
"extract_peer_cn",
|
|
156
|
+
"extract_peer_sans",
|
|
157
|
+
"generate_test_certificates",
|
|
158
|
+
"grid_to_pythonic",
|
|
159
|
+
"parse",
|
|
160
|
+
]
|