neo4j-test-server 0.1.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.
@@ -0,0 +1,19 @@
1
+ # Gradle
2
+ build/
3
+ .gradle/
4
+
5
+ # IDE
6
+ .idea/
7
+ *.iml
8
+ *.ipr
9
+ *.iws
10
+ .vscode/
11
+
12
+ # Python
13
+ __pycache__/
14
+ *.pyc
15
+ .venv/
16
+ venv/
17
+
18
+ # OS
19
+ .DS_Store
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.5
2
+ Name: neo4j-test-server
3
+ Version: 0.1.0
4
+ Summary: Disposable in-process Neo4j test server for Python integration tests
5
+ Author: vshofar
6
+ License: MIT
7
+ Keywords: graph-database,integration-tests,neo4j,testing
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Testing
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: neo4j>=5.0
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=7.0; extra == 'test'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # neo4j-test-server (Python)
20
+
21
+ Pacote Python para subir um servidor Neo4j descartável em testes de integração.
22
+
23
+ O pacote baixa automaticamente o fat JAR do
24
+ [GitHub Releases](https://github.com/vshofar/neo4j-test-server/releases) na
25
+ primeira utilização e o cacheia localmente. Em runtime, apenas **Java 21+** é
26
+ necessário — nada de Gradle ou Maven.
27
+
28
+ ## Instalação
29
+
30
+ ```bash
31
+ pip install neo4j-test-server
32
+ ```
33
+
34
+ ## Uso
35
+
36
+ ### Context manager
37
+
38
+ ```python
39
+ from neo4j_test_server import Neo4jTestServer
40
+ from neo4j import GraphDatabase
41
+
42
+ with Neo4jTestServer() as server:
43
+ driver = GraphDatabase.driver(
44
+ server.bolt_uri,
45
+ auth=(server.username, server.password),
46
+ )
47
+ with driver.session() as session:
48
+ session.run("CREATE (:Test {name: 'hello'})")
49
+ driver.close()
50
+ # servidor encerrado automaticamente
51
+ ```
52
+
53
+ ### Fixture pytest
54
+
55
+ O pacote registra automaticamente uma fixture `neo4j_test_server`
56
+ (session-scoped) via entry point `pytest11`:
57
+
58
+ ```python
59
+ def test_something(neo4j_test_server):
60
+ driver = GraphDatabase.driver(
61
+ neo4j_test_server.bolt_uri,
62
+ auth=(neo4j_test_server.username, neo4j_test_server.password),
63
+ )
64
+ # ...
65
+ ```
66
+
67
+ ## Configuração
68
+
69
+ | Variável de ambiente | Descrição |
70
+ |---|---|
71
+ | `NEO4J_TEST_SERVER_JAR` | Caminho para um JAR local (pula download) |
72
+ | `NEO4J_TEST_SERVER_CACHE` | Diretório de cache (padrão: `~/.cache/neo4j-test-server`) |
73
+ | `NEO4J_TEST_SERVER_JAR_VERSION` | Versão do JAR a baixar (padrão: versão fixada no pacote) |
74
+ | `JAVA_HOME` / `java` no PATH | Runtime Java 21+ |
75
+
76
+ ## Desenvolvimento local
77
+
78
+ Para testar o pacote contra um JAR buildado localmente:
79
+
80
+ ```bash
81
+ # na raiz do monorepo
82
+ ./gradlew shadowJar
83
+
84
+ # aponte o pacote para o JAR local
85
+ export NEO4J_TEST_SERVER_JAR=$PWD/build/libs/neo4j-test-server-all.jar
86
+
87
+ # instale o pacote em modo editável
88
+ pip install -e python/
89
+ ```
@@ -0,0 +1,71 @@
1
+ # neo4j-test-server (Python)
2
+
3
+ Pacote Python para subir um servidor Neo4j descartável em testes de integração.
4
+
5
+ O pacote baixa automaticamente o fat JAR do
6
+ [GitHub Releases](https://github.com/vshofar/neo4j-test-server/releases) na
7
+ primeira utilização e o cacheia localmente. Em runtime, apenas **Java 21+** é
8
+ necessário — nada de Gradle ou Maven.
9
+
10
+ ## Instalação
11
+
12
+ ```bash
13
+ pip install neo4j-test-server
14
+ ```
15
+
16
+ ## Uso
17
+
18
+ ### Context manager
19
+
20
+ ```python
21
+ from neo4j_test_server import Neo4jTestServer
22
+ from neo4j import GraphDatabase
23
+
24
+ with Neo4jTestServer() as server:
25
+ driver = GraphDatabase.driver(
26
+ server.bolt_uri,
27
+ auth=(server.username, server.password),
28
+ )
29
+ with driver.session() as session:
30
+ session.run("CREATE (:Test {name: 'hello'})")
31
+ driver.close()
32
+ # servidor encerrado automaticamente
33
+ ```
34
+
35
+ ### Fixture pytest
36
+
37
+ O pacote registra automaticamente uma fixture `neo4j_test_server`
38
+ (session-scoped) via entry point `pytest11`:
39
+
40
+ ```python
41
+ def test_something(neo4j_test_server):
42
+ driver = GraphDatabase.driver(
43
+ neo4j_test_server.bolt_uri,
44
+ auth=(neo4j_test_server.username, neo4j_test_server.password),
45
+ )
46
+ # ...
47
+ ```
48
+
49
+ ## Configuração
50
+
51
+ | Variável de ambiente | Descrição |
52
+ |---|---|
53
+ | `NEO4J_TEST_SERVER_JAR` | Caminho para um JAR local (pula download) |
54
+ | `NEO4J_TEST_SERVER_CACHE` | Diretório de cache (padrão: `~/.cache/neo4j-test-server`) |
55
+ | `NEO4J_TEST_SERVER_JAR_VERSION` | Versão do JAR a baixar (padrão: versão fixada no pacote) |
56
+ | `JAVA_HOME` / `java` no PATH | Runtime Java 21+ |
57
+
58
+ ## Desenvolvimento local
59
+
60
+ Para testar o pacote contra um JAR buildado localmente:
61
+
62
+ ```bash
63
+ # na raiz do monorepo
64
+ ./gradlew shadowJar
65
+
66
+ # aponte o pacote para o JAR local
67
+ export NEO4J_TEST_SERVER_JAR=$PWD/build/libs/neo4j-test-server-all.jar
68
+
69
+ # instale o pacote em modo editável
70
+ pip install -e python/
71
+ ```
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "neo4j-test-server"
7
+ version = "0.1.0"
8
+ description = "Disposable in-process Neo4j test server for Python integration tests"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "vshofar" }]
13
+ keywords = ["neo4j", "testing", "integration-tests", "graph-database"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Testing",
20
+ ]
21
+ dependencies = [
22
+ "neo4j>=5.0",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ test = ["pytest>=7.0"]
27
+
28
+ [project.entry-points.pytest11]
29
+ neo4j_test_server = "neo4j_test_server.pytest_plugin"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src/neo4j_test_server"]
@@ -0,0 +1,18 @@
1
+ """Disposable in-process Neo4j test server for Python integration tests."""
2
+
3
+ from neo4j_test_server.server import (
4
+ JAR_VERSION,
5
+ Neo4jTestServer,
6
+ ServerInfo,
7
+ ensure_jar,
8
+ jar_path,
9
+ )
10
+
11
+ __all__ = [
12
+ "JAR_VERSION",
13
+ "Neo4jTestServer",
14
+ "ServerInfo",
15
+ "ensure_jar",
16
+ "jar_path",
17
+ ]
18
+ __version__ = "0.1.0"
@@ -0,0 +1,20 @@
1
+ """Pytest plugin exposing a session-scoped Neo4j test server fixture."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterator
6
+
7
+ import pytest
8
+
9
+ from neo4j_test_server.server import Neo4jTestServer
10
+
11
+
12
+ @pytest.fixture(scope="session")
13
+ def neo4j_test_server() -> Iterator[Neo4jTestServer]:
14
+ """Session-scoped fixture that starts the Neo4j test server once.
15
+
16
+ The server is shared by all tests in the session and shut down
17
+ automatically at the end.
18
+ """
19
+ with Neo4jTestServer() as server:
20
+ yield server
@@ -0,0 +1,235 @@
1
+ """Bootstrap and lifecycle management for the Neo4j test server JAR.
2
+
3
+ The server is a self-contained fat JAR published as a GitHub Release
4
+ asset. On first use it is downloaded and cached locally; only a Java
5
+ 21+ runtime is required.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ import urllib.request
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import IO, Optional
20
+
21
+ JAR_VERSION = os.environ.get("NEO4J_TEST_SERVER_JAR_VERSION", "v0.1.0")
22
+ JAR_NAME = "neo4j-test-server-all.jar"
23
+ RELEASE_BASE_URL = (
24
+ "https://github.com/vshofar/neo4j-test-server/releases/download"
25
+ )
26
+
27
+ DEFAULT_CACHE_DIR = (
28
+ Path(os.environ.get("NEO4J_TEST_SERVER_CACHE", ""))
29
+ if os.environ.get("NEO4J_TEST_SERVER_CACHE")
30
+ else Path.home() / ".cache" / "neo4j-test-server"
31
+ )
32
+
33
+ STARTUP_TIMEOUT_SECONDS = 180
34
+ SHUTDOWN_TIMEOUT_SECONDS = 60
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ServerInfo:
39
+ bolt_uri: str
40
+ username: str
41
+ password: str
42
+
43
+
44
+ def jar_path(version: str = JAR_VERSION, cache_dir: Optional[Path] = None) -> Path:
45
+ """Return the cached location of the JAR for a given version."""
46
+ return (cache_dir or DEFAULT_CACHE_DIR) / version / JAR_NAME
47
+
48
+
49
+ def release_url(version: str = JAR_VERSION) -> str:
50
+ return f"{RELEASE_BASE_URL}/{version}/{JAR_NAME}"
51
+
52
+
53
+ def ensure_jar(
54
+ version: str = JAR_VERSION,
55
+ cache_dir: Optional[Path] = None,
56
+ url: Optional[str] = None,
57
+ ) -> Path:
58
+ """Return a local path to the JAR, downloading it if necessary.
59
+
60
+ Honors the ``NEO4J_TEST_SERVER_JAR`` environment variable, which
61
+ points directly to a local JAR and skips downloading entirely.
62
+ """
63
+ override = os.environ.get("NEO4J_TEST_SERVER_JAR")
64
+ if override:
65
+ path = Path(override)
66
+ if not path.exists():
67
+ raise FileNotFoundError(f"NEO4J_TEST_SERVER_JAR not found: {path}")
68
+ return path
69
+
70
+ dest = jar_path(version, cache_dir)
71
+ if dest.exists():
72
+ return dest
73
+
74
+ url = url or release_url(version)
75
+ dest.parent.mkdir(parents=True, exist_ok=True)
76
+ tmp = dest.with_suffix(".jar.part")
77
+
78
+ print(f"Downloading {JAR_NAME} ({version}) from {url} ...", file=sys.stderr)
79
+ try:
80
+ with urllib.request.urlopen(url) as response, open(tmp, "wb") as out:
81
+ shutil.copyfileobj(response, out, length=1024 * 1024)
82
+ tmp.replace(dest)
83
+ except Exception:
84
+ tmp.unlink(missing_ok=True)
85
+ raise
86
+
87
+ print(f"Cached at {dest}", file=sys.stderr)
88
+ return dest
89
+
90
+
91
+ class Neo4jTestServer:
92
+ """Context manager that runs the Neo4j test server as a subprocess.
93
+
94
+ Usage::
95
+
96
+ with Neo4jTestServer() as server:
97
+ driver = GraphDatabase.driver(
98
+ server.bolt_uri,
99
+ auth=(server.username, server.password),
100
+ )
101
+ """
102
+
103
+ def __init__(
104
+ self,
105
+ jar: Optional[Path] = None,
106
+ java: str = "java",
107
+ working_dir: Optional[Path] = None,
108
+ env: Optional[dict] = None,
109
+ ) -> None:
110
+ self._jar = Path(jar) if jar else None
111
+ self._java = java
112
+ self._working_dir = working_dir
113
+ self._env = env
114
+ self._process: Optional[subprocess.Popen] = None
115
+ self._info: Optional[ServerInfo] = None
116
+
117
+ @property
118
+ def info(self) -> ServerInfo:
119
+ if self._info is None:
120
+ raise RuntimeError("server is not running")
121
+ return self._info
122
+
123
+ @property
124
+ def bolt_uri(self) -> str:
125
+ return self.info.bolt_uri
126
+
127
+ @property
128
+ def username(self) -> str:
129
+ return self.info.username
130
+
131
+ @property
132
+ def password(self) -> str:
133
+ return self.info.password
134
+
135
+ @property
136
+ def process(self) -> subprocess.Popen:
137
+ if self._process is None:
138
+ raise RuntimeError("server is not running")
139
+ return self._process
140
+
141
+ def start(self) -> ServerInfo:
142
+ if self._process is not None:
143
+ raise RuntimeError("server already running")
144
+
145
+ jar = self._jar or ensure_jar()
146
+ cmd = [self._java, "-jar", str(jar)]
147
+ if self._working_dir is not None:
148
+ cmd += ["--working-dir", str(self._working_dir)]
149
+
150
+ self._process = subprocess.Popen(
151
+ cmd,
152
+ stdin=subprocess.PIPE,
153
+ stdout=subprocess.PIPE,
154
+ stderr=None,
155
+ text=True,
156
+ env=self._env,
157
+ )
158
+ try:
159
+ self._info = self._read_ready(self._process)
160
+ except Exception:
161
+ self._terminate()
162
+ raise
163
+ return self._info
164
+
165
+ def stop(self) -> None:
166
+ if self._process is None:
167
+ return
168
+ process = self._process
169
+ self._process = None
170
+ self._info = None
171
+
172
+ if process.stdin:
173
+ try:
174
+ process.stdin.write("shutdown\n")
175
+ process.stdin.flush()
176
+ except (BrokenPipeError, OSError):
177
+ pass
178
+ try:
179
+ process.wait(timeout=SHUTDOWN_TIMEOUT_SECONDS)
180
+ except subprocess.TimeoutExpired:
181
+ process.kill()
182
+ process.wait()
183
+
184
+ def _read_ready(self, process: subprocess.Popen) -> ServerInfo:
185
+ stdout: IO[str] = process.stdout # type: ignore[assignment]
186
+ deadline = _monotonic() + STARTUP_TIMEOUT_SECONDS
187
+ for line in stdout:
188
+ line = line.strip()
189
+ if line.startswith("{"):
190
+ data = json.loads(line)
191
+ return ServerInfo(
192
+ bolt_uri=data["bolt_uri"],
193
+ username=data["username"],
194
+ password=data["password"],
195
+ )
196
+ if not process.poll() is None:
197
+ raise RuntimeError(
198
+ f"server exited before readiness message (code {process.returncode})"
199
+ )
200
+ if _monotonic() > deadline:
201
+ raise TimeoutError("timed out waiting for readiness message")
202
+ raise RuntimeError("server closed stdout before readiness message")
203
+
204
+ def _terminate(self) -> None:
205
+ process = self._process
206
+ self._process = None
207
+ self._info = None
208
+ if process is None:
209
+ return
210
+ if process.poll() is None:
211
+ process.kill()
212
+ process.wait()
213
+
214
+ def __enter__(self) -> "Neo4jTestServer":
215
+ self.start()
216
+ return self
217
+
218
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
219
+ self.stop()
220
+
221
+
222
+ def _monotonic() -> float:
223
+ import time
224
+
225
+ return time.monotonic()
226
+
227
+
228
+ __all__ = [
229
+ "JAR_VERSION",
230
+ "Neo4jTestServer",
231
+ "ServerInfo",
232
+ "ensure_jar",
233
+ "jar_path",
234
+ "release_url",
235
+ ]
@@ -0,0 +1,30 @@
1
+ """Tests for the neo4j-test-server Python package.
2
+
3
+ Requires NEO4J_TEST_SERVER_JAR pointing to a built fat JAR
4
+ (see .github/workflows/build.yml).
5
+ """
6
+
7
+ import os
8
+
9
+ import pytest
10
+
11
+ from neo4j_test_server import Neo4jTestServer, ensure_jar
12
+
13
+ pytestmark = pytest.mark.skipif(
14
+ not os.environ.get("NEO4J_TEST_SERVER_JAR"),
15
+ reason="NEO4J_TEST_SERVER_JAR not set; skipping integration test",
16
+ )
17
+
18
+
19
+ def test_server_lifecycle() -> None:
20
+ with Neo4jTestServer() as server:
21
+ assert server.bolt_uri.startswith("bolt://")
22
+ assert server.username
23
+ assert server.password
24
+ assert server.process.poll() is None
25
+
26
+
27
+ def test_ensure_jar_uses_env_override() -> None:
28
+ path = ensure_jar()
29
+ assert path.exists()
30
+ assert path.name == "neo4j-test-server-all.jar"