elastik 0.0.1__py3-none-win_amd64.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.
- elastik/__init__.py +151 -0
- elastik/__main__.py +67 -0
- elastik/_bin/elastik-core.exe +0 -0
- elastik/_spawn.py +151 -0
- elastik/reactor.py +275 -0
- elastik/sdk.py +129 -0
- elastik-0.0.1.dist-info/METADATA +141 -0
- elastik-0.0.1.dist-info/RECORD +11 -0
- elastik-0.0.1.dist-info/WHEEL +5 -0
- elastik-0.0.1.dist-info/entry_points.txt +2 -0
- elastik-0.0.1.dist-info/top_level.txt +1 -0
elastik/__init__.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""elastik — pastebin with HMAC that accidentally became a web OS.
|
|
2
|
+
|
|
3
|
+
Quickstart (NumPy-style — module-level calls, no instantiation):
|
|
4
|
+
|
|
5
|
+
import elastik
|
|
6
|
+
|
|
7
|
+
e = elastik.start(token="x") # spawns the bundled rust core
|
|
8
|
+
e.put("/home/note", "hello") # PUT
|
|
9
|
+
print(e.get("/home/note", raw=True)) # bytes back
|
|
10
|
+
elastik.stop() # kills the child
|
|
11
|
+
|
|
12
|
+
Or skip the explicit start() and point at an already-running elastik
|
|
13
|
+
(env: `ELASTIK_URL=http://elastik.local:3005 ELASTIK_TOKEN=xxx`):
|
|
14
|
+
|
|
15
|
+
import elastik
|
|
16
|
+
elastik.put("/home/note", "hello")
|
|
17
|
+
print(elastik.get("/home/note", raw=True))
|
|
18
|
+
|
|
19
|
+
Reactor (declarative event handlers — see Elastik-core/README.md §L2):
|
|
20
|
+
|
|
21
|
+
@elastik.listen("/home/inbox/*")
|
|
22
|
+
def triage(body, world, meta):
|
|
23
|
+
if b"urgent" in body:
|
|
24
|
+
return elastik.Reply(f"/home/alerts/{world.split('/')[-1]}", body)
|
|
25
|
+
return elastik.Archive()
|
|
26
|
+
|
|
27
|
+
elastik.serve(port=3200) # runs as fanout sidecar
|
|
28
|
+
|
|
29
|
+
Bundled binary lives at `elastik/_bin/elastik-core[.exe]` and is invoked
|
|
30
|
+
as a child process. No FFI, no compile-on-install. Same shape as
|
|
31
|
+
NumPy shipping precompiled C kernels.
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
from elastik.sdk import Elastik, ElastikError
|
|
39
|
+
from elastik.reactor import (
|
|
40
|
+
listen,
|
|
41
|
+
MoveTo,
|
|
42
|
+
Reply,
|
|
43
|
+
Archive,
|
|
44
|
+
Drop,
|
|
45
|
+
Action,
|
|
46
|
+
Ctx,
|
|
47
|
+
finalize,
|
|
48
|
+
serve,
|
|
49
|
+
)
|
|
50
|
+
from elastik._spawn import (
|
|
51
|
+
start,
|
|
52
|
+
stop,
|
|
53
|
+
is_running,
|
|
54
|
+
default_url,
|
|
55
|
+
binary_info,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Class — for users who want explicit instances
|
|
60
|
+
"Elastik",
|
|
61
|
+
"ElastikError",
|
|
62
|
+
# Reactor sugar
|
|
63
|
+
"listen", "MoveTo", "Reply", "Archive", "Drop", "Action", "Ctx",
|
|
64
|
+
"finalize", "serve",
|
|
65
|
+
# Lifecycle
|
|
66
|
+
"start", "stop", "is_running", "default_url", "binary_info",
|
|
67
|
+
# Module-level convenience (NumPy-shaped)
|
|
68
|
+
"put", "get", "head", "delete", "list_worlds", "shaped",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
__version__ = "0.0.1"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── module-level singleton client ──────────────────────────────────
|
|
75
|
+
# Lazy: only built on first call. Lets `import elastik` succeed even if
|
|
76
|
+
# no server is running (you only need a server when you actually call).
|
|
77
|
+
|
|
78
|
+
_default_client: Elastik | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _client() -> Elastik:
|
|
82
|
+
"""Get-or-build the default singleton client.
|
|
83
|
+
|
|
84
|
+
Pinned to ELASTIK_URL (default http://127.0.0.1:3005, the Python
|
|
85
|
+
reference impl port). Token from ELASTIK_TOKEN.
|
|
86
|
+
|
|
87
|
+
If you spawned a child via `elastik.start(port=N, token=T)`, that
|
|
88
|
+
call returns its own client and ALSO updates this singleton so
|
|
89
|
+
module-level `elastik.put(...)` lands at your child.
|
|
90
|
+
"""
|
|
91
|
+
global _default_client
|
|
92
|
+
if _default_client is None:
|
|
93
|
+
_default_client = Elastik(
|
|
94
|
+
default_url(),
|
|
95
|
+
token=os.getenv("ELASTIK_TOKEN", ""),
|
|
96
|
+
)
|
|
97
|
+
return _default_client
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _set_default(client: Elastik) -> None:
|
|
101
|
+
"""Internal: rebind the singleton (called from start()).
|
|
102
|
+
Public API is just: call start(), use module-level functions."""
|
|
103
|
+
global _default_client
|
|
104
|
+
_default_client = client
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Re-export start() to also bind the singleton, so the next 模式 works:
|
|
108
|
+
# e = elastik.start() # works (returns client)
|
|
109
|
+
# elastik.put("/x", "y") # also works (uses the same client)
|
|
110
|
+
_orig_start = start
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def start(*args: Any, **kwargs: Any): # type: ignore[no-redef]
|
|
114
|
+
client = _orig_start(*args, **kwargs)
|
|
115
|
+
_set_default(client)
|
|
116
|
+
return client
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ── module-level CRUD ──────────────────────────────────────────────
|
|
120
|
+
# These are the "np.array([...])" of elastik. Brutal. One import,
|
|
121
|
+
# call.
|
|
122
|
+
|
|
123
|
+
def put(path: str, data, **meta: Any) -> dict:
|
|
124
|
+
"""elastik.put('/home/note', 'hello', actor='me') -> {'ok': True, ...}"""
|
|
125
|
+
return _client().put(path, data, **meta)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get(path: str, raw: bool = False) -> Any:
|
|
129
|
+
"""elastik.get('/home/note', raw=True) -> bytes
|
|
130
|
+
elastik.get('/home/note') -> dict envelope"""
|
|
131
|
+
return _client().get(path, raw=raw)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def head(path: str) -> dict[str, str]:
|
|
135
|
+
"""elastik.head('/home/note') -> {'x-meta-...': '...'}"""
|
|
136
|
+
return _client().head(path)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def delete(path: str) -> bool:
|
|
140
|
+
"""elastik.delete('/home/note') -> True/False"""
|
|
141
|
+
return _client().delete(path)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def list_worlds() -> list[str]:
|
|
145
|
+
"""elastik.list_worlds() -> ['inbox/x', 'archive/y', ...]"""
|
|
146
|
+
return _client().list()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def shaped(path: str, accept: str = "text/html", intent: str = "") -> bytes:
|
|
150
|
+
"""elastik.shaped('/home/x', accept='text/html', intent='render as card')"""
|
|
151
|
+
return _client().shaped(path, accept=accept, intent=intent)
|
elastik/__main__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""`python -m elastik` — quick info + REPL helpers.
|
|
2
|
+
|
|
3
|
+
python -m elastik # show binary info
|
|
4
|
+
python -m elastik run # spawn the bundled core, block
|
|
5
|
+
python -m elastik run --port 3105 # custom port + env
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from elastik._spawn import binary_info, start, stop
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(prog="elastik")
|
|
17
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
18
|
+
|
|
19
|
+
sub.add_parser("info", help="show bundled-binary info")
|
|
20
|
+
|
|
21
|
+
p_run = sub.add_parser("run", help="launch bundled elastik-core (foreground)")
|
|
22
|
+
p_run.add_argument("--host", default="127.0.0.1")
|
|
23
|
+
p_run.add_argument("--port", type=int, default=3105)
|
|
24
|
+
p_run.add_argument("--key", default="elastik-default-key")
|
|
25
|
+
p_run.add_argument("--token", default="")
|
|
26
|
+
p_run.add_argument("--approve-token", dest="approve_token", default="")
|
|
27
|
+
p_run.add_argument("--data-dir", dest="data_dir", default=None)
|
|
28
|
+
p_run.add_argument("--listeners", default="")
|
|
29
|
+
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
if args.cmd in (None, "info"):
|
|
33
|
+
info = binary_info()
|
|
34
|
+
for k, v in info.items():
|
|
35
|
+
print(f" {k}: {v}")
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
if args.cmd == "run":
|
|
39
|
+
try:
|
|
40
|
+
client = start(
|
|
41
|
+
host=args.host,
|
|
42
|
+
port=args.port,
|
|
43
|
+
key=args.key,
|
|
44
|
+
token=args.token,
|
|
45
|
+
approve_token=args.approve_token,
|
|
46
|
+
data_dir=args.data_dir,
|
|
47
|
+
listeners=args.listeners,
|
|
48
|
+
quiet=False,
|
|
49
|
+
)
|
|
50
|
+
print(f"elastik running at {client.url}", flush=True)
|
|
51
|
+
print(" Ctrl-C to stop", flush=True)
|
|
52
|
+
try:
|
|
53
|
+
while True:
|
|
54
|
+
import time
|
|
55
|
+
time.sleep(3600)
|
|
56
|
+
except KeyboardInterrupt:
|
|
57
|
+
pass
|
|
58
|
+
finally:
|
|
59
|
+
stop()
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
parser.print_help()
|
|
63
|
+
return 2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
sys.exit(main())
|
|
Binary file
|
elastik/_spawn.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Spawn the bundled elastik-core Rust binary as a child process.
|
|
2
|
+
|
|
3
|
+
This is the "NumPy ships C kernels in the wheel" trick. The Rust
|
|
4
|
+
binary lives at `elastik/_bin/elastik-core[.exe]`, was built once on a
|
|
5
|
+
build machine, and is shipped as `package_data`. `elastik.start()`
|
|
6
|
+
launches it in a subprocess and returns a pre-bound `Elastik` client.
|
|
7
|
+
|
|
8
|
+
The user does:
|
|
9
|
+
|
|
10
|
+
import elastik
|
|
11
|
+
e = elastik.start(token="x")
|
|
12
|
+
e.put("/home/note", "hi")
|
|
13
|
+
print(e.get("/home/note", raw=True))
|
|
14
|
+
elastik.stop()
|
|
15
|
+
|
|
16
|
+
…and never sees a Cargo.toml.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import atexit
|
|
21
|
+
import os
|
|
22
|
+
import socket
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_proc: Optional[subprocess.Popen] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _binary_path() -> Path:
|
|
34
|
+
"""Locate the bundled binary inside the installed package."""
|
|
35
|
+
here = Path(__file__).resolve().parent / "_bin"
|
|
36
|
+
name = "elastik-core.exe" if sys.platform == "win32" else "elastik-core"
|
|
37
|
+
return here / name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wait_for_port(host: str, port: int, deadline_s: float = 10.0) -> bool:
|
|
41
|
+
"""Poll until the server accepts a TCP connection or we give up."""
|
|
42
|
+
end = time.time() + deadline_s
|
|
43
|
+
while time.time() < end:
|
|
44
|
+
try:
|
|
45
|
+
with socket.create_connection((host, port), timeout=0.3):
|
|
46
|
+
return True
|
|
47
|
+
except OSError:
|
|
48
|
+
time.sleep(0.05)
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def start(
|
|
53
|
+
port: int = 3105,
|
|
54
|
+
host: str = "127.0.0.1",
|
|
55
|
+
key: str = "elastik-default-key",
|
|
56
|
+
token: str = "",
|
|
57
|
+
approve_token: str = "",
|
|
58
|
+
data_dir: Optional[str] = None,
|
|
59
|
+
listeners: str = "",
|
|
60
|
+
quiet: bool = True,
|
|
61
|
+
):
|
|
62
|
+
"""Launch the bundled elastik-core. Returns a pre-bound Elastik client.
|
|
63
|
+
|
|
64
|
+
All process state is the user's: token, port, key, data dir. We only
|
|
65
|
+
set sane defaults so `elastik.start()` with no arguments produces a
|
|
66
|
+
working instance pinned to localhost.
|
|
67
|
+
"""
|
|
68
|
+
global _proc
|
|
69
|
+
binary = _binary_path()
|
|
70
|
+
if not binary.exists():
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
f"bundled binary not found: {binary}\n"
|
|
73
|
+
"If you're working from source, build it first:\n"
|
|
74
|
+
" cd Elastik-core && cargo build --release\n"
|
|
75
|
+
" cp target/release/elastik-core* "
|
|
76
|
+
f"{binary.parent}/"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if _proc is not None and _proc.poll() is None:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
"elastik already running in this process — call elastik.stop() first"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
env = os.environ.copy()
|
|
85
|
+
env["ELASTIK_HOST"] = host
|
|
86
|
+
env["ELASTIK_PORT"] = str(port)
|
|
87
|
+
env["ELASTIK_KEY"] = key
|
|
88
|
+
if token:
|
|
89
|
+
env["ELASTIK_TOKEN"] = token
|
|
90
|
+
if approve_token:
|
|
91
|
+
env["ELASTIK_APPROVE_TOKEN"] = approve_token
|
|
92
|
+
if data_dir:
|
|
93
|
+
env["ELASTIK_DATA"] = str(data_dir)
|
|
94
|
+
if listeners:
|
|
95
|
+
env["ELASTIK_LISTENERS"] = listeners
|
|
96
|
+
|
|
97
|
+
out = subprocess.DEVNULL if quiet else None
|
|
98
|
+
_proc = subprocess.Popen([str(binary)], env=env, stdout=out, stderr=out)
|
|
99
|
+
atexit.register(stop)
|
|
100
|
+
|
|
101
|
+
if not _wait_for_port(host, port):
|
|
102
|
+
stop()
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
f"elastik-core failed to start on {host}:{port} within 10s"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Re-import here to dodge a circular import at module load
|
|
108
|
+
from elastik.sdk import Elastik
|
|
109
|
+
|
|
110
|
+
return Elastik(f"http://{host}:{port}", token=token)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def stop() -> None:
|
|
114
|
+
"""Kill the launched binary, if any. Safe to call multiple times."""
|
|
115
|
+
global _proc
|
|
116
|
+
if _proc is None:
|
|
117
|
+
return
|
|
118
|
+
try:
|
|
119
|
+
_proc.terminate()
|
|
120
|
+
try:
|
|
121
|
+
_proc.wait(timeout=3)
|
|
122
|
+
except subprocess.TimeoutExpired:
|
|
123
|
+
_proc.kill()
|
|
124
|
+
_proc.wait(timeout=2)
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
_proc = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def is_running() -> bool:
|
|
131
|
+
return _proc is not None and _proc.poll() is None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def default_url() -> str:
|
|
135
|
+
"""Where the module-level `elastik.put`/`elastik.get` calls land by
|
|
136
|
+
default. Override with `ELASTIK_URL`. Falls back to the local
|
|
137
|
+
server.py port (3005) so this module also works against the existing
|
|
138
|
+
Python reference impl."""
|
|
139
|
+
return os.getenv("ELASTIK_URL", "http://127.0.0.1:3005")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def binary_info() -> dict:
|
|
143
|
+
"""Where is the bundled binary? How big? Does it exist? — useful
|
|
144
|
+
for `python -m elastik` debugging."""
|
|
145
|
+
p = _binary_path()
|
|
146
|
+
return {
|
|
147
|
+
"path": str(p),
|
|
148
|
+
"exists": p.exists(),
|
|
149
|
+
"size_bytes": p.stat().st_size if p.exists() else None,
|
|
150
|
+
"platform": sys.platform,
|
|
151
|
+
}
|
elastik/reactor.py
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""L2 — declarative reactor sugar over L1 atoms.
|
|
2
|
+
|
|
3
|
+
The reactor is the "千人千面 in one event loop" pattern, made writable.
|
|
4
|
+
Two deployment shapes; same `@listen` syntax for both:
|
|
5
|
+
|
|
6
|
+
1. **Plugin** (in-process with elastik-python).
|
|
7
|
+
Write a plugin file, end with `ROUTES, handle = finalize()`.
|
|
8
|
+
elastik-python's plugin runtime hot-loads it.
|
|
9
|
+
|
|
10
|
+
2. **Sidecar** (separate process; elastik-core fans out via HTTP).
|
|
11
|
+
Run `python my_agent.py` which calls `serve(host, port)`.
|
|
12
|
+
Configure elastik-core with
|
|
13
|
+
`ELASTIK_LISTENERS="/home/inbox/*=http://my-agent:port"`.
|
|
14
|
+
|
|
15
|
+
Both shapes match the manifesto: code lives in one place; the runtime
|
|
16
|
+
decides how to invoke it. The handler is pure — input -> Action.
|
|
17
|
+
|
|
18
|
+
Action classes describe intent. Execution happens in the reactor (not
|
|
19
|
+
in the handler). This is the Harvard split inside the reactor itself:
|
|
20
|
+
handler = judgment, executor = side effects.
|
|
21
|
+
|
|
22
|
+
stdlib + sdk.elastik. ~200 lines.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import http.server
|
|
27
|
+
import inspect
|
|
28
|
+
import json
|
|
29
|
+
import threading
|
|
30
|
+
from typing import Any, Callable
|
|
31
|
+
|
|
32
|
+
from elastik.sdk import Elastik
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── registration ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
_routes: dict[str, Callable[..., Any]] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def listen(pattern: str):
|
|
41
|
+
"""Register a handler for a path pattern.
|
|
42
|
+
|
|
43
|
+
Pattern is prefix-with-trailing-`*`:
|
|
44
|
+
"/home/inbox/*" matches "/home/inbox/alice/abc"
|
|
45
|
+
"/home/foo" matches exactly "/home/foo"
|
|
46
|
+
"*" matches everything
|
|
47
|
+
|
|
48
|
+
The handler signature is introspected. It receives `body` (bytes)
|
|
49
|
+
plus any of these named kwargs it asks for:
|
|
50
|
+
world str — the URL path that was written
|
|
51
|
+
version int — the new version after the write
|
|
52
|
+
pattern str — the registered pattern that matched
|
|
53
|
+
meta dict — X-Meta-* headers as a flat dict
|
|
54
|
+
e Elastik— SDK client, pre-bound to the configured backend
|
|
55
|
+
|
|
56
|
+
Return: an Action, a list of Actions, or None.
|
|
57
|
+
"""
|
|
58
|
+
def deco(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
59
|
+
_routes[pattern] = func
|
|
60
|
+
return func
|
|
61
|
+
return deco
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _matches(pattern: str, path: str) -> bool:
|
|
65
|
+
if pattern == "*":
|
|
66
|
+
return True
|
|
67
|
+
if pattern.endswith("*"):
|
|
68
|
+
return path.startswith(pattern[:-1])
|
|
69
|
+
return path == pattern
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── actions (intent objects) ───────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
class Action:
|
|
75
|
+
"""Base. Subclasses describe intent; .execute(ctx) does the work."""
|
|
76
|
+
|
|
77
|
+
def execute(self, ctx: Ctx) -> None:
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MoveTo(Action):
|
|
82
|
+
"""Write to dest, then delete the source."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, dest: str, **meta: Any):
|
|
85
|
+
self.dest = dest
|
|
86
|
+
self.meta = meta
|
|
87
|
+
|
|
88
|
+
def execute(self, ctx: Ctx) -> None:
|
|
89
|
+
ctx.e.put(self.dest, ctx.body, **self.meta)
|
|
90
|
+
ctx.e.delete(ctx.world)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Reply(Action):
|
|
94
|
+
"""Write a new world. Source untouched."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, dest: str, body: bytes | str, **meta: Any):
|
|
97
|
+
self.dest = dest
|
|
98
|
+
self.body = body
|
|
99
|
+
self.meta = meta
|
|
100
|
+
|
|
101
|
+
def execute(self, ctx: Ctx) -> None:
|
|
102
|
+
ctx.e.put(self.dest, self.body, **self.meta)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Archive(Action):
|
|
106
|
+
"""Move source under prefix/<basename>."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, prefix: str = "/home/archive/"):
|
|
109
|
+
self.prefix = prefix
|
|
110
|
+
|
|
111
|
+
def execute(self, ctx: Ctx) -> None:
|
|
112
|
+
name = ctx.world.rstrip("/").rsplit("/", 1)[-1] or "anon"
|
|
113
|
+
ctx.e.put(self.prefix.rstrip("/") + "/" + name, ctx.body)
|
|
114
|
+
ctx.e.delete(ctx.world)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Drop(Action):
|
|
118
|
+
"""Delete the source. No reply."""
|
|
119
|
+
|
|
120
|
+
def execute(self, ctx: Ctx) -> None:
|
|
121
|
+
ctx.e.delete(ctx.world)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ── context passed to handlers + actions ────────────────────────────
|
|
125
|
+
|
|
126
|
+
class Ctx:
|
|
127
|
+
__slots__ = ("body", "world", "version", "pattern", "meta", "e")
|
|
128
|
+
|
|
129
|
+
def __init__(self, body: bytes, world: str, version: int,
|
|
130
|
+
pattern: str, meta: dict[str, str], e: Elastik):
|
|
131
|
+
self.body = body
|
|
132
|
+
self.world = world
|
|
133
|
+
self.version = version
|
|
134
|
+
self.pattern = pattern
|
|
135
|
+
self.meta = meta
|
|
136
|
+
self.e = e
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ── core dispatch (used by both plugin and sidecar shape) ──────────
|
|
140
|
+
|
|
141
|
+
def _call_handler(handler: Callable[..., Any], ctx: Ctx) -> Any:
|
|
142
|
+
"""Pass only the kwargs the handler signature asks for."""
|
|
143
|
+
sig = inspect.signature(handler)
|
|
144
|
+
accepts = sig.parameters.keys()
|
|
145
|
+
candidates = {
|
|
146
|
+
"world": ctx.world,
|
|
147
|
+
"version": ctx.version,
|
|
148
|
+
"pattern": ctx.pattern,
|
|
149
|
+
"meta": ctx.meta,
|
|
150
|
+
"e": ctx.e,
|
|
151
|
+
}
|
|
152
|
+
kwargs = {k: v for k, v in candidates.items() if k in accepts}
|
|
153
|
+
has_var = any(
|
|
154
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
155
|
+
)
|
|
156
|
+
if has_var:
|
|
157
|
+
kwargs = candidates
|
|
158
|
+
return handler(ctx.body, **kwargs)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _run_action(result: Any, ctx: Ctx) -> None:
|
|
162
|
+
if result is None:
|
|
163
|
+
return
|
|
164
|
+
if isinstance(result, Action):
|
|
165
|
+
result.execute(ctx)
|
|
166
|
+
return
|
|
167
|
+
if isinstance(result, (list, tuple)):
|
|
168
|
+
for item in result:
|
|
169
|
+
if isinstance(item, Action):
|
|
170
|
+
item.execute(ctx)
|
|
171
|
+
return
|
|
172
|
+
# Anything else — ignored. Reactor is forgiving by design; the
|
|
173
|
+
# PUT response shouldn't fail because a handler returned a string.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _dispatch(world: str, body: bytes, version: int, meta: dict[str, str],
|
|
177
|
+
e: Elastik) -> None:
|
|
178
|
+
"""Find every matching handler, run it, execute its action(s)."""
|
|
179
|
+
for pattern, handler in _routes.items():
|
|
180
|
+
if not _matches(pattern, world):
|
|
181
|
+
continue
|
|
182
|
+
ctx = Ctx(body=body, world=world, version=version,
|
|
183
|
+
pattern=pattern, meta=meta, e=e)
|
|
184
|
+
try:
|
|
185
|
+
result = _call_handler(handler, ctx)
|
|
186
|
+
_run_action(result, ctx)
|
|
187
|
+
except Exception as ex:
|
|
188
|
+
print(f" reactor [{pattern}] raised: "
|
|
189
|
+
f"{type(ex).__name__}: {ex}", flush=True)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ── shape 1: emit ROUTES + handle for elastik-python plugin runtime ─
|
|
193
|
+
|
|
194
|
+
def finalize(elastik_url: str = "http://localhost:3005",
|
|
195
|
+
token: str = "") -> tuple[list[str], Callable]:
|
|
196
|
+
"""Return (ROUTES, handle) for an elastik-python plugin.
|
|
197
|
+
|
|
198
|
+
Use at the bottom of a plugin file:
|
|
199
|
+
|
|
200
|
+
from sdk.reactor import listen, MoveTo, finalize
|
|
201
|
+
@listen("/home/inbox/*")
|
|
202
|
+
def triage(body): return MoveTo("/home/archive/")
|
|
203
|
+
ROUTES, handle = finalize()
|
|
204
|
+
"""
|
|
205
|
+
e = Elastik(elastik_url, token=token)
|
|
206
|
+
routes = list(_routes.keys())
|
|
207
|
+
|
|
208
|
+
async def handle(method: str, body: Any, params: dict) -> dict:
|
|
209
|
+
if method != "PUT":
|
|
210
|
+
return {"_status": 200, "ok": True, "skipped": method}
|
|
211
|
+
scope = params.get("_scope", {})
|
|
212
|
+
world = scope.get("path", "")
|
|
213
|
+
version = int(scope.get("_version", 0))
|
|
214
|
+
meta = {
|
|
215
|
+
k.decode().lower(): v.decode()
|
|
216
|
+
for k, v in scope.get("headers", [])
|
|
217
|
+
if k.decode().lower().startswith("x-meta-")
|
|
218
|
+
}
|
|
219
|
+
body_bytes = body if isinstance(body, (bytes, bytearray)) else \
|
|
220
|
+
(body or "").encode("utf-8")
|
|
221
|
+
_dispatch(world, body_bytes, version, meta, e)
|
|
222
|
+
return {"_status": 200, "ok": True}
|
|
223
|
+
|
|
224
|
+
return routes, handle
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ── shape 2: standalone sidecar HTTP server ────────────────────────
|
|
228
|
+
|
|
229
|
+
def serve(host: str = "127.0.0.1", port: int = 3200,
|
|
230
|
+
elastik_url: str = "http://localhost:3105",
|
|
231
|
+
token: str = "") -> None:
|
|
232
|
+
"""Run an HTTP server that elastik-core's fanout posts to.
|
|
233
|
+
|
|
234
|
+
elastik-core sends:
|
|
235
|
+
POST /<anything>
|
|
236
|
+
X-Elastik-World: <url path>
|
|
237
|
+
X-Elastik-Version: <int>
|
|
238
|
+
X-Elastik-Pattern: <matched pattern>
|
|
239
|
+
X-Meta-*: <forwarded>
|
|
240
|
+
body: the original PUT body
|
|
241
|
+
"""
|
|
242
|
+
e = Elastik(elastik_url, token=token)
|
|
243
|
+
state = {"e": e}
|
|
244
|
+
|
|
245
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
246
|
+
def do_POST(self):
|
|
247
|
+
n = int(self.headers.get("Content-Length", "0"))
|
|
248
|
+
body = self.rfile.read(n)
|
|
249
|
+
world = self.headers.get("X-Elastik-World", "")
|
|
250
|
+
version = int(self.headers.get("X-Elastik-Version", "0") or "0")
|
|
251
|
+
pattern = self.headers.get("X-Elastik-Pattern", "")
|
|
252
|
+
meta = {
|
|
253
|
+
k.lower(): v for k, v in self.headers.items()
|
|
254
|
+
if k.lower().startswith("x-meta-")
|
|
255
|
+
}
|
|
256
|
+
# reactor matches all registered patterns against the world,
|
|
257
|
+
# not just the one elastik-core sent (X-Elastik-Pattern is
|
|
258
|
+
# informational — one elastik-core listener URL may serve
|
|
259
|
+
# many @listen patterns in one process).
|
|
260
|
+
_dispatch(world, body, version, meta, state["e"])
|
|
261
|
+
self.send_response(200)
|
|
262
|
+
self.send_header("Content-Length", "0")
|
|
263
|
+
self.end_headers()
|
|
264
|
+
|
|
265
|
+
# Quiet by default
|
|
266
|
+
def log_message(self, *_): pass
|
|
267
|
+
|
|
268
|
+
srv = http.server.ThreadingHTTPServer((host, port), Handler)
|
|
269
|
+
print(f"reactor sidecar listening on http://{host}:{port}/", flush=True)
|
|
270
|
+
print(f" elastik backend: {elastik_url}", flush=True)
|
|
271
|
+
print(f" registered patterns: {list(_routes.keys())}", flush=True)
|
|
272
|
+
try:
|
|
273
|
+
srv.serve_forever()
|
|
274
|
+
except KeyboardInterrupt:
|
|
275
|
+
srv.shutdown()
|
elastik/sdk.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""L1 — atom bindings for elastik-core.
|
|
2
|
+
|
|
3
|
+
This is the SDK. It is not the frontend. The frontend is what you build
|
|
4
|
+
*with* these atoms (see sdk.reactor + your own scripts).
|
|
5
|
+
|
|
6
|
+
`e.put("/home/x", data)` is "Lumos" — a declaration. Underneath, the
|
|
7
|
+
runtime may store, audit, fan out, broadcast, cache, log. The caller
|
|
8
|
+
says three words.
|
|
9
|
+
|
|
10
|
+
stdlib only — no httpx, no requests. urllib + json. ~80 lines.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.parse
|
|
17
|
+
import urllib.request
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ElastikError(Exception):
|
|
22
|
+
def __init__(self, status: int, body: bytes):
|
|
23
|
+
self.status = status
|
|
24
|
+
self.body = body
|
|
25
|
+
super().__init__(f"elastik {status}: {body[:200]!r}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Elastik:
|
|
29
|
+
"""Pythonic bindings to elastik-core's HTTP atoms.
|
|
30
|
+
|
|
31
|
+
>>> e = Elastik("http://localhost:3105", token="t2")
|
|
32
|
+
>>> e.put("/home/note", b"hello") # PUT, returns dict
|
|
33
|
+
>>> e.get("/home/note", raw=True) # GET ?raw, returns bytes
|
|
34
|
+
>>> e.head("/home/note") # HEAD, returns headers dict
|
|
35
|
+
>>> e.delete("/home/note") # DELETE
|
|
36
|
+
>>> e.list() # GET /proc/worlds
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, url: str = "http://localhost:3005", token: str = ""):
|
|
40
|
+
self.url = url.rstrip("/")
|
|
41
|
+
self.token = token
|
|
42
|
+
|
|
43
|
+
# ── atoms ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def put(self, path: str, data: bytes | str, **meta: Any) -> dict:
|
|
46
|
+
"""PUT body to path. kwargs become X-Meta-* headers."""
|
|
47
|
+
body = data.encode("utf-8") if isinstance(data, str) else data
|
|
48
|
+
headers = {f"X-Meta-{k.replace('_', '-')}": str(v) for k, v in meta.items()}
|
|
49
|
+
return self._json("PUT", path, body, headers)
|
|
50
|
+
|
|
51
|
+
def get(self, path: str, raw: bool = False) -> Any:
|
|
52
|
+
"""GET path. raw=True returns bytes; otherwise the JSON envelope."""
|
|
53
|
+
suffix = "?raw" if raw else ""
|
|
54
|
+
status, _, body = self._raw("GET", path + suffix)
|
|
55
|
+
if status == 404:
|
|
56
|
+
raise ElastikError(404, body)
|
|
57
|
+
if status >= 400:
|
|
58
|
+
raise ElastikError(status, body)
|
|
59
|
+
if raw:
|
|
60
|
+
return body
|
|
61
|
+
return json.loads(body)
|
|
62
|
+
|
|
63
|
+
def head(self, path: str) -> dict[str, str]:
|
|
64
|
+
"""HEAD path. Returns headers as a lowercased dict."""
|
|
65
|
+
status, headers, _ = self._raw("HEAD", path)
|
|
66
|
+
if status == 404:
|
|
67
|
+
raise ElastikError(404, b"")
|
|
68
|
+
return headers
|
|
69
|
+
|
|
70
|
+
def delete(self, path: str) -> bool:
|
|
71
|
+
"""DELETE path. Returns True on 204, False on 404."""
|
|
72
|
+
status, _, _ = self._raw("DELETE", path)
|
|
73
|
+
if status == 204:
|
|
74
|
+
return True
|
|
75
|
+
if status == 404:
|
|
76
|
+
return False
|
|
77
|
+
raise ElastikError(status, b"")
|
|
78
|
+
|
|
79
|
+
def list(self) -> list[str]:
|
|
80
|
+
"""GET /proc/worlds. Returns list of world names."""
|
|
81
|
+
status, _, body = self._raw("GET", "/proc/worlds")
|
|
82
|
+
if status >= 400:
|
|
83
|
+
raise ElastikError(status, body)
|
|
84
|
+
return [w["name"] for w in json.loads(body)]
|
|
85
|
+
|
|
86
|
+
def shaped(self, path: str, accept: str = "text/html",
|
|
87
|
+
intent: str = "") -> bytes:
|
|
88
|
+
"""GET /shaped/<path> with Accept + X-Semantic-Intent. Forwards
|
|
89
|
+
to whatever shaper sidecar elastik routes /shaped/ to. Bytes back."""
|
|
90
|
+
headers = {"Accept": accept}
|
|
91
|
+
if intent:
|
|
92
|
+
headers["X-Semantic-Intent"] = intent
|
|
93
|
+
status, _, body = self._raw("GET", f"/shaped{path}", headers=headers)
|
|
94
|
+
if status >= 400:
|
|
95
|
+
raise ElastikError(status, body)
|
|
96
|
+
return body
|
|
97
|
+
|
|
98
|
+
# ── transport ─────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def _raw(self, method: str, path: str, body: bytes | None = None,
|
|
101
|
+
headers: dict[str, str] | None = None) -> tuple[int, dict[str, str], bytes]:
|
|
102
|
+
url = self.url + (path if path.startswith("/") else "/" + path)
|
|
103
|
+
h = dict(headers or {})
|
|
104
|
+
if self.token:
|
|
105
|
+
h.setdefault("Authorization", f"Bearer {self.token}")
|
|
106
|
+
req = urllib.request.Request(url, data=body, method=method, headers=h)
|
|
107
|
+
try:
|
|
108
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
109
|
+
return (
|
|
110
|
+
r.status,
|
|
111
|
+
{k.lower(): v for k, v in r.headers.items()},
|
|
112
|
+
r.read(),
|
|
113
|
+
)
|
|
114
|
+
except urllib.error.HTTPError as e:
|
|
115
|
+
return (
|
|
116
|
+
e.code,
|
|
117
|
+
{k.lower(): v for k, v in (e.headers or {}).items()},
|
|
118
|
+
e.read() if e.fp else b"",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _json(self, method: str, path: str, body: bytes | None,
|
|
122
|
+
headers: dict[str, str] | None = None) -> dict:
|
|
123
|
+
status, _, raw = self._raw(method, path, body, headers)
|
|
124
|
+
if status >= 400:
|
|
125
|
+
raise ElastikError(status, raw)
|
|
126
|
+
try:
|
|
127
|
+
return json.loads(raw)
|
|
128
|
+
except (ValueError, json.JSONDecodeError):
|
|
129
|
+
return {"raw": raw}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: elastik
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Pastebin with HMAC that accidentally became a web OS. pip install one ring.
|
|
5
|
+
Author: Ranger Chen
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rangersui/Elastik
|
|
8
|
+
Project-URL: Repository, https://github.com/rangersui/Elastik
|
|
9
|
+
Keywords: http,pastebin,local-first,ai,web-os,rust
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: POSIX
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Rust
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# elastik
|
|
23
|
+
|
|
24
|
+
> `pip install elastik` — and you get a Rust web OS pretending to be a Python module.
|
|
25
|
+
|
|
26
|
+
Same trick as NumPy: the `.whl` ships a precompiled binary inside the
|
|
27
|
+
package; the Python layer is a thin client. You write Python; a Rust
|
|
28
|
+
server does the work; you never see Cargo.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import elastik
|
|
32
|
+
|
|
33
|
+
e = elastik.start(token="x") # spawn bundled rust core
|
|
34
|
+
e.put("/home/note", "hello")
|
|
35
|
+
print(e.get("/home/note", raw=True)) # b'hello'
|
|
36
|
+
elastik.stop()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Module-level too — point at any running elastik via `ELASTIK_URL`:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import elastik
|
|
43
|
+
elastik.put("/home/note", "hi")
|
|
44
|
+
print(elastik.get("/home/note", raw=True))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Reactor (declarative event handlers):
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import elastik
|
|
51
|
+
|
|
52
|
+
@elastik.listen("/home/inbox/*")
|
|
53
|
+
def triage(body, world, meta):
|
|
54
|
+
if b"urgent" in body:
|
|
55
|
+
return elastik.Reply(f"/home/alerts/{world.rsplit('/',1)[-1]}", body)
|
|
56
|
+
return elastik.Archive()
|
|
57
|
+
|
|
58
|
+
elastik.serve(port=3200)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Set elastik-core to fan out to that port:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
ELASTIK_LISTENERS="/home/inbox/*=http://localhost:3200/triage" \
|
|
65
|
+
elastik run --port 3105 --token t
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install elastik
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
(For now: build from source — see "Build from source" below.)
|
|
75
|
+
|
|
76
|
+
## CLI
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
elastik info # show bundled-binary path + size
|
|
80
|
+
elastik run --port 3105 # spawn the bundled core, block
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## What's bundled
|
|
84
|
+
|
|
85
|
+
- **Rust binary** at `elastik/_bin/elastik-core[.exe]`
|
|
86
|
+
- HTTP + SQLite + HMAC + auth + harvard gate + listener fanout
|
|
87
|
+
- ~4.3 MB stripped
|
|
88
|
+
- source: `Elastik-server/Elastik-core/`
|
|
89
|
+
- **Python SDK** (`elastik.sdk.Elastik`)
|
|
90
|
+
- atom bindings: `put`, `get`, `head`, `delete`, `list`, `shaped`
|
|
91
|
+
- stdlib only (urllib + json), no requests / httpx
|
|
92
|
+
- **Python reactor sugar** (`elastik.reactor`)
|
|
93
|
+
- `@listen(pattern)` decorator
|
|
94
|
+
- `MoveTo`, `Reply`, `Archive`, `Drop` action classes
|
|
95
|
+
- two deployment shapes: as elastik-python plugin (`finalize()`),
|
|
96
|
+
or as standalone sidecar (`serve()`)
|
|
97
|
+
|
|
98
|
+
## The PyTorch parallel
|
|
99
|
+
|
|
100
|
+
| layer | what | who writes |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| L0 — Rust core | bedrock atoms (HTTP/SQLite/HMAC) | one human, once |
|
|
103
|
+
| L1 — Python SDK | `elastik.put`, `elastik.get`, … | AI, once |
|
|
104
|
+
| L1.5 — plugin runtime | (in elastik-server, hot-reload) | already exists |
|
|
105
|
+
| L2 — reactor sugar | `@elastik.listen` + Action classes | AI, once |
|
|
106
|
+
| L3 — agent programs | composition: if/for + L1 + L2 + ai | AI, daily |
|
|
107
|
+
|
|
108
|
+
`elastik.put()` is `np.array()`. The frontend isn't this call — it's
|
|
109
|
+
what you compose with these atoms. See [Elastik-core/README.md] for
|
|
110
|
+
the full architecture.
|
|
111
|
+
|
|
112
|
+
## Build from source
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 1. build the rust core
|
|
116
|
+
cd Elastik-core
|
|
117
|
+
cargo build --release
|
|
118
|
+
|
|
119
|
+
# 2. drop the binary into the python package
|
|
120
|
+
cp target/release/elastik-core* ../elastik-pip/src/elastik/_bin/
|
|
121
|
+
|
|
122
|
+
# 3. install the python package
|
|
123
|
+
cd ../elastik-pip
|
|
124
|
+
pip install -e .
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Then:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -c "import elastik; e = elastik.start(); print(e.put('/home/x', 'hi'))"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Status
|
|
134
|
+
|
|
135
|
+
Sketch. One platform at a time (this build is for the host you ran
|
|
136
|
+
`cargo build` on). Multi-platform wheels via `cibuildwheel` is the
|
|
137
|
+
next step.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
elastik/__init__.py,sha256=Jz7gbBLvz-vAxDjJzOigbZov-6mfnV3reH7oAl_UH-c,4716
|
|
2
|
+
elastik/__main__.py,sha256=sWVfw-_KE4h5PexCVTfwmn4tqNen-XOyssEyBKL8NrE,2017
|
|
3
|
+
elastik/_spawn.py,sha256=PH_wYunv9PRhxwSXi2k7Qg3vdzB4Hdtkbe6nec778qc,4422
|
|
4
|
+
elastik/reactor.py,sha256=lZhac69xjUqLMxMGh9bM1U280J8h1lx0Hwza7Vmb800,9466
|
|
5
|
+
elastik/sdk.py,sha256=9n34SAhFxF4bWJtgTdIKiZICF15RBZNyjlZpueq4oKo,5138
|
|
6
|
+
elastik/_bin/elastik-core.exe,sha256=dGY9pCRVty7yfbSKLBvyfIGg3F6FyVrWScJxeWcAYQw,4459008
|
|
7
|
+
elastik-0.0.1.dist-info/METADATA,sha256=4m_upx-ZCl1Vw2EPMxl67DEJQPjFDH0qdLXX7EeC9AE,4074
|
|
8
|
+
elastik-0.0.1.dist-info/WHEEL,sha256=QR8DNjG6Lr6bNErJWJgF4dP2dJ2N7NpY-BWly1OvcTM,97
|
|
9
|
+
elastik-0.0.1.dist-info/entry_points.txt,sha256=fCrNw6tXUegXaxbef2tOBuh7bKnYixVipJlIAEtNdXM,50
|
|
10
|
+
elastik-0.0.1.dist-info/top_level.txt,sha256=noR3uFxC-xJg60tpDis-duq7JsNUwkYM4Zw626omNzM,8
|
|
11
|
+
elastik-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
elastik
|