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 +75 -0
- erubus-0.2.0/README.md +56 -0
- erubus-0.2.0/erubus.egg-info/PKG-INFO +75 -0
- erubus-0.2.0/erubus.egg-info/SOURCES.txt +7 -0
- erubus-0.2.0/erubus.egg-info/dependency_links.txt +1 -0
- erubus-0.2.0/erubus.egg-info/top_level.txt +1 -0
- erubus-0.2.0/erubus.py +270 -0
- erubus-0.2.0/pyproject.toml +30 -0
- erubus-0.2.0/setup.cfg +4 -0
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 @@
|
|
|
1
|
+
|
|
@@ -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