erubus 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
erubus-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: erubus
3
+ Version: 0.2.0
4
+ Summary: Client for Erubus — temporal knowledge graph for AI agents and robots
5
+ Author: Ashraf Galib Shaik
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AshrafGalibShaik/Erubus
8
+ Project-URL: Source, https://github.com/AshrafGalibShaik/Erubus
9
+ Project-URL: Issues, https://github.com/AshrafGalibShaik/Erubus/issues
10
+ Keywords: knowledge-graph,temporal,robotics,agents,memory
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+
20
+ # erubus (Python client)
21
+
22
+ Stdlib-only client for [Erubus](https://github.com/AshrafGalibShaik/Erubus), a
23
+ temporal knowledge graph for AI agents and robots. No dependencies.
24
+
25
+ ```bash
26
+ pip install -e . # or just copy erubus.py next to your code
27
+ erubus serve & # the Rust binary, anywhere on your PATH
28
+ python test_erubus.py # smoke test against it
29
+ ```
30
+
31
+ ```python
32
+ from erubus import Erubus
33
+
34
+ kg = Erubus() # http://127.0.0.1:8080
35
+ # Auth / namespace (optional; when set, sent on every request):
36
+ # kg = Erubus(token="abc123", ns="prod") # Authorization: Bearer …, X-Erubus-Ns: …
37
+
38
+ kg.set_decay("vision:", half_life_secs=5) # sensor beliefs go stale
39
+ kg.assert_fact("pallet-7", "at", "bay-3", confidence=0.9, source="vision:cam1")
40
+
41
+ kg.value_at("pallet-7", "at")
42
+ # {'value': 'bay-3', 'effective_confidence': 0.87, 'server_time': ...}
43
+
44
+ # What did the robot believe when it decided, not what was true:
45
+ kg.value_at("pallet-7", "at", t=20.0, known_at=decision_wall_clock)
46
+
47
+ kg.merge("bay 3", "Bay-03") # same entity, two spellings
48
+ kg.suggest_merge(q="bay 3") # fuzzy candidates (does not merge)
49
+ kg.context("pallet-7", depth=2) # everything reachable, as of now
50
+
51
+ kg.set_schema("located_in", functional=True) # schema registry
52
+ kg.get_schema() # {"relations": [...]}
53
+ kg.retention(86400) # drop closed edges older than 1d
54
+ kg.backup("/tmp/erubus.db") # consistent snapshot → local file
55
+
56
+ for fact in kg.watch("pallet-7", reconnect=True): # live SSE stream
57
+ print(fact)
58
+ ```
59
+
60
+ High-rate sensors should batch — one HTTP round-trip per fact is what makes a
61
+ 30Hz loop expensive:
62
+
63
+ ```python
64
+ with kg.batch(max_size=256) as b: # flushes at max_size and on exit
65
+ for det in detections:
66
+ b.add(det.id, "at", det.bay, confidence=det.score, source="vision:cam1")
67
+ ```
68
+
69
+ Batches are all-or-nothing: if any fact is invalid (e.g. a clock-skewed
70
+ timestamp) the server rejects the whole batch and nothing lands, so retrying is
71
+ safe.
72
+
73
+ All times are **UTC epoch seconds** — `time.time()` is already correct. Omit
74
+ `t` and the server stamps it. Writes with a timestamp far in the future are
75
+ rejected; call `kg.clock_skew()` to check your clock against the server's.
erubus-0.2.0/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # erubus (Python client)
2
+
3
+ Stdlib-only client for [Erubus](https://github.com/AshrafGalibShaik/Erubus), a
4
+ temporal knowledge graph for AI agents and robots. No dependencies.
5
+
6
+ ```bash
7
+ pip install -e . # or just copy erubus.py next to your code
8
+ erubus serve & # the Rust binary, anywhere on your PATH
9
+ python test_erubus.py # smoke test against it
10
+ ```
11
+
12
+ ```python
13
+ from erubus import Erubus
14
+
15
+ kg = Erubus() # http://127.0.0.1:8080
16
+ # Auth / namespace (optional; when set, sent on every request):
17
+ # kg = Erubus(token="abc123", ns="prod") # Authorization: Bearer …, X-Erubus-Ns: …
18
+
19
+ kg.set_decay("vision:", half_life_secs=5) # sensor beliefs go stale
20
+ kg.assert_fact("pallet-7", "at", "bay-3", confidence=0.9, source="vision:cam1")
21
+
22
+ kg.value_at("pallet-7", "at")
23
+ # {'value': 'bay-3', 'effective_confidence': 0.87, 'server_time': ...}
24
+
25
+ # What did the robot believe when it decided, not what was true:
26
+ kg.value_at("pallet-7", "at", t=20.0, known_at=decision_wall_clock)
27
+
28
+ kg.merge("bay 3", "Bay-03") # same entity, two spellings
29
+ kg.suggest_merge(q="bay 3") # fuzzy candidates (does not merge)
30
+ kg.context("pallet-7", depth=2) # everything reachable, as of now
31
+
32
+ kg.set_schema("located_in", functional=True) # schema registry
33
+ kg.get_schema() # {"relations": [...]}
34
+ kg.retention(86400) # drop closed edges older than 1d
35
+ kg.backup("/tmp/erubus.db") # consistent snapshot → local file
36
+
37
+ for fact in kg.watch("pallet-7", reconnect=True): # live SSE stream
38
+ print(fact)
39
+ ```
40
+
41
+ High-rate sensors should batch — one HTTP round-trip per fact is what makes a
42
+ 30Hz loop expensive:
43
+
44
+ ```python
45
+ with kg.batch(max_size=256) as b: # flushes at max_size and on exit
46
+ for det in detections:
47
+ b.add(det.id, "at", det.bay, confidence=det.score, source="vision:cam1")
48
+ ```
49
+
50
+ Batches are all-or-nothing: if any fact is invalid (e.g. a clock-skewed
51
+ timestamp) the server rejects the whole batch and nothing lands, so retrying is
52
+ safe.
53
+
54
+ All times are **UTC epoch seconds** — `time.time()` is already correct. Omit
55
+ `t` and the server stamps it. Writes with a timestamp far in the future are
56
+ rejected; call `kg.clock_skew()` to check your clock against the server's.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: erubus
3
+ Version: 0.2.0
4
+ Summary: Client for Erubus — temporal knowledge graph for AI agents and robots
5
+ Author: Ashraf Galib Shaik
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/AshrafGalibShaik/Erubus
8
+ Project-URL: Source, https://github.com/AshrafGalibShaik/Erubus
9
+ Project-URL: Issues, https://github.com/AshrafGalibShaik/Erubus/issues
10
+ Keywords: knowledge-graph,temporal,robotics,agents,memory
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+
20
+ # erubus (Python client)
21
+
22
+ Stdlib-only client for [Erubus](https://github.com/AshrafGalibShaik/Erubus), a
23
+ temporal knowledge graph for AI agents and robots. No dependencies.
24
+
25
+ ```bash
26
+ pip install -e . # or just copy erubus.py next to your code
27
+ erubus serve & # the Rust binary, anywhere on your PATH
28
+ python test_erubus.py # smoke test against it
29
+ ```
30
+
31
+ ```python
32
+ from erubus import Erubus
33
+
34
+ kg = Erubus() # http://127.0.0.1:8080
35
+ # Auth / namespace (optional; when set, sent on every request):
36
+ # kg = Erubus(token="abc123", ns="prod") # Authorization: Bearer …, X-Erubus-Ns: …
37
+
38
+ kg.set_decay("vision:", half_life_secs=5) # sensor beliefs go stale
39
+ kg.assert_fact("pallet-7", "at", "bay-3", confidence=0.9, source="vision:cam1")
40
+
41
+ kg.value_at("pallet-7", "at")
42
+ # {'value': 'bay-3', 'effective_confidence': 0.87, 'server_time': ...}
43
+
44
+ # What did the robot believe when it decided, not what was true:
45
+ kg.value_at("pallet-7", "at", t=20.0, known_at=decision_wall_clock)
46
+
47
+ kg.merge("bay 3", "Bay-03") # same entity, two spellings
48
+ kg.suggest_merge(q="bay 3") # fuzzy candidates (does not merge)
49
+ kg.context("pallet-7", depth=2) # everything reachable, as of now
50
+
51
+ kg.set_schema("located_in", functional=True) # schema registry
52
+ kg.get_schema() # {"relations": [...]}
53
+ kg.retention(86400) # drop closed edges older than 1d
54
+ kg.backup("/tmp/erubus.db") # consistent snapshot → local file
55
+
56
+ for fact in kg.watch("pallet-7", reconnect=True): # live SSE stream
57
+ print(fact)
58
+ ```
59
+
60
+ High-rate sensors should batch — one HTTP round-trip per fact is what makes a
61
+ 30Hz loop expensive:
62
+
63
+ ```python
64
+ with kg.batch(max_size=256) as b: # flushes at max_size and on exit
65
+ for det in detections:
66
+ b.add(det.id, "at", det.bay, confidence=det.score, source="vision:cam1")
67
+ ```
68
+
69
+ Batches are all-or-nothing: if any fact is invalid (e.g. a clock-skewed
70
+ timestamp) the server rejects the whole batch and nothing lands, so retrying is
71
+ safe.
72
+
73
+ All times are **UTC epoch seconds** — `time.time()` is already correct. Omit
74
+ `t` and the server stamps it. Writes with a timestamp far in the future are
75
+ rejected; call `kg.clock_skew()` to check your clock against the server's.
@@ -0,0 +1,7 @@
1
+ README.md
2
+ erubus.py
3
+ pyproject.toml
4
+ erubus.egg-info/PKG-INFO
5
+ erubus.egg-info/SOURCES.txt
6
+ erubus.egg-info/dependency_links.txt
7
+ erubus.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ erubus
erubus-0.2.0/erubus.py ADDED
@@ -0,0 +1,270 @@
1
+ """Erubus client — temporal knowledge graph for agents and robots.
2
+
3
+ Stdlib only, no dependencies. All times are UTC UNIX epoch seconds; `time.time()`
4
+ is already correct, so pass nothing and the server stamps `now`.
5
+
6
+ from erubus import Erubus
7
+ kg = Erubus()
8
+ kg.assert_fact("pallet-7", "at", "bay-3", confidence=0.9, source="vision:cam1")
9
+ kg.value_at("pallet-7", "at") # -> {"value": "bay-3", "effective_confidence": 0.87, ...}
10
+ for fact in kg.watch("pallet-7"): # blocking SSE stream
11
+ print(fact)
12
+ """
13
+
14
+ import json
15
+ import time
16
+ import urllib.error
17
+ import urllib.parse
18
+ import urllib.request
19
+
20
+ __all__ = ["Erubus", "Batch", "ErubusError"]
21
+
22
+
23
+ class ErubusError(RuntimeError):
24
+ """Server rejected the request (e.g. clock skew, bad params)."""
25
+
26
+
27
+ class Erubus:
28
+ def __init__(self, url="http://127.0.0.1:8080", timeout=10.0, token=None, ns=None):
29
+ """`token` → Authorization: Bearer; `ns` → X-Erubus-Ns on every request."""
30
+ self.url = url.rstrip("/")
31
+ self.timeout = timeout
32
+ self.token = token
33
+ self.ns = ns
34
+
35
+ # -- internals ---------------------------------------------------------
36
+ def _auth_headers(self):
37
+ headers = {}
38
+ if self.token is not None:
39
+ headers["Authorization"] = "Bearer %s" % self.token
40
+ if self.ns is not None:
41
+ headers["X-Erubus-Ns"] = self.ns
42
+ return headers
43
+
44
+ def _raise_http(self, path, e):
45
+ detail = e.read().decode(errors="replace")
46
+ try:
47
+ detail = json.loads(detail).get("error", detail)
48
+ except ValueError:
49
+ pass
50
+ raise ErubusError("%s %s: %s" % (e.code, path, detail)) from None
51
+
52
+ def _request(self, method, path, params=None, body=None, timeout=None):
53
+ target = self.url + path
54
+ if params:
55
+ clean = {k: v for k, v in params.items() if v is not None}
56
+ if clean:
57
+ target += "?" + urllib.parse.urlencode(clean)
58
+ data = json.dumps(body).encode() if body is not None else None
59
+ req = urllib.request.Request(target, data=data, method=method)
60
+ for k, v in self._auth_headers().items():
61
+ req.add_header(k, v)
62
+ if data is not None:
63
+ req.add_header("Content-Type", "application/json")
64
+ try:
65
+ with urllib.request.urlopen(req, timeout=timeout or self.timeout) as r:
66
+ return json.loads(r.read() or b"{}")
67
+ except urllib.error.HTTPError as e:
68
+ self._raise_http(path, e)
69
+
70
+ # -- writes ------------------------------------------------------------
71
+ def assert_fact(self, subject, rel, obj, confidence=1.0, source="python", t=None):
72
+ """Record `subject --rel--> obj`. `t` defaults to server-side now (UTC)."""
73
+ body = {
74
+ "subject": subject,
75
+ "rel": rel,
76
+ "object": obj,
77
+ "confidence": confidence,
78
+ "source": source,
79
+ }
80
+ if t is not None:
81
+ body["t"] = float(t)
82
+ return self._request("POST", "/fact", body=body)
83
+
84
+ def assert_facts(self, facts):
85
+ """Record many facts in one request. Returns the accepted count.
86
+
87
+ `facts` is an iterable of dicts (`subject`/`rel`/`object` required;
88
+ `confidence`, `source`, `t` optional) or 3-tuples. All-or-nothing: if
89
+ any element is invalid the whole batch is rejected and nothing lands,
90
+ so retrying is safe.
91
+ """
92
+ body = []
93
+ for f in facts:
94
+ if isinstance(f, dict):
95
+ item = dict(f)
96
+ else:
97
+ subject, rel, obj = f
98
+ item = {"subject": subject, "rel": rel, "object": obj}
99
+ item.setdefault("confidence", 1.0)
100
+ item.setdefault("source", "python")
101
+ body.append(item)
102
+ return self._request("POST", "/facts", body=body).get("accepted", 0)
103
+
104
+ def batch(self, max_size=256):
105
+ """Buffered writer for high-rate sensors. Use as a context manager:
106
+
107
+ with kg.batch() as b:
108
+ for det in detections:
109
+ b.add(det.id, "at", det.bay, confidence=det.score)
110
+ """
111
+ return Batch(self, max_size)
112
+
113
+ def merge(self, alias, canonical):
114
+ """Declare `alias` and `canonical` the same entity; rewrites history."""
115
+ return self._request("POST", "/merge", body={"alias": alias, "canonical": canonical})
116
+
117
+ def set_decay(self, source_prefix, half_life_secs):
118
+ """Beliefs from sources matching `source_prefix` lose half their
119
+ confidence every `half_life_secs` since last observation."""
120
+ return self._request(
121
+ "POST", "/decay",
122
+ body={"source_prefix": source_prefix, "half_life_secs": float(half_life_secs)},
123
+ )
124
+
125
+ def set_schema(self, rel, functional=False):
126
+ """Register (or upsert) relation `rel` in the schema registry."""
127
+ return self._request(
128
+ "POST", "/schema",
129
+ body={"rel": rel, "functional": bool(functional)},
130
+ )
131
+
132
+ def retention(self, older_than_secs):
133
+ """Delete closed edges older than `older_than_secs` (server minimum 1h)."""
134
+ return self._request(
135
+ "POST", "/retention",
136
+ body={"older_than_secs": float(older_than_secs)},
137
+ )
138
+
139
+ # -- reads -------------------------------------------------------------
140
+ def value_at(self, subject, rel, t=None, known_at=None):
141
+ """What we believe(d) `subject --rel--> ?` at valid time `t`.
142
+
143
+ `known_at` restricts to facts learned by then — "what did the robot
144
+ believe at the moment it decided", as distinct from what was true.
145
+ """
146
+ return self._request("GET", "/value", params={
147
+ "subject": subject, "rel": rel, "t": t, "known_at": known_at,
148
+ })
149
+
150
+ def context(self, root, depth=2, t=None):
151
+ """Everything reachable from `root` within `depth` hops, as of `t`."""
152
+ return self._request("GET", "/context", params={"root": root, "depth": depth, "t": t})
153
+
154
+ def get_schema(self):
155
+ """List registered relations (`{"relations": [{"rel", "functional"}, ...]}`)."""
156
+ return self._request("GET", "/schema")
157
+
158
+ def suggest_merge(self, q=None):
159
+ """Fuzzy merge candidates (suggestions only — does not merge).
160
+
161
+ Returns a list of dicts with `canonical_guess` and `members`. Optional
162
+ `q` restricts to the normalized form of that entity id.
163
+ """
164
+ return self._request("GET", "/suggest_merge", params={"q": q}).get("suggestions", [])
165
+
166
+ def backup(self, dest_path):
167
+ """Stream a consistent DB snapshot (`GET /backup`) to `dest_path`.
168
+
169
+ Returns the number of bytes written.
170
+ """
171
+ target = self.url + "/backup"
172
+ req = urllib.request.Request(target, method="GET")
173
+ for k, v in self._auth_headers().items():
174
+ req.add_header(k, v)
175
+ try:
176
+ with urllib.request.urlopen(req, timeout=self.timeout) as r:
177
+ written = 0
178
+ with open(dest_path, "wb") as f:
179
+ while True:
180
+ chunk = r.read(65536)
181
+ if not chunk:
182
+ break
183
+ f.write(chunk)
184
+ written += len(chunk)
185
+ return written
186
+ except urllib.error.HTTPError as e:
187
+ self._raise_http("/backup", e)
188
+
189
+ def health(self):
190
+ return self._request("GET", "/health")
191
+
192
+ def clock_skew(self):
193
+ """Seconds this client's clock is ahead of the server's. Writes whose
194
+ `t` exceeds the server's skew window are rejected, so check this if
195
+ `assert_fact` raises about future timestamps."""
196
+ return time.time() - float(self.health()["server_time"])
197
+
198
+ # -- streaming ---------------------------------------------------------
199
+ def watch(self, subject=None, timeout=None, reconnect=False, retry_secs=1.0):
200
+ """Yield facts as they are ingested. Blocking generator over SSE.
201
+
202
+ `subject=None` streams everything. With `reconnect=True` the stream is
203
+ re-established after a dropped connection; facts produced while
204
+ disconnected are missed, so re-read state with `context()` on resume if
205
+ you need a consistent view.
206
+ """
207
+ while True:
208
+ try:
209
+ yield from self._watch_once(subject, timeout)
210
+ except (urllib.error.URLError, ConnectionError, TimeoutError):
211
+ if not reconnect:
212
+ raise
213
+ if not reconnect:
214
+ return
215
+ time.sleep(retry_secs)
216
+
217
+ def _watch_once(self, subject, timeout):
218
+ target = self.url + "/watch"
219
+ if subject is not None:
220
+ target += "?" + urllib.parse.urlencode({"subject": subject})
221
+ headers = {"Accept": "text/event-stream"}
222
+ headers.update(self._auth_headers())
223
+ req = urllib.request.Request(target, headers=headers)
224
+ try:
225
+ with urllib.request.urlopen(req, timeout=timeout) as r:
226
+ for raw in r:
227
+ line = raw.decode(errors="replace").strip()
228
+ if line.startswith("data:"):
229
+ payload = line[5:].strip()
230
+ if payload:
231
+ yield json.loads(payload)
232
+ except urllib.error.HTTPError as e:
233
+ self._raise_http("/watch", e)
234
+
235
+
236
+ class Batch:
237
+ """Accumulates facts and flushes them in one request.
238
+
239
+ Auto-flushes at `max_size`; flushes on exit. A 30Hz sensor writing one fact
240
+ per HTTP round-trip is bound by network latency — batching is what makes
241
+ that workload cheap.
242
+ """
243
+
244
+ def __init__(self, client, max_size=256):
245
+ self.client = client
246
+ self.max_size = max_size
247
+ self.pending = []
248
+
249
+ def add(self, subject, rel, obj, confidence=1.0, source="python", t=None):
250
+ item = {"subject": subject, "rel": rel, "object": obj,
251
+ "confidence": confidence, "source": source}
252
+ if t is not None:
253
+ item["t"] = float(t)
254
+ self.pending.append(item)
255
+ if len(self.pending) >= self.max_size:
256
+ return self.flush()
257
+ return 0
258
+
259
+ def flush(self):
260
+ if not self.pending:
261
+ return 0
262
+ batch, self.pending = self.pending, []
263
+ return self.client.assert_facts(batch)
264
+
265
+ def __enter__(self):
266
+ return self
267
+
268
+ def __exit__(self, *exc):
269
+ self.flush()
270
+ return False
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "erubus"
7
+ version = "0.2.0"
8
+ description = "Client for Erubus — temporal knowledge graph for AI agents and robots"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Ashraf Galib Shaik" }]
13
+ keywords = ["knowledge-graph", "temporal", "robotics", "agents", "memory"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Database",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/AshrafGalibShaik/Erubus"
26
+ Source = "https://github.com/AshrafGalibShaik/Erubus"
27
+ Issues = "https://github.com/AshrafGalibShaik/Erubus/issues"
28
+
29
+ [tool.setuptools]
30
+ py-modules = ["erubus"]
erubus-0.2.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+