osmem-server 0.1.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.
@@ -0,0 +1,182 @@
1
+ """In-memory OpenSearch-compatible server for tests.
2
+
3
+ from osmem_server import OsmemServer
4
+
5
+ with OsmemServer.start(seed=["testdata/seed"], freeze=True) as server:
6
+ with server.clone() as clone:
7
+ client = OpenSearch(hosts=[clone.url])
8
+ ...
9
+
10
+ The pytest plugin (loaded automatically) provides the ``osmem_server``
11
+ (session) and ``osmem_clone`` (function) fixtures; see ``osmem_server.pytest_plugin``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import os
18
+ import platform
19
+ import subprocess
20
+ import sys
21
+ import threading
22
+ import time
23
+ import urllib.error
24
+ import urllib.request
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any, Iterable, Optional, Sequence, Union
28
+
29
+ __all__ = ["OsmemServer", "OsmemClone", "OsmemError", "resolve_binary"]
30
+
31
+ __version__ = "0.1.1"
32
+
33
+
34
+ class OsmemError(RuntimeError):
35
+ """Raised when the server cannot be started or a request fails."""
36
+
37
+
38
+ def resolve_binary(binary: Optional[str] = None) -> str:
39
+ """Locate osmem-server: explicit path, OSMEM_SERVER_BIN, then the bundled binary."""
40
+ if binary:
41
+ return binary
42
+ env = os.environ.get("OSMEM_SERVER_BIN")
43
+ if env:
44
+ return env
45
+ name = "osmem-server.exe" if sys.platform == "win32" else "osmem-server"
46
+ bundled = Path(__file__).parent / "bin" / name
47
+ if bundled.exists():
48
+ return str(bundled)
49
+ raise OsmemError(
50
+ f"osmem: no server binary bundled for {sys.platform}/{platform.machine()}; "
51
+ "install a platform wheel or set OSMEM_SERVER_BIN"
52
+ )
53
+
54
+
55
+ def _request(method: str, url: str, body: Any = None, timeout: float = 30.0) -> Any:
56
+ data = None if body is None else json.dumps(body).encode()
57
+ req = urllib.request.Request(url, data=data, method=method, headers={"Content-Type": "application/json"})
58
+ try:
59
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
60
+ text = resp.read().decode()
61
+ except urllib.error.HTTPError as e:
62
+ text = e.read().decode()
63
+ try:
64
+ err = json.loads(text).get("error", text)
65
+ detail = f"{err.get('type')}: {err.get('reason')}" if isinstance(err, dict) else str(err)
66
+ except ValueError:
67
+ detail = text
68
+ raise OsmemError(f"osmem: {method} {url}: {e.code} {detail}") from None
69
+ return json.loads(text) if text else {}
70
+
71
+
72
+ @dataclass
73
+ class OsmemClone:
74
+ """A clone of the base cluster served on its own port."""
75
+
76
+ server: "OsmemServer"
77
+ id: str
78
+ url: str
79
+
80
+ def close(self) -> None:
81
+ try:
82
+ _request("DELETE", f"{self.server.url}/_osmem/clones/{self.id}")
83
+ except OsmemError:
84
+ pass
85
+
86
+ def __enter__(self) -> "OsmemClone":
87
+ return self
88
+
89
+ def __exit__(self, *exc: Any) -> None:
90
+ self.close()
91
+
92
+
93
+ @dataclass
94
+ class OsmemServer:
95
+ """A running osmem-server process hosting a base cluster."""
96
+
97
+ process: subprocess.Popen
98
+ url: str
99
+ pid: int
100
+ version: str
101
+ japanese: bool
102
+ indices: list = field(default_factory=list)
103
+
104
+ @classmethod
105
+ def start(
106
+ cls,
107
+ seed: Union[None, str, os.PathLike, Sequence[Union[str, os.PathLike]]] = None,
108
+ *,
109
+ freeze: bool = False,
110
+ japanese: bool = True,
111
+ addr: Optional[str] = None,
112
+ binary: Optional[str] = None,
113
+ startup_timeout: float = 30.0,
114
+ ) -> "OsmemServer":
115
+ """Start the server and wait until it is ready."""
116
+ args = [resolve_binary(binary), "--parent-pid", str(os.getpid())]
117
+ seeds: Iterable[Any] = [] if seed is None else ([seed] if isinstance(seed, (str, os.PathLike)) else seed)
118
+ for s in seeds:
119
+ args += ["--seed", os.fspath(s)]
120
+ if freeze:
121
+ args.append("--freeze")
122
+ if not japanese:
123
+ args.append("--no-ja")
124
+ if addr:
125
+ args += ["--addr", addr]
126
+ proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, text=True)
127
+ ready: dict = {}
128
+ error: list = []
129
+
130
+ def reader() -> None:
131
+ assert proc.stdout is not None
132
+ for line in proc.stdout:
133
+ try:
134
+ msg = json.loads(line)
135
+ except ValueError:
136
+ continue
137
+ if isinstance(msg, dict) and "url" in msg:
138
+ ready.update(msg)
139
+ return
140
+ error.append("server exited during startup")
141
+
142
+ t = threading.Thread(target=reader, daemon=True)
143
+ t.start()
144
+ t.join(startup_timeout)
145
+ if not ready:
146
+ proc.kill()
147
+ raise OsmemError("osmem: " + (error[0] if error else f"server did not start within {startup_timeout} s"))
148
+ return cls(proc, ready["url"], ready.get("pid", proc.pid), ready.get("version", ""), ready.get("japanese", False), list(ready.get("indices", [])))
149
+
150
+ def clone(self) -> OsmemClone:
151
+ """Create a clone served on its own port; freezes the base."""
152
+ res = _request("POST", f"{self.url}/_osmem/clones")
153
+ return OsmemClone(self, res["id"], res["url"])
154
+
155
+ def freeze(self) -> None:
156
+ _request("POST", f"{self.url}/_osmem/base/freeze")
157
+
158
+ def request(self, method: str, path: str, body: Any = None) -> Any:
159
+ """Any request against the base, decoded as JSON; raises OsmemError on error status."""
160
+ return _request(method, f"{self.url}{path}", body)
161
+
162
+ def close(self, grace: float = 5.0) -> None:
163
+ """Stop the process: close stdin, then kill after the grace period."""
164
+ if self.process.poll() is not None:
165
+ return
166
+ try:
167
+ if self.process.stdin:
168
+ self.process.stdin.close()
169
+ except OSError:
170
+ pass
171
+ deadline = time.monotonic() + grace
172
+ while self.process.poll() is None and time.monotonic() < deadline:
173
+ time.sleep(0.05)
174
+ if self.process.poll() is None:
175
+ self.process.kill()
176
+ self.process.wait()
177
+
178
+ def __enter__(self) -> "OsmemServer":
179
+ return self
180
+
181
+ def __exit__(self, *exc: Any) -> None:
182
+ self.close()
Binary file
@@ -0,0 +1,51 @@
1
+ """pytest fixtures for osmem.
2
+
3
+ Configure the seed in pytest.ini / pyproject.toml::
4
+
5
+ [tool.pytest.ini_options]
6
+ osmem_seed = ["testdata/seed"]
7
+ osmem_freeze = true
8
+
9
+ or override the ``osmem_server`` fixture in conftest.py to call
10
+ ``OsmemServer.start`` yourself. Tests take ``osmem_clone`` (a fresh clone
11
+ per test, closed afterwards) or ``osmem_server`` (shared, read-only use).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import pytest
17
+
18
+ from osmem_server import OsmemServer
19
+
20
+
21
+ def pytest_addoption(parser: pytest.Parser) -> None:
22
+ parser.addini("osmem_seed", "osmem seed directories or .ndjson files", type="paths", default=[])
23
+ parser.addini("osmem_freeze", "freeze the osmem base after seeding", type="bool", default=True)
24
+ parser.addini("osmem_japanese", "enable Japanese analysis in osmem", type="bool", default=True)
25
+
26
+
27
+ @pytest.fixture(scope="session")
28
+ def osmem_server(request: pytest.FixtureRequest):
29
+ """A session-wide osmem-server seeded from the osmem_seed ini option."""
30
+ cfg = request.config
31
+ server = OsmemServer.start(
32
+ seed=[str(p) for p in cfg.getini("osmem_seed")],
33
+ freeze=cfg.getini("osmem_freeze"),
34
+ japanese=cfg.getini("osmem_japanese"),
35
+ )
36
+ yield server
37
+ server.close()
38
+
39
+
40
+ @pytest.fixture
41
+ def osmem_clone(osmem_server: OsmemServer):
42
+ """A clone of the base for one test; use ``osmem_clone.url`` with a client."""
43
+ clone = osmem_server.clone()
44
+ yield clone
45
+ clone.close()
46
+
47
+
48
+ @pytest.fixture
49
+ def osmem_url(osmem_clone) -> str:
50
+ """The clone URL, for tests that only need the address."""
51
+ return osmem_clone.url
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: osmem-server
3
+ Version: 0.1.1
4
+ Summary: In-memory OpenSearch-compatible server for tests (osmem-server launcher, pytest fixtures)
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/shibukawa/osmem
7
+ Keywords: opensearch,elasticsearch,testing,pytest,in-memory
8
+ Classifier: Framework :: Pytest
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Software Development :: Testing
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Dynamic: license-file
15
+
16
+ # osmem-server
17
+
18
+ In-memory OpenSearch-compatible server for tests (`pip install osmem-server`).
19
+ The wheel bundles the `osmem-server` binary for your platform; no Docker,
20
+ no JVM. The import name is `osmem_server`.
21
+
22
+ ```python
23
+ # pytest.ini / pyproject.toml
24
+ [tool.pytest.ini_options]
25
+ osmem_seed = ["testdata/seed"]
26
+ ```
27
+
28
+ ```python
29
+ from opensearchpy import OpenSearch
30
+
31
+ def test_search(osmem_clone):
32
+ client = OpenSearch(hosts=[osmem_clone.url])
33
+ client.index(index="products", id="x", body={"name": "new"}, refresh=True)
34
+ assert client.count(index="products")["count"] == 6
35
+ ```
36
+
37
+ Each test gets its own clone of the seeded base cluster (milliseconds,
38
+ copy-on-write); the server itself starts once per session and exits with
39
+ the test process. Without pytest:
40
+
41
+ ```python
42
+ from osmem_server import OsmemServer
43
+
44
+ with OsmemServer.start(seed=["testdata/seed"]) as server, server.clone() as clone:
45
+ ...
46
+ ```
47
+
48
+ Set `OSMEM_SERVER_BIN` to use a locally built binary. Seed layout and the
49
+ management API are documented in the
50
+ [osmem repository](https://github.com/shibukawa/osmem).
51
+
52
+ Full guide: [English](https://shibukawa.github.io/osmem/python/) · [日本語](https://shibukawa.github.io/osmem/ja/python/)
@@ -0,0 +1,9 @@
1
+ osmem_server/__init__.py,sha256=F9WK5e1NJ4dbBtXF6uGizJg80XgYHjiMDgM0eCOtd7Q,6101
2
+ osmem_server/pytest_plugin.py,sha256=_g0Rx7nIUr6VFyKsaQagT2IZfbaHW7EMTpRiRMSkWuY,1598
3
+ osmem_server/bin/osmem-server.exe,sha256=qkrL7_FWFoxMoNdtu94tCciq9jy84IZRMTFENqcgXpA,27565056
4
+ osmem_server-0.1.1.dist-info/licenses/LICENSE,sha256=-EG0igTVeHkIYfDKGDH47dLaukAH-bCdolHP1EePNDg,1074
5
+ osmem_server-0.1.1.dist-info/METADATA,sha256=j_fMIc-yeiWHpW6IPnqLw87heoTPFlAc_-eyZMjxq3s,1738
6
+ osmem_server-0.1.1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
7
+ osmem_server-0.1.1.dist-info/entry_points.txt,sha256=wxcpLmhcQ5vEXAgljqllV7UBE7kiEtwc658ZrJknJuc,46
8
+ osmem_server-0.1.1.dist-info/top_level.txt,sha256=w3zz1pOxTpWKZcrNdZqg8yBCmuW6w01Wy0CE1ccOVH0,13
9
+ osmem_server-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
5
+
@@ -0,0 +1,2 @@
1
+ [pytest11]
2
+ osmem = osmem_server.pytest_plugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yoshiki Shibukawa
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.
@@ -0,0 +1 @@
1
+ osmem_server