From 48caa692398283d6e71b4d67519d02814363f258 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 00:52:21 +0200 Subject: [PATCH 01/23] RavenDB-27069 Add Lab 01: embedded zero-config (guide + runnable script + labs index) --- labs/01-embedded-zero-config.md | 47 +++++++++++++++++++++++++++++++++ labs/01_embedded_zero_config.py | 39 +++++++++++++++++++++++++++ labs/README.md | 16 +++++++++++ 3 files changed, 102 insertions(+) create mode 100644 labs/01-embedded-zero-config.md create mode 100644 labs/01_embedded_zero_config.py create mode 100644 labs/README.md diff --git a/labs/01-embedded-zero-config.md b/labs/01-embedded-zero-config.md new file mode 100644 index 0000000..d07ca6c --- /dev/null +++ b/labs/01-embedded-zero-config.md @@ -0,0 +1,47 @@ +# Lab 01: Embedded, zero-config + +**For:** local development or CI on a machine that already has .NET installed. You want +`pip install ravendb-embedded` and a working RavenDB server with nothing set up on the side. + +**Requirement:** RavenDB 7.2.x needs **.NET 10** (7.1.x needed .NET 8). Verify with +`dotnet --list-runtimes` and look for `Microsoft.NETCore.App 10.0.x`. + +## Run it + +```bash +pip install ravendb-embedded +python labs/01_embedded_zero_config.py +``` + +The complete, runnable example is [`01_embedded_zero_config.py`](01_embedded_zero_config.py). +The core is just: + +```python +from ravendb_embedded import EmbeddedServer + +with EmbeddedServer() as server: + server.start_server() + with server.get_document_store("Lab") as store: + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() +``` + +## How the .NET runtime is picked (under the hood) + +1. The default provider (`CopyServerFromNugetProvider`) unpacks the **framework-dependent** + server (`Raven.Server.dll`) that ships inside the wheel. +2. `RavenServerRunner` launches it as **`dotnet Raven.Server.dll ...`**, using the `dotnet` + found on your `PATH` (`ServerOptions.dot_net_path`). +3. `ServerOptions.framework_version` is empty by default, so no `--fx-version` is passed and + standard .NET host resolution applies. The host does **not** roll forward across major + versions, which is why a net10.0 server hard-fails on a machine that only has .NET 8 + (error: "You must install ... version 10.0.x"). +4. The one exception: point the server at a **self-contained** build (an apphost + `Raven.Server` with the runtime bundled) and it runs **without `dotnet`**. That is Lab 02. + +## Takeaway + +This is the most convenient path, but it buys a hard dependency on a system-wide .NET in a +specific major version. If you would rather not manage .NET, use Lab 02 (external +self-contained server) or Lab 03 (attach to a server you run yourself, e.g. via Docker). diff --git a/labs/01_embedded_zero_config.py b/labs/01_embedded_zero_config.py new file mode 100644 index 0000000..3526c97 --- /dev/null +++ b/labs/01_embedded_zero_config.py @@ -0,0 +1,39 @@ +"""Lab 01: Embedded, zero-config. + +For: local development or CI on a machine that already has .NET installed. +You want `pip install ravendb-embedded` and a working RavenDB server, no extra setup. + +Requirement: RavenDB 7.2.x needs .NET 10 (7.1.x needed .NET 8). +Check your runtime with: dotnet --list-runtimes (look for Microsoft.NETCore.App 10.0.x) + +Run: python labs/01_embedded_zero_config.py +""" + +import tempfile +from pathlib import Path + +from ravendb_embedded import EmbeddedServer, ServerOptions + + +def main() -> None: + with tempfile.TemporaryDirectory() as work_dir: + options = ServerOptions() + options.data_directory = str(Path(work_dir, "RavenDB")) + options.logs_path = str(Path(work_dir, "Logs")) + + with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("Lab") as store: + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() + + with store.open_session() as session: + loaded = session.load("people/1", dict) + assert loaded["name"] == "Ayende", loaded + + print("Lab 01 OK: embedded server started, wrote and read a document.") + + +if __name__ == "__main__": + main() diff --git a/labs/README.md b/labs/README.md new file mode 100644 index 0000000..a326f56 --- /dev/null +++ b/labs/README.md @@ -0,0 +1,16 @@ +# RavenDB Python: test server labs + +Three ways to get a RavenDB server for your tests, from most convenient to most portable. +Pick the one that matches your environment. + +| Lab | Path | For whom | Needs system .NET? | +|-----|------|----------|--------------------| +| [01](01-embedded-zero-config.md) | Embedded, zero-config | Local dev or CI on a machine that already has .NET | Yes (.NET 10 for 7.2.x) | +| 02 (in progress) | External self-contained server | You do not want to install .NET at all | No | +| 03 (in progress) | Attach to a server you run yourself (Docker, testcontainers, shared CI) | Containerized CI pipelines | No | + +RavenDB version to .NET mapping: **7.1.x needs .NET 8, 7.2.x needs .NET 10.** The bundled +server is the deciding factor, so this can change on a minor bump; always check the lab for +your version. + +Every lab ships a runnable script next to it, so you can run the exact code the guide shows. From 5f6d3792e088841565b364bd8fb1f0c5191a595a Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 01:13:01 +0200 Subject: [PATCH 02/23] RavenDB-27069 Detect self-contained servers in ExternalServerProvider (run apphost, no dotnet) Read Raven.Server.runtimeconfig.json: an includedFrameworks build bundles the runtime, so run the native apphost (Raven.Server[.exe]) directly instead of 'dotnet Raven.Server.dll'. Fixes the no-.NET path, which previously fell back to a system dotnet because a self-contained dir also contains Raven.Server.dll. Also pick the platform apphost (.exe on Windows). --- ravendb_embedded/provide.py | 55 ++++++++++++++++++------- ravendb_embedded/raven_server_runner.py | 6 ++- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/ravendb_embedded/provide.py b/ravendb_embedded/provide.py index 373cdee..f429810 100644 --- a/ravendb_embedded/provide.py +++ b/ravendb_embedded/provide.py @@ -1,3 +1,4 @@ +import json import os import pkgutil import shutil @@ -92,25 +93,51 @@ def __init__(self, server_location: str): self.inner_provider = ExtractFromZipServerProvider(server_location) return - # Alternatively, it might be a directory - look for Raven.Server.exe inside - if os.path.isdir(file_server_location) and os.path.exists( - os.path.join(file_server_location, self.SERVER_DLL_FILENAME) - ): - self.inner_provider = CopyServerProvider(server_location) - return - - # Also look for Single File App file - Raven.Server - if os.path.isdir(file_server_location) and os.path.exists( - os.path.join(file_server_location, self.SERVER_SFA_FILENAME) - ): - self.is_single_file_app = True - self.inner_provider = CopyServerProvider(server_location) - return + # A directory can be a self-contained build (bundled runtime, run the native apphost + # directly) or a framework-dependent build (run via a system `dotnet`). Check + # self-contained first: a self-contained dir also contains Raven.Server.dll, so the + # old "look for the .dll" heuristic would misclassify it as framework-dependent and + # fall back to `dotnet`, needing a system .NET. + if os.path.isdir(file_server_location): + if self._is_self_contained(file_server_location): + self.is_single_file_app = True + self.inner_provider = CopyServerProvider(server_location) + return + + if os.path.exists(os.path.join(file_server_location, self.SERVER_DLL_FILENAME)): + self.inner_provider = CopyServerProvider(server_location) + return raise ValueError( f"Unable to find RavenDB server (expected directory with {self.SERVER_DLL_FILENAME}) or zip file. " f"Used directory = {server_location}" ) + @staticmethod + def _is_self_contained(directory: str) -> bool: + # A self-contained server bundles the .NET runtime, so it runs via its native apphost + # (Raven.Server[.exe]) with no system `dotnet`. The reliable marker is + # `includedFrameworks` in the runtime config; otherwise treat a single-file publish + # (apphost present, no managed .dll) as self-contained too. + runtime_config = os.path.join(directory, "Raven.Server.runtimeconfig.json") + if os.path.isfile(runtime_config): + try: + with open(runtime_config, encoding="utf-8") as config_file: + runtime_options = json.load(config_file).get("runtimeOptions", {}) + if runtime_options.get("includedFrameworks"): + return True + except (OSError, ValueError): + pass + + has_managed_dll = os.path.exists(os.path.join(directory, ExternalServerProvider.SERVER_DLL_FILENAME)) + has_apphost = any( + os.path.exists(os.path.join(directory, name)) + for name in ( + ExternalServerProvider.SERVER_SFA_FILENAME, + f"{ExternalServerProvider.SERVER_SFA_FILENAME}.exe", + ) + ) + return has_apphost and not has_managed_dll + def provide(self, target_directory: str) -> None: self.inner_provider.provide(target_directory) diff --git a/ravendb_embedded/raven_server_runner.py b/ravendb_embedded/raven_server_runner.py index bcd6e4c..55905c1 100644 --- a/ravendb_embedded/raven_server_runner.py +++ b/ravendb_embedded/raven_server_runner.py @@ -32,7 +32,11 @@ def run(options: ServerOptions) -> subprocess.Popen: raise ValueError("logs_path cannot be None or whitespace") is_sfa = isinstance(options.provider, ExternalServerProvider) and options.provider.is_single_file_app - file_name = "Raven.Server" if is_sfa else "Raven.Server.dll" + if is_sfa: + # Self-contained / single-file build: run the native apphost, no `dotnet`. + file_name = "Raven.Server.exe" if os.name == "nt" else "Raven.Server" + else: + file_name = "Raven.Server.dll" server_paths = [ f"{file_name}", From 0479d2d99258c037b6e9cb34a0e832c63d7455d2 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 01:13:02 +0200 Subject: [PATCH 03/23] RavenDB-27069 Add Lab 02: external self-contained server (no system .NET) --- labs/02-embedded-external-server.md | 48 ++++++++++++++++++++++++++ labs/02_embedded_external_server.py | 53 +++++++++++++++++++++++++++++ labs/README.md | 2 +- 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 labs/02-embedded-external-server.md create mode 100644 labs/02_embedded_external_server.py diff --git a/labs/02-embedded-external-server.md b/labs/02-embedded-external-server.md new file mode 100644 index 0000000..192a0f5 --- /dev/null +++ b/labs/02-embedded-external-server.md @@ -0,0 +1,48 @@ +# Lab 02: External self-contained server (no system .NET) + +**For:** machines or CI that do not have .NET installed. You bring a **self-contained** +RavenDB server (it bundles the .NET runtime); the driver runs its native apphost directly and +never calls `dotnet`. + +## Get a self-contained server + +Download and extract a server build from ravendb.net (one per platform), for example: + +- Linux x64: `https://hibernatingrhinos.com/downloads/RavenDB%20for%20Linux%20x64/latest?version=7.2` +- Windows x64: `https://hibernatingrhinos.com/downloads/RavenDB%20for%20Windows%20x64/latest?version=7.2` + +The server files live in the `Server/` subfolder of the extracted archive. + +## Run it + +```bash +pip install ravendb-embedded +RAVENDB_SELF_CONTAINED_SERVER=/path/to/extracted/Server python labs/02_embedded_external_server.py +``` + +The complete example is [`02_embedded_external_server.py`](02_embedded_external_server.py). +The core is: + +```python +from ravendb_embedded import EmbeddedServer, ServerOptions + +options = ServerOptions() +options.with_external_server("/path/to/extracted/Server") # a self-contained build +with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("Lab") as store: + ... # ordinary RavenDB client code, with no .NET on the machine +``` + +## How it decides not to use `dotnet` + +`ExternalServerProvider` reads `Raven.Server.runtimeconfig.json`. A self-contained build lists +`includedFrameworks` (its bundled runtime), so the driver marks it "run the native apphost" +and launches `Raven.Server` (or `Raven.Server.exe` on Windows) directly, with **no `dotnet`**. +A framework-dependent build (only a `framework` reference, no bundled runtime) still runs via +`dotnet Raven.Server.dll` and needs a system .NET, which is Lab 01. + +## Takeaway + +No .NET on the box, at the cost of fetching and caching the server build yourself. If you would +rather not manage a server at all, run one in a container and attach to it: Lab 03. diff --git a/labs/02_embedded_external_server.py b/labs/02_embedded_external_server.py new file mode 100644 index 0000000..ec0a821 --- /dev/null +++ b/labs/02_embedded_external_server.py @@ -0,0 +1,53 @@ +"""Lab 02: External self-contained server (no system .NET). + +For: machines or CI that do NOT have .NET installed. You bring a self-contained RavenDB +server build (it bundles the .NET runtime), and the driver runs its native apphost directly, +never calling `dotnet`. + +Get a self-contained server from ravendb.net downloads, for example: + Linux x64: https://hibernatingrhinos.com/downloads/RavenDB%20for%20Linux%20x64/latest?version=7.2 + Windows x64: https://hibernatingrhinos.com/downloads/RavenDB%20for%20Windows%20x64/latest?version=7.2 +Extract it; the server files live in the `Server/` subfolder. + +Run: + RAVENDB_SELF_CONTAINED_SERVER=/path/to/extracted/Server python labs/02_embedded_external_server.py +""" + +import os +import sys +import tempfile +from pathlib import Path + +from ravendb_embedded import EmbeddedServer, ServerOptions + +SERVER = os.environ.get("RAVENDB_SELF_CONTAINED_SERVER") or (sys.argv[1] if len(sys.argv) > 1 else None) +if not SERVER: + sys.exit("Set RAVENDB_SELF_CONTAINED_SERVER (or pass a path) to the extracted 'Server' folder. See the header.") + + +def main() -> None: + with tempfile.TemporaryDirectory() as work: + options = ServerOptions() + options.target_server_location = str(Path(work, "server")) + options.data_directory = str(Path(work, "data")) + options.logs_path = str(Path(work, "logs")) + # Bogus on purpose: a self-contained build must never shell out to `dotnet`, so if the + # server still boots, we have proven the no-.NET path works. + options.dot_net_path = "__no_dotnet__" + options.with_external_server(SERVER) + + with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("Lab") as store: + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() + + with store.open_session() as session: + assert session.load("people/1", dict)["name"] == "Ayende" + + print("Lab 02 OK: self-contained server ran with NO system .NET.") + + +if __name__ == "__main__": + main() diff --git a/labs/README.md b/labs/README.md index a326f56..468578a 100644 --- a/labs/README.md +++ b/labs/README.md @@ -6,7 +6,7 @@ Pick the one that matches your environment. | Lab | Path | For whom | Needs system .NET? | |-----|------|----------|--------------------| | [01](01-embedded-zero-config.md) | Embedded, zero-config | Local dev or CI on a machine that already has .NET | Yes (.NET 10 for 7.2.x) | -| 02 (in progress) | External self-contained server | You do not want to install .NET at all | No | +| [02](02-embedded-external-server.md) | External self-contained server | You do not want to install .NET at all | No | | 03 (in progress) | Attach to a server you run yourself (Docker, testcontainers, shared CI) | Containerized CI pipelines | No | RavenDB version to .NET mapping: **7.1.x needs .NET 8, 7.2.x needs .NET 10.** The bundled From 62bdcae794088d639f4e3557b5e24b236401661a Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 01:24:05 +0200 Subject: [PATCH 04/23] RavenDB-27069 CI: net10-only matrix, enforce required .NET, run labs + self-contained path The 7.2 server targets net10.0, so the old .NET 8 matrix cell was fake (green only because runners preinstall .NET 10). Now: OS {ubuntu,windows} x Python 3.13 x .NET 10; a derive+enforce step (scripts/check_dotnet_requirement.py) fails if the installed runtime does not match the bundled server; Lab 01 runs on the .NET job; a separate job proves the self-contained (no-.NET) path via Lab 02. --- .github/workflows/tests.yml | 53 +++++++++++++++++++++++------ scripts/check_dotnet_requirement.py | 50 +++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 scripts/check_dotnet_requirement.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1d8a1d2..31b46e5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,17 +9,14 @@ on: jobs: test: + # Path A: embedded framework-dependent server -> needs a system .NET. The 7.2 server + # targets net10.0 (7.1 was net8.0), so we install .NET 10 explicitly and assert the + # bundled server really needs it (no more green-by-luck on a preinstalled runtime). runs-on: ${{ matrix.os }} - env: - # .NET 8 (the server's target) is exercised by default resolution; .NET 10 must be - # forced since runners preinstall .NET 8 and would otherwise win. tests/pin_framework_version. - RAVENDB_TEST_FRAMEWORK_VERSION: ${{ matrix.dotnet == '10.0' && '10.0.x' || '' }} strategy: fail-fast: false - # Latest Python only (the ravendb client requires 3.10+); full OS x .NET matrix. matrix: os: [ubuntu-latest, windows-latest] - dotnet: ["8.0", "10.0"] steps: - uses: actions/checkout@v4 @@ -29,10 +26,10 @@ jobs: with: python-version: "3.13" - - name: Set up .NET ${{ matrix.dotnet }} + - name: Set up .NET 10 uses: actions/setup-dotnet@v4 with: - dotnet-version: ${{ matrix.dotnet }} + dotnet-version: "10.0" - name: Install package run: | @@ -44,12 +41,48 @@ jobs: pip install black black --check . - # Populates ravendb_embedded/target/nuget via the project's sdist hook (the server the - # tests copy from). setuptools is installed explicitly; modern virtualenvs omit it. + # Downloads + unpacks the bundled (framework-dependent) server into + # ravendb_embedded/target/nuget. setuptools is installed explicitly; modern venvs omit it. - name: Fetch bundled RavenDB server run: | pip install setuptools wheel python setup.py sdist + - name: Assert CI .NET matches the server's required runtime + run: python scripts/check_dotnet_requirement.py + - name: Run tests run: python -m unittest discover -s tests + + - name: Run Lab 01 (embedded zero-config) + run: python labs/01_embedded_zero_config.py + + self-contained: + # Path B: external self-contained server -> runs the native apphost, no system .NET. + # We do not set up .NET; the lab uses a bogus dot_net_path so a successful run proves the + # apphost path is genuinely dotnet-free even though the runner happens to ship a .NET. + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.13 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install package + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Download a self-contained RavenDB server + run: | + curl -fsSL -o ravendb.tar.bz2 "https://hibernatingrhinos.com/downloads/RavenDB%20for%20Linux%20x64/latest?version=7.2" + mkdir server + tar xjf ravendb.tar.bz2 -C server + + - name: Run Lab 02 (self-contained, no system .NET) + run: | + SERVER="$(find server -type d -name Server | head -1)" + RAVENDB_SELF_CONTAINED_SERVER="$SERVER" python labs/02_embedded_external_server.py diff --git a/scripts/check_dotnet_requirement.py b/scripts/check_dotnet_requirement.py new file mode 100644 index 0000000..c59eefe --- /dev/null +++ b/scripts/check_dotnet_requirement.py @@ -0,0 +1,50 @@ +"""Assert the installed .NET runtime matches what the bundled RavenDB server requires. + +Reads the bundled server's runtimeconfig and checks that a matching Microsoft.NETCore.App +major runtime is installed. This turns the otherwise hidden server-to-.NET coupling (which +silently jumped from .NET 8 to .NET 10 between 7.1 and 7.2) into an enforced invariant, so CI +fails loudly instead of passing by luck on a preinstalled runtime. + +Run after fetching the bundled server (`python setup.py sdist`): + python scripts/check_dotnet_requirement.py +""" + +import json +import subprocess +import sys +from pathlib import Path + +RUNTIME_CONFIG = Path( + "ravendb_embedded/target/nuget/contentFiles/any/any/RavenDBServer/Raven.Server.runtimeconfig.json" +) + + +def required_dotnet_major(runtime_config: Path) -> str: + options = json.loads(runtime_config.read_text(encoding="utf-8"))["runtimeOptions"] + frameworks = list(options.get("frameworks") or options.get("includedFrameworks") or []) + if isinstance(options.get("framework"), dict): + frameworks.insert(0, options["framework"]) + for framework in frameworks: + if framework.get("name") == "Microsoft.NETCore.App" and framework.get("version"): + return framework["version"].split(".")[0] + return options.get("tfm", "").removeprefix("net").split(".")[0] # e.g. "net10.0" -> "10" + + +def main() -> object: + if not RUNTIME_CONFIG.exists(): + return f"Server runtimeconfig not found at {RUNTIME_CONFIG}; fetch the server first (python setup.py sdist)." + + major = required_dotnet_major(RUNTIME_CONFIG) + if not major: + return f"Could not determine the required .NET version from {RUNTIME_CONFIG}." + + installed = subprocess.run(["dotnet", "--list-runtimes"], capture_output=True, text=True).stdout + matched = f"Microsoft.NETCore.App {major}." in installed + print(f"Bundled server requires .NET major {major}; matching runtime installed: {matched}") + if not matched: + return f"Installed .NET runtimes do not include major {major}, which the bundled server requires." + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 7d58db13c400fee7eaf52ae114b1bc0d33be9e30 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 01:24:06 +0200 Subject: [PATCH 05/23] RavenDB-27069 Cleanup: drop dead pin_framework_version helper; require Python >=3.10 The .NET-matrix pin machinery is unused now that CI is net10-only. Bump python_requires to >=3.10 (the ravendb client uses PEP 604 unions at runtime and cannot import on 3.9). --- setup.py | 2 +- tests/__init__.py | 12 ------------ tests/test_basic.py | 4 +--- tests/test_custom_provider.py | 3 +-- tests/test_secured_basic.py | 3 +-- 5 files changed, 4 insertions(+), 20 deletions(-) diff --git a/setup.py b/setup.py index a7ac5e9..4342ddd 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ def run(self): setup( - python_requires=">=3.9", + python_requires=">=3.10", cmdclass={"sdist": CustomSDist}, name="ravendb-embedded", packages=["ravendb_embedded"], diff --git a/tests/__init__.py b/tests/__init__.py index 81176b6..a9e5908 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,16 +1,4 @@ -import os - - class Person: def __init__(self, Id: str = None, name: str = None): self.Id = Id self.name = name - - -def pin_framework_version(server_options): - # CI's .NET-version matrix sets RAVENDB_TEST_FRAMEWORK_VERSION to force the server onto a - # specific runtime (e.g. "10.0.x"); a no-op locally when it's unset. - framework_version = os.environ.get("RAVENDB_TEST_FRAMEWORK_VERSION") - if framework_version: - server_options.framework_version = framework_version - return server_options diff --git a/tests/test_basic.py b/tests/test_basic.py index b3c1f86..526eb08 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -4,7 +4,7 @@ from unittest import TestCase from ravendb_embedded import EmbeddedServer, ServerOptions, CopyServerFromNugetProvider, DatabaseOptions -from tests import Person, pin_framework_version +from tests import Person class BasicTest(TestCase): @@ -17,7 +17,6 @@ def test_embedded(self): server_options.logs_path = str(Path(temp_dir, "Logs")) server_options.provider = CopyServerFromNugetProvider() server_options.command_line_args = ["--Features.Availability=Experimental"] - pin_framework_version(server_options) embedded.start_server(server_options) database_options = DatabaseOptions.from_database_name("Test") @@ -38,7 +37,6 @@ def test_embedded(self): server_options = ServerOptions() server_options.data_directory = str(Path(temp_dir, "RavenDB")) server_options.provider = CopyServerFromNugetProvider() - pin_framework_version(server_options) embedded.start_server(server_options) with embedded.get_document_store("Test") as store: diff --git a/tests/test_custom_provider.py b/tests/test_custom_provider.py index 9880a5d..bae95a7 100644 --- a/tests/test_custom_provider.py +++ b/tests/test_custom_provider.py @@ -6,7 +6,7 @@ from ravendb_embedded.embedded_server import EmbeddedServer from ravendb_embedded.options import ServerOptions, DatabaseOptions from ravendb_embedded.provide import CopyServerFromNugetProvider -from tests import Person, pin_framework_version +from tests import Person class TestCustomProvider(TestCase): @@ -16,7 +16,6 @@ def configure_server_options(temp_dir: str, server_options: ServerOptions) -> Se server_options.data_directory = str(Path(temp_dir, "RavenDB")) server_options.logs_path = str(Path(temp_dir, "Logs")) server_options.command_line_args = ["--Features.Availability=Experimental"] - pin_framework_version(server_options) return server_options def test_can_use_zip_as_external_server_source(self): diff --git a/tests/test_secured_basic.py b/tests/test_secured_basic.py index 72feebc..cf8bcf7 100644 --- a/tests/test_secured_basic.py +++ b/tests/test_secured_basic.py @@ -6,7 +6,7 @@ from ravendb_embedded.embedded_server import EmbeddedServer from ravendb_embedded.options import ServerOptions, DatabaseOptions from ravendb_embedded.provide import CopyServerFromNugetProvider -from tests import Person, pin_framework_version +from tests import Person from tests.certificates import generate_self_signed_certificates @@ -21,7 +21,6 @@ def test_secured_embedded(self): server_options.data_directory = str(Path(temp_dir, "RavenDB")) server_options.logs_path = str(Path(temp_dir, "Logs")) server_options.provider = CopyServerFromNugetProvider() - pin_framework_version(server_options) embedded.start_server(server_options) database_options = DatabaseOptions.from_database_name("Test") From 5515182c38120b99bc3a625526ff7dbfa54ae401 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 01:26:54 +0200 Subject: [PATCH 06/23] RavenDB-27069 Remove no-op CommandLineArgumentEscaper (Popen gets an arg list, no shell) --- ravendb_embedded/raven_server_runner.py | 40 ++++++------------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/ravendb_embedded/raven_server_runner.py b/ravendb_embedded/raven_server_runner.py index 55905c1..fd27b72 100644 --- a/ravendb_embedded/raven_server_runner.py +++ b/ravendb_embedded/raven_server_runner.py @@ -13,12 +13,6 @@ ) -class CommandLineArgumentEscaper: - @staticmethod - def escape_single_arg(arg: str) -> str: - return arg # lol - - class RavenServerRunner: @staticmethod def run(options: ServerOptions) -> subprocess.Popen: @@ -58,41 +52,30 @@ def run(options: ServerOptions) -> subprocess.Popen: if not options.dot_net_path.strip(): raise ValueError("dot_net_path cannot be None or whitespace") + # Args are passed to Popen as a list (no shell), so they need no manual escaping. command_line_args = [ f"--Embedded.ParentProcessId={RavenServerRunner.get_process_id('0')}", f"--License.Eula.Accepted={'true' if options.accept_eula else 'false'}", "--Setup.Mode=None", - f"--DataDir={CommandLineArgumentEscaper.escape_single_arg(options.data_directory)}", - f"--Logs.Path={CommandLineArgumentEscaper.escape_single_arg(options.logs_path)}", + f"--DataDir={options.data_directory}", + f"--Logs.Path={options.logs_path}", ] if options.security: options.server_url = options.server_url or "https://127.0.0.1:0" if options.security.server_pfx_certificate_path: - command_line_args.extend( - [ - f"--Security.Certificate.Path=" - f"{CommandLineArgumentEscaper.escape_single_arg(options.security.server_pfx_certificate_path)}" - ] - ) + command_line_args.append(f"--Security.Certificate.Path={options.security.server_pfx_certificate_path}") if options.security.server_pfx_certificate_password: - command_line_args.extend( - [ - "--Security.Certificate.Password=" - + CommandLineArgumentEscaper.escape_single_arg( - options.security.server_pfx_certificate_password - ) - ] + command_line_args.append( + f"--Security.Certificate.Password={options.security.server_pfx_certificate_password}" ) elif options.security.certificate_exec: command_line_args.extend( [ - f"--Security.Certificate.Exec=" - f"{CommandLineArgumentEscaper.escape_single_arg(options.security.certificate_exec)}", - f"--Security.Certificate.Exec.Arguments=" - f"{CommandLineArgumentEscaper.escape_single_arg(options.security.certificate_arguments)}", + f"--Security.Certificate.Exec={options.security.certificate_exec}", + f"--Security.Certificate.Exec.Arguments={options.security.certificate_arguments}", ] ) if options.security.client_pem_certificate_path: @@ -102,12 +85,7 @@ def run(options: ServerOptions) -> subprocess.Popen: cert = x509.load_pem_x509_certificate(cert_data, default_backend()) thumbprint = cert.fingerprint(hashes.SHA256()).hex() - command_line_args.extend( - [ - f"--Security.WellKnownCertificates.Admin=" - f"{CommandLineArgumentEscaper.escape_single_arg(thumbprint)}" - ] - ) + command_line_args.append(f"--Security.WellKnownCertificates.Admin={thumbprint}") else: options.server_url = options.server_url or "http://127.0.0.1:0" From acac43d88679ae59033fe5a6012d9db77c1453e7 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 01:27:54 +0200 Subject: [PATCH 07/23] RavenDB-27069 Document the .NET runtime requirement and Python 3.10+ (link the labs) --- README.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.rst b/README.rst index e706ad5..e3a1f75 100644 --- a/README.rst +++ b/README.rst @@ -24,6 +24,24 @@ Install ``ravendb-embedded`` from `PyPi `_ using: Installing ``ravendb-embedded`` from pip will also provide you with a copy of the RavenDB server binary files. +============ +Requirements +============ + +Python 3.10+ is required. + +The bundled server is a .NET application, so a matching **.NET runtime** must be available on the machine: + +- ``ravendb-embedded`` 7.2.x requires **.NET 10** +- ``ravendb-embedded`` 7.1.x requires **.NET 8** + +Check what you have with ``dotnet --list-runtimes``. If you would rather not install .NET, you can run a +self-contained server (bundles its own runtime) or attach to a server you run yourself (for example in +Docker). Runnable guides live in the ``labs/`` folder: + +- ``labs/01-embedded-zero-config.md`` - embedded server, needs a system .NET +- ``labs/02-embedded-external-server.md`` - external self-contained server, no .NET required + ======== Usage ======== From b09b24e6d6a27e32962cf51c7cf001f1042d621b Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 02:04:47 +0200 Subject: [PATCH 08/23] RavenDB-27069 Add Lab 04: on-demand cached self-contained server (exploration) Fetches a self-contained build for the current platform on first use, caches it, and reuses the cached copy afterwards (no re-download, no system .NET). Marked as exploration; not wired into the package default. Updates the labs index (Lab 03 attach lives in the test-driver repo) and ignores the local RavenDB data dir created by test runs. --- .gitignore | 1 + labs/04-on-demand-server.md | 67 +++++++++++++++++++++++++++++ labs/README.md | 9 +++- labs/on_demand_server.py | 85 +++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 labs/04-on-demand-server.md create mode 100644 labs/on_demand_server.py diff --git a/.gitignore b/.gitignore index 3474397..5f6470d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /dist /ravendb_embedded.egg-info /ravendb_embedded/target +/ravendb_embedded/RavenDB *.pyc *.log *.raven-topology diff --git a/labs/04-on-demand-server.md b/labs/04-on-demand-server.md new file mode 100644 index 0000000..d2e5b05 --- /dev/null +++ b/labs/04-on-demand-server.md @@ -0,0 +1,67 @@ +# Lab 04: On-demand, cached self-contained server (exploration) + +**Status:** exploration. This is NOT wired into the package default. It shows how a future +"just works, no .NET" acquisition path could look, in the spirit of how Playwright fetches its +browsers on first use. + +**For:** anyone who wants Lab 02 (no system .NET) without manually downloading and extracting a +server. The helper fetches a self-contained build for the current platform on first use, caches +it, and reuses the cached copy every time after that. + +## The idea + +Lab 02 needs a self-contained `Server/` folder that you download and extract yourself. This lab +automates that one step: + +1. On first use, download the self-contained build for this OS and architecture. +2. Extract it into a cache directory and remember it. +3. On every later run, reuse the cached copy: no re-download, no `dotnet`. + +## Run it + +```bash +pip install ravendb-embedded +python labs/on_demand_server.py +``` + +The complete example is [`on_demand_server.py`](on_demand_server.py). The core is: + +```python +from ravendb_embedded import EmbeddedServer, ServerOptions +from on_demand_server import ensure_server + +server_dir = ensure_server() # download+cache on first use, cache hit afterwards +options = ServerOptions() +options.with_external_server(server_dir) # a self-contained build, so no dotnet +with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("Lab") as store: + ... # ordinary RavenDB client code, with no .NET on the machine +``` + +## Caching, by design + +`ensure_server()` keys the cache on version + platform label and looks for +`Raven.Server.dll` under the cache directory. If it is present the download step is skipped +entirely, so the second run (and every run after) is offline and instant. The default cache +root is `~/.cache/ravendb-embedded`; pass `cache_root=...` to override it (the CI-style pattern +is to point it at a directory the CI cache restores between runs). + +Because the downloaded build is self-contained, the run path is identical to Lab 02: +`ExternalServerProvider` sees `includedFrameworks` in the runtime config, runs the native +apphost (`Raven.Server.exe` on Windows, `Raven.Server` elsewhere), and never calls `dotnet`. + +## Why it is only an exploration + +- It downloads `latest` for a version line, so a run is only as reproducible as that endpoint. +- A self-contained build is large (100 MB+), so the first-use cost and the cache footprint are + real; a shipped version would want checksums, a pinned build, and a documented cache location. +- Making this the default changes the install story (a network fetch on first use) and needs a + decision on where the cache lives and how it is invalidated. + +## Takeaway + +The acquisition step from Lab 02 can be automated and cached, giving a no-.NET experience with +no manual download. Shipping it as a default is a product decision, not a code gap: the +mechanism works (this lab runs it), what is missing is the reproducibility and cache-policy +guarantees a default would need. diff --git a/labs/README.md b/labs/README.md index 468578a..3a158fc 100644 --- a/labs/README.md +++ b/labs/README.md @@ -1,13 +1,18 @@ # RavenDB Python: test server labs -Three ways to get a RavenDB server for your tests, from most convenient to most portable. +Ways to get a RavenDB server for your tests, from most convenient to most portable. Pick the one that matches your environment. | Lab | Path | For whom | Needs system .NET? | |-----|------|----------|--------------------| | [01](01-embedded-zero-config.md) | Embedded, zero-config | Local dev or CI on a machine that already has .NET | Yes (.NET 10 for 7.2.x) | | [02](02-embedded-external-server.md) | External self-contained server | You do not want to install .NET at all | No | -| 03 (in progress) | Attach to a server you run yourself (Docker, testcontainers, shared CI) | Containerized CI pipelines | No | +| 03 | Attach to a server you run yourself (Docker, testcontainers, shared CI) | Containerized CI pipelines | No | +| [04](04-on-demand-server.md) | On-demand cached self-contained download (exploration) | Lab 02 without the manual download | No | + +Lab 03 (attach) lives in the [`ravendb-python-testdriver`](https://github.com/ravendb/ravendb-python-testdriver) +repository, since attaching to an external server is a test-driver feature. Lab 04 is an +exploration and is not wired into the package default. RavenDB version to .NET mapping: **7.1.x needs .NET 8, 7.2.x needs .NET 10.** The bundled server is the deciding factor, so this can change on a minor bump; always check the lab for diff --git a/labs/on_demand_server.py b/labs/on_demand_server.py new file mode 100644 index 0000000..79ef7f1 --- /dev/null +++ b/labs/on_demand_server.py @@ -0,0 +1,85 @@ +"""Lab 04 (exploration): on-demand, cached self-contained server download. + +A Playwright-style acquisition prototype: fetch a self-contained RavenDB server for the current +platform on first use, cache it, and reuse the cached copy next time (no re-download, no .NET). +This is exploratory and is NOT wired into the package default; it shows how a future +"just works, no .NET" acquisition path could look. + +Run: python labs/on_demand_server.py +""" + +import platform +import tarfile +import tempfile +import urllib.request +import zipfile +from pathlib import Path + +RAVENDB_VERSION = "7.2" + + +def _platform_download(): + machine = platform.machine().lower() + arch = "arm64" if machine in ("arm64", "aarch64") else "x64" + system = platform.system() + if system == "Windows": + return f"RavenDB for Windows {arch}", "zip" + if system == "Darwin": + return f"RavenDB for OSX {arch}", "tar.bz2" + return f"RavenDB for Linux {arch}", "tar.bz2" + + +def ensure_server(version=RAVENDB_VERSION, cache_root=None): + """Return a local self-contained Server directory, downloading and caching on first use.""" + cache_root = Path(cache_root) if cache_root else Path.home() / ".cache" / "ravendb-embedded" + label, extension = _platform_download() + target = cache_root / version / label.replace(" ", "_") + + cached = next(target.rglob("Raven.Server.dll"), None) if target.is_dir() else None + if cached: # cache hit: never download again ("by design") + return str(cached.parent) + + target.mkdir(parents=True, exist_ok=True) + url = f"https://hibernatingrhinos.com/downloads/{label.replace(' ', '%20')}/latest?version={version}" + archive = target / f"ravendb.{extension}" + print(f"downloading {url}") + urllib.request.urlretrieve(url, archive) + if extension == "zip": + with zipfile.ZipFile(archive) as bundle: + bundle.extractall(target) + else: + with tarfile.open(archive) as bundle: + bundle.extractall(target) + archive.unlink() + + server = next(target.rglob("Raven.Server.dll"), None) + if not server: + raise RuntimeError(f"Server binaries not found under {target}") + return str(server.parent) + + +def main(): + from ravendb_embedded import EmbeddedServer, ServerOptions + + server_dir = ensure_server() + print("server ready at", server_dir) + + with tempfile.TemporaryDirectory() as work: + options = ServerOptions() + options.target_server_location = str(Path(work, "server")) + options.data_directory = str(Path(work, "data")) + options.logs_path = str(Path(work, "logs")) + options.dot_net_path = "__no_dotnet__" # proves the cached build needs no system .NET + options.with_external_server(server_dir) + with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("Lab") as store: + with store.open_session() as session: + session.store({"name": "on-demand"}, "people/1") + session.save_changes() + + print("Lab 04 OK: downloaded + cached a self-contained server and ran it with no system .NET.") + + +if __name__ == "__main__": + main() From 5b9b519179770d2893e9d7bbc92b6a6702b3a7ef Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 02:36:48 +0200 Subject: [PATCH 09/23] RavenDB-27069 Trim comments to critical one-liners; drop dead noise --- labs/02_embedded_external_server.py | 3 +-- ravendb_embedded/provide.py | 17 ++++------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/labs/02_embedded_external_server.py b/labs/02_embedded_external_server.py index ec0a821..d6eff0c 100644 --- a/labs/02_embedded_external_server.py +++ b/labs/02_embedded_external_server.py @@ -31,8 +31,7 @@ def main() -> None: options.target_server_location = str(Path(work, "server")) options.data_directory = str(Path(work, "data")) options.logs_path = str(Path(work, "logs")) - # Bogus on purpose: a self-contained build must never shell out to `dotnet`, so if the - # server still boots, we have proven the no-.NET path works. + # Bogus on purpose: a self-contained build must never call `dotnet`, so if it still boots the no-.NET path works. options.dot_net_path = "__no_dotnet__" options.with_external_server(SERVER) diff --git a/ravendb_embedded/provide.py b/ravendb_embedded/provide.py index f429810..c19aacd 100644 --- a/ravendb_embedded/provide.py +++ b/ravendb_embedded/provide.py @@ -48,7 +48,6 @@ def __init__(self, source_location: str): self.source_location = source_location def provide(self, target_directory): - # Ensure the target directory exists os.makedirs(target_directory, exist_ok=True) with open(self.source_location, "rb") as zip_file: self.unzip(zip_file, target_directory) @@ -63,15 +62,12 @@ class ExtractFromPkgResourceServerProvider(ProvideRavenDBServer): def provide(self, target_directory): resource_name = "ravendb_server.zip" - # Get binary data from the resource resource_data = pkgutil.get_data(self.__class__.__module__, resource_name) if resource_data is None: raise RuntimeError(f"Unable to find resource: {resource_name}") - # Create a bytes buffer from the binary data with BytesIO(resource_data) as bytes_buffer: - # Call the unzip method to extract contents to the target directory ExtractFromZipServerProvider.unzip(bytes_buffer.read(), target_directory) @@ -93,11 +89,8 @@ def __init__(self, server_location: str): self.inner_provider = ExtractFromZipServerProvider(server_location) return - # A directory can be a self-contained build (bundled runtime, run the native apphost - # directly) or a framework-dependent build (run via a system `dotnet`). Check - # self-contained first: a self-contained dir also contains Raven.Server.dll, so the - # old "look for the .dll" heuristic would misclassify it as framework-dependent and - # fall back to `dotnet`, needing a system .NET. + # Check self-contained first: it also ships Raven.Server.dll, so a .dll-first check + # would misroute it to `dotnet` and force a system .NET install. if os.path.isdir(file_server_location): if self._is_self_contained(file_server_location): self.is_single_file_app = True @@ -115,10 +108,8 @@ def __init__(self, server_location: str): @staticmethod def _is_self_contained(directory: str) -> bool: - # A self-contained server bundles the .NET runtime, so it runs via its native apphost - # (Raven.Server[.exe]) with no system `dotnet`. The reliable marker is - # `includedFrameworks` in the runtime config; otherwise treat a single-file publish - # (apphost present, no managed .dll) as self-contained too. + # Self-contained marker: `includedFrameworks` in the runtime config, or an apphost + # present with no managed .dll. runtime_config = os.path.join(directory, "Raven.Server.runtimeconfig.json") if os.path.isfile(runtime_config): try: From f66cc31533aba11f6c95f0984989bf20c4572a75 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 02:40:05 +0200 Subject: [PATCH 10/23] RavenDB-27069 Rewrite README around usage cases (why/how + link labs) --- README.rst | 174 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 118 insertions(+), 56 deletions(-) diff --git a/README.rst b/README.rst index e3a1f75..fafbf94 100644 --- a/README.rst +++ b/README.rst @@ -2,101 +2,163 @@ Overview ======== -``ravendb-embedded`` is a RavenDB package for running RavenDB in embedded mode. +``ravendb-embedded`` runs a real RavenDB server from inside your Python program. You +``pip install`` it, start the server in-process, and talk to it with the normal ``ravendb`` +client. There is no separate server to install, configure, or keep running: the server's +lifetime follows your process. + +Reach for it when you want: + +- **Local development** without setting up a standalone RavenDB. +- **Integration tests** against a real server instead of a mock (see also ``ravendb-test-driver``). +- **Small or self-contained apps** that ship the database alongside the code. .. code-block:: python - EmbeddedServer().start_server() - with EmbeddedServer().get_document_store("Embedded") as store: - with store.open_session() as session: - session.store(User(name="Ilay", age=4)) - session.save_changes() + from ravendb_embedded import EmbeddedServer + + with EmbeddedServer() as server: + server.start_server() + with server.get_document_store("Embedded") as store: + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() ============ Installation ============ -Install ``ravendb-embedded`` from `PyPi `_ using: - .. code-block:: bash pip install ravendb-embedded -Installing ``ravendb-embedded`` from pip will also provide you with a copy of the RavenDB server binary files. - -============ -Requirements -============ +The install includes a copy of the RavenDB server binaries. Python 3.10+ is required. -Python 3.10+ is required. +================================ +The .NET requirement (read this) +================================ -The bundled server is a .NET application, so a matching **.NET runtime** must be available on the machine: +The bundled server is a .NET application, so a matching **.NET runtime** must be on the machine. +The required version tracks the bundled server: -- ``ravendb-embedded`` 7.2.x requires **.NET 10** -- ``ravendb-embedded`` 7.1.x requires **.NET 8** +============================ ================== +``ravendb-embedded`` version Required runtime +============================ ================== +7.2.x .NET 10 +7.1.x .NET 8 +============================ ================== -Check what you have with ``dotnet --list-runtimes``. If you would rather not install .NET, you can run a -self-contained server (bundles its own runtime) or attach to a server you run yourself (for example in -Docker). Runnable guides live in the ``labs/`` folder: +Check what is installed with ``dotnet --list-runtimes`` (look for ``Microsoft.NETCore.App``). +Because the requirement follows the bundled server, it can change on a minor upgrade, so +re-check it when you bump versions. -- ``labs/01-embedded-zero-config.md`` - embedded server, needs a system .NET -- ``labs/02-embedded-external-server.md`` - external self-contained server, no .NET required +If the machine cannot or should not have .NET, use the self-contained path under +`Run without installing .NET`_ below. -======== +===== Usage -======== +===== -Start a server --------------- +The three sections below are the ways people actually use this package. Pick the one that +matches your environment; each links to a runnable walkthrough in ``labs/``. -To start the RavenDB server, call the ``start_server()`` method from an ``EmbeddedServer`` instance. +Run it (the default, needs .NET) +-------------------------------- + +Start the server and get a document store. This is the zero-config path and uses the system +.NET described above. Pass a ``ServerOptions`` when you want to control where data lives, the +bind URL, and so on. .. code-block:: python - EmbeddedServer.start_server() + from ravendb_embedded import EmbeddedServer, ServerOptions + + options = ServerOptions() + options.data_directory = "MYPATH/RavenDBDataDir" # optional; defaults to a local RavenDB folder + + with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("MyDb") as store: + ... # ordinary ravendb client code -For more control over your server, you can pass ``server_options`` to the ``start_server()`` method. +Runnable walkthrough: `labs/01-embedded-zero-config.md `_. -ServerOptions -------------- +Run without installing .NET +--------------------------- -- ``framework_version``: The framework version to run the server with. -- ``data_directory``: Where to save the database data (if None, the files will be saved in the RavenDB folder in the base folder). -- ``server_url``: The URL the server will be opened on (if None, the server will open on localhost). -- ``dotnet_path``: The location of ``dotnet.exe`` (if .NET Core is not installed on your machine, you can download `dotnet binaries `_ and provide the path). -- ``command_line_args``: A list of all `server command arguments `_. +On locked-down hosts or minimal CI images where you do not want a system .NET, bring a +**self-contained** RavenDB build (it bundles its own runtime). Point the server at the extracted +``Server`` folder: the driver detects the bundled runtime and launches the server's native +apphost directly, never calling ``dotnet``. .. code-block:: python - server_options = ServerOptions(data_directory="MYPATH/RavenDBDataDir") - EmbeddedServer().start_server(server_options) + from ravendb_embedded import EmbeddedServer, ServerOptions -Security --------- + options = ServerOptions() + options.with_external_server("/path/to/extracted/Server") # a self-contained build -You can secure ``ravendb-embedded`` using the ``secured()`` method: + with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("MyDb") as store: + ... -.. code-block:: python +Download self-contained builds from the RavenDB downloads page (one archive per platform); the +server files live in the archive's ``Server/`` folder. Runnable walkthrough: +`labs/02-embedded-external-server.md `_. An exploratory +helper that downloads and caches a build on first use is in +`labs/04-on-demand-server.md `_. + +Don't manage a server at all (tests) +------------------------------------- + +For test suites that should not touch .NET or embedded startup, ``ravendb-test-driver`` can +attach to a RavenDB you run yourself (Docker, testcontainers, a shared CI service) while still +giving each test its own database. See the ``ravendb-python-testdriver`` repository. + +============= +Configuration +============= - secured(server_pfx_certificate_path, client_pem_certificate_path, server_pfx_certificate_password=None, ca_certificate_path=None) +``ServerOptions`` +----------------- -- Provide the path to ``.pfx`` and ``.pem`` files, and optionally a password and CA certificate file. -- Minimal setup requires both a ``.pfx`` server and a ``.pem`` client certificate. +Create ``ServerOptions()`` and set attributes: -Get Document Store ------------------- +- ``data_directory``: where database data is stored (defaults to a local ``RavenDB`` folder). +- ``server_url``: the URL to bind (defaults to localhost on a free port). +- ``dot_net_path``: path to ``dotnet`` when it is not on ``PATH`` (ignored on the self-contained path). +- ``command_line_args``: extra `server command-line arguments `_. +- ``framework_version``: pin an exact .NET version (advanced; leave empty to autodetect the installed runtime). + +Security +-------- + +Secure the server with ``ServerOptions.secured()``: + +.. code-block:: python -After initializing and starting the server, you can use the ``get_document_store`` method to obtain a ``DocumentStore`` and start working with RavenDB as usual. + options = ServerOptions() + options.secured( + server_pfx_certificate_path, # server certificate (.pfx), required + client_pem_certificate_path, # client certificate (.pem) + server_pfx_certificate_password=None, + ca_certificate_path=None, + ) -``get_document_store`` method can take either just the ``database_name`` or ``DatabaseOptions``. +Working with data +----------------- -DatabaseOptions ---------------- +``get_document_store(database_name)`` returns a ``DocumentStore`` you use like any RavenDB +client. For finer control, build a ``DatabaseOptions`` (via ``DatabaseOptions.from_database_name``) +and call ``get_document_store_from_options``; set ``skip_creating_database=True`` to not +auto-create the database. -- ``database_name``: The name of the database. -- ``skip_creating_database``: ``get_document_store`` will create a new database if it does not exist. If this option is set to True, the database won't be created (Default False). +Call ``open_studio_in_browser()`` to open RavenDB Studio in your default browser. -Open RavenDB Studio in the Browser ------------------------------------ +==== +Labs +==== -To open RavenDB Studio from ``ravendb-embedded``, use the ``open_studio_in_browser`` method, and the studio will open automatically in your default browser. +The ``labs/`` folder holds runnable, self-checking guides, one per usage case above. Start at +`labs/README.md `_. From 499fc9590d0cfcbae66d5eb9525262d42b0ba17c Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 03:43:55 +0200 Subject: [PATCH 11/23] RavenDB-27069 Bump to 7.2.5 (bundled server + package); pin client to ravendb==7.2.3.post1 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 4342ddd..acfc511 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import urllib.request from setuptools.command.sdist import sdist -RAVENDB_VERSION = "7.2.3" +RAVENDB_VERSION = "7.2.5" ZIP_FILE_NAME = "server.zip" RAVENDB_DOWNLOAD_URL = f"https://www.nuget.org/api/v2/package/RavenDB.Embedded/{RAVENDB_VERSION}" @@ -45,7 +45,7 @@ def run(self): package_dir={"ravendb_embedded": "ravendb_embedded"}, include_package_data=True, long_description=open("README.rst").read(), - version="7.2.3", + version="7.2.5", description="RavenDB Embedded library to run RavenDB in an embedded way", author="RavenDB", author_email="support@ravendb.net", @@ -53,7 +53,7 @@ def run(self): license="Custom EULA", keywords="ravendb embedded database nosql doc db", install_requires=[ - "ravendb~=7.2.3", + "ravendb==7.2.3.post1", "cryptography>=42.0.0", ], license_files="LICENSE", From 0476851b4bec9b12a8fddfa74ee0ae5a3a99997e Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 03:43:56 +0200 Subject: [PATCH 12/23] RavenDB-27069 Pin GitHub Actions to commit SHA; add Dependabot (actions + pip) --- .github/dependabot.yml | 15 +++++++++++++++ .github/workflows/tests.yml | 10 +++++----- 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b8a5b00 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + github-actions: + patterns: + - "*" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31b46e5..204b9f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,15 +19,15 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Set up .NET 10 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: "10.0" @@ -64,10 +64,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Set up Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" From ca363bb6a760cec5d6716f548ecf2838a23f39aa Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 03:49:42 +0200 Subject: [PATCH 13/23] RavenDB-27069 Renumber on-demand to Lab 03; document pulling latest self-contained as intentional --- README.rst | 2 +- ...emand-server.md => 03-on-demand-server.md} | 27 ++++++++++------- labs/README.md | 30 ++++++++----------- labs/on_demand_server.py | 4 +-- 4 files changed, 33 insertions(+), 30 deletions(-) rename labs/{04-on-demand-server.md => 03-on-demand-server.md} (68%) diff --git a/README.rst b/README.rst index fafbf94..8977354 100644 --- a/README.rst +++ b/README.rst @@ -107,7 +107,7 @@ Download self-contained builds from the RavenDB downloads page (one archive per server files live in the archive's ``Server/`` folder. Runnable walkthrough: `labs/02-embedded-external-server.md `_. An exploratory helper that downloads and caches a build on first use is in -`labs/04-on-demand-server.md `_. +`labs/03-on-demand-server.md `_. Don't manage a server at all (tests) ------------------------------------- diff --git a/labs/04-on-demand-server.md b/labs/03-on-demand-server.md similarity index 68% rename from labs/04-on-demand-server.md rename to labs/03-on-demand-server.md index d2e5b05..8026636 100644 --- a/labs/04-on-demand-server.md +++ b/labs/03-on-demand-server.md @@ -1,4 +1,4 @@ -# Lab 04: On-demand, cached self-contained server (exploration) +# Lab 03: On-demand, cached self-contained server (exploration) **Status:** exploration. This is NOT wired into the package default. It shows how a future "just works, no .NET" acquisition path could look, in the spirit of how Playwright fetches its @@ -51,17 +51,24 @@ Because the downloaded build is self-contained, the run path is identical to Lab `ExternalServerProvider` sees `includedFrameworks` in the runtime config, runs the native apphost (`Raven.Server.exe` on Windows, `Raven.Server` elsewhere), and never calls `dotnet`. -## Why it is only an exploration +## Why pulling `latest` is fine (on purpose) -- It downloads `latest` for a version line, so a run is only as reproducible as that endpoint. -- A self-contained build is large (100 MB+), so the first-use cost and the cache footprint are - real; a shipped version would want checksums, a pinned build, and a documented cache location. +Fetching the `latest` self-contained build for the version line is deliberate. A self-contained +build bundles its own .NET runtime, so it does not have to match anything on the host: whatever +`latest` returns is a server that just runs. There is no host .NET compatibility matrix to pin +against, which is exactly what makes "grab latest and run" safe here (a framework-dependent build +could not make that promise). The cache then freezes whatever you first pulled, so later runs stay +stable without any extra pinning. + +## Why it is still only an exploration + +- A self-contained build is large (100 MB+), so the first-use download and the on-disk cache + footprint are real costs. - Making this the default changes the install story (a network fetch on first use) and needs a - decision on where the cache lives and how it is invalidated. + decision on where the cache lives, how big it may grow, and when it is invalidated. ## Takeaway -The acquisition step from Lab 02 can be automated and cached, giving a no-.NET experience with -no manual download. Shipping it as a default is a product decision, not a code gap: the -mechanism works (this lab runs it), what is missing is the reproducibility and cache-policy -guarantees a default would need. +The acquisition step from Lab 02 can be automated and cached, giving a no-.NET experience with no +manual download. The mechanism works (this lab runs it); making it the package default is a +product decision about the install story and cache policy, not a code gap. diff --git a/labs/README.md b/labs/README.md index 3a158fc..0524100 100644 --- a/labs/README.md +++ b/labs/README.md @@ -1,21 +1,17 @@ -# RavenDB Python: test server labs +# ravendb-embedded: labs -Ways to get a RavenDB server for your tests, from most convenient to most portable. -Pick the one that matches your environment. +Runnable, self-checking guides for getting a RavenDB server, from most convenient to most +portable. Each lab ships a script next to it, so you can run the exact code the guide shows. -| Lab | Path | For whom | Needs system .NET? | -|-----|------|----------|--------------------| -| [01](01-embedded-zero-config.md) | Embedded, zero-config | Local dev or CI on a machine that already has .NET | Yes (.NET 10 for 7.2.x) | -| [02](02-embedded-external-server.md) | External self-contained server | You do not want to install .NET at all | No | -| 03 | Attach to a server you run yourself (Docker, testcontainers, shared CI) | Containerized CI pipelines | No | -| [04](04-on-demand-server.md) | On-demand cached self-contained download (exploration) | Lab 02 without the manual download | No | +| Lab | Covers | Needs system .NET? | +|-----|--------|--------------------| +| [01](01-embedded-zero-config.md) | Embedded, zero-config (the default) | Yes (.NET 10 for 7.2.x) | +| [02](02-embedded-external-server.md) | External self-contained server you provide | No | +| [03](03-on-demand-server.md) | On-demand cached self-contained download (exploration) | No | -Lab 03 (attach) lives in the [`ravendb-python-testdriver`](https://github.com/ravendb/ravendb-python-testdriver) -repository, since attaching to an external server is a test-driver feature. Lab 04 is an -exploration and is not wired into the package default. +RavenDB version to .NET mapping: **7.1.x needs .NET 8, 7.2.x needs .NET 10.** The bundled server +decides this, so it can change on a minor bump; check the lab for your version. -RavenDB version to .NET mapping: **7.1.x needs .NET 8, 7.2.x needs .NET 10.** The bundled -server is the deciding factor, so this can change on a minor bump; always check the lab for -your version. - -Every lab ships a runnable script next to it, so you can run the exact code the guide shows. +Want the driver to attach to a server you run yourself (Docker, testcontainers, shared CI) instead +of running one? That is a test-driver feature and has its own lab in the +[`ravendb-python-testdriver`](https://github.com/ravendb/ravendb-python-testdriver) package. diff --git a/labs/on_demand_server.py b/labs/on_demand_server.py index 79ef7f1..f5d8424 100644 --- a/labs/on_demand_server.py +++ b/labs/on_demand_server.py @@ -1,4 +1,4 @@ -"""Lab 04 (exploration): on-demand, cached self-contained server download. +"""Lab 03 (exploration): on-demand, cached self-contained server download. A Playwright-style acquisition prototype: fetch a self-contained RavenDB server for the current platform on first use, cache it, and reuse the cached copy next time (no re-download, no .NET). @@ -78,7 +78,7 @@ def main(): session.store({"name": "on-demand"}, "people/1") session.save_changes() - print("Lab 04 OK: downloaded + cached a self-contained server and ran it with no system .NET.") + print("Lab 03 OK: downloaded + cached a self-contained server and ran it with no system .NET.") if __name__ == "__main__": From a7c60687a8fa7a8a755d6d9be72057a5b35274c1 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 04:53:08 +0200 Subject: [PATCH 14/23] RavenDB-27069 Add Lab 04 (secured) and Lab 05 (persistent data dir); run both in CI --- .github/workflows/tests.yml | 6 ++ README.rst | 6 +- labs/04-embedded-secured.md | 43 +++++++++++++ labs/04_embedded_secured.py | 113 +++++++++++++++++++++++++++++++++ labs/05-embedded-persistent.md | 48 ++++++++++++++ labs/05_embedded_persistent.py | 51 +++++++++++++++ labs/README.md | 2 + 7 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 labs/04-embedded-secured.md create mode 100644 labs/04_embedded_secured.py create mode 100644 labs/05-embedded-persistent.md create mode 100644 labs/05_embedded_persistent.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 204b9f9..e7c7259 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,6 +57,12 @@ jobs: - name: Run Lab 01 (embedded zero-config) run: python labs/01_embedded_zero_config.py + - name: Run Lab 04 (secured embedded) + run: python labs/04_embedded_secured.py + + - name: Run Lab 05 (persistent data directory) + run: python labs/05_embedded_persistent.py + self-contained: # Path B: external self-contained server -> runs the native apphost, no system .NET. # We do not set up .NET; the lab uses a bogus dot_net_path so a successful run proves the diff --git a/README.rst b/README.rst index 8977354..cc6f81f 100644 --- a/README.rst +++ b/README.rst @@ -125,7 +125,8 @@ Configuration Create ``ServerOptions()`` and set attributes: -- ``data_directory``: where database data is stored (defaults to a local ``RavenDB`` folder). +- ``data_directory``: where database data is stored (defaults to a local ``RavenDB`` folder). Set a + stable path for data that outlives the process, see `labs/05-embedded-persistent.md `_. - ``server_url``: the URL to bind (defaults to localhost on a free port). - ``dot_net_path``: path to ``dotnet`` when it is not on ``PATH`` (ignored on the self-contained path). - ``command_line_args``: extra `server command-line arguments `_. @@ -146,6 +147,9 @@ Secure the server with ``ServerOptions.secured()``: ca_certificate_path=None, ) +Runnable example (HTTPS + client-certificate auth): +`labs/04-embedded-secured.md `_. + Working with data ----------------- diff --git a/labs/04-embedded-secured.md b/labs/04-embedded-secured.md new file mode 100644 index 0000000..b478edd --- /dev/null +++ b/labs/04-embedded-secured.md @@ -0,0 +1,43 @@ +# Lab 04: Secured embedded server (HTTPS + client certificate) + +**For:** running the embedded server with TLS and client-certificate authentication instead of +plain HTTP. You bring a server certificate (`.pfx`) and a client certificate (`.pem`); +`ServerOptions.secured()` wires them in, and the driver hands you a store already configured with +the client cert, so your session code does not change. + +## Run it + +```bash +pip install ravendb-embedded +python labs/04_embedded_secured.py +``` + +The complete example is [`04_embedded_secured.py`](04_embedded_secured.py). The core is: + +```python +from ravendb_embedded import EmbeddedServer, ServerOptions +from ravendb_embedded.options import DatabaseOptions + +options = ServerOptions() +options.secured(server_pfx_path, client_pem_path, ca_certificate_path=ca_crt_path) + +with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store_from_options(DatabaseOptions.from_database_name("Lab")) as store: + # store.urls[0] is now https://...; the client certificate is already attached + ... +``` + +## Certificates + +`secured()` needs a server `.pfx` and a client `.pem`; a password and a CA certificate are +optional. In production you bring your own (for example from RavenDB's setup wizard or your CA). +The lab generates a throwaway self-signed pair so it can run unattended; that generator is demo +code, not something to ship. A RavenDB server certificate needs the right extensions +(digitalSignature key usage, serverAuth/clientAuth EKU, and SANs for how the server is reached), +which the lab's generator sets. + +## Takeaway + +Securing the embedded server is one `secured()` call plus certificates. Everything after +`get_document_store` is ordinary RavenDB client code, now over HTTPS with client-certificate auth. diff --git a/labs/04_embedded_secured.py b/labs/04_embedded_secured.py new file mode 100644 index 0000000..cb835d7 --- /dev/null +++ b/labs/04_embedded_secured.py @@ -0,0 +1,113 @@ +"""Lab 04: Secured embedded server (HTTPS + client-certificate auth). + +For: running the embedded server with TLS and client-certificate authentication instead of plain +HTTP. Point `ServerOptions.secured()` at a server certificate (.pfx) and a client certificate +(.pem); the driver wires the client cert into the store, so your session code is unchanged. + +In production you bring your own certificates. This lab generates a throwaway self-signed pair so +it can run unattended. + +Run: python labs/04_embedded_secured.py +""" + +import datetime +import ipaddress +import shutil +import tempfile +from pathlib import Path + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.serialization import pkcs12 +from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID + +from ravendb_embedded import EmbeddedServer, ServerOptions +from ravendb_embedded.options import DatabaseOptions + + +def _demo_certificates(directory): + """Throwaway self-signed server.pfx + client.pem (demo only; bring your own in production). + + Carries what RavenDB requires of a server certificate: digitalSignature key usage, + serverAuth/clientAuth EKU, and SANs for how the embedded server is reached (localhost, + 127.0.0.1). The same cert doubles as the trusted admin client certificate. + """ + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")]) + now = datetime.datetime.now(datetime.timezone.utc) + cert = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - datetime.timedelta(days=1)) + .not_valid_after(now + datetime.timedelta(days=3650)) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("localhost"), x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]), + critical=False, + ) + .add_extension( + x509.KeyUsage( + digital_signature=True, + key_encipherment=True, + content_commitment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ), + critical=True, + ) + .add_extension( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH, ExtendedKeyUsageOID.CLIENT_AUTH]), critical=False + ) + .sign(key, hashes.SHA256()) + ) + cert_pem = cert.public_bytes(serialization.Encoding.PEM) + key_pem = key.private_bytes( + serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption() + ) + server_pfx, client_pem, ca_crt = ( + Path(directory, "server.pfx"), + Path(directory, "client.pem"), + Path(directory, "ca.crt"), + ) + server_pfx.write_bytes( + pkcs12.serialize_key_and_certificates(b"localhost", key, cert, None, serialization.NoEncryption()) + ) + client_pem.write_bytes(key_pem + cert_pem) + ca_crt.write_bytes(cert_pem) + return str(server_pfx), str(client_pem), str(ca_crt) + + +def main() -> None: + work = tempfile.mkdtemp() + try: + server_pfx, client_pem, ca_crt = _demo_certificates(work) + + options = ServerOptions() + options.secured(server_pfx, client_pem, ca_certificate_path=ca_crt) # HTTPS + client-cert auth + options.data_directory = str(Path(work, "RavenDB")) + options.logs_path = str(Path(work, "Logs")) + + with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store_from_options(DatabaseOptions.from_database_name("Lab")) as store: + assert store.urls[0].startswith("https://"), store.urls + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() + with store.open_session() as session: + assert session.load("people/1", dict)["name"] == "Ayende" + finally: + shutil.rmtree(work, ignore_errors=True) + + print("Lab 04 OK: secured embedded server over HTTPS with client-certificate auth.") + + +if __name__ == "__main__": + main() diff --git a/labs/05-embedded-persistent.md b/labs/05-embedded-persistent.md new file mode 100644 index 0000000..9a1f869 --- /dev/null +++ b/labs/05-embedded-persistent.md @@ -0,0 +1,48 @@ +# Lab 05: Persistent data directory (data survives restarts) + +**For:** using the embedded server as a durable local database rather than a throwaway. Point +`data_directory` at a fixed folder and your databases and documents live there, still present the +next time you start the server against the same folder. + +## Run it + +```bash +pip install ravendb-embedded +python labs/05_embedded_persistent.py +``` + +The complete example is [`05_embedded_persistent.py`](05_embedded_persistent.py). The core is: + +```python +from ravendb_embedded import EmbeddedServer, ServerOptions + +def options(data_directory, logs_path): + o = ServerOptions() + o.data_directory = data_directory # a fixed folder you reuse across restarts + o.logs_path = logs_path + return o + +# First run: write, then shut down. +with EmbeddedServer() as server: + server.start_server(options(data_directory, logs_path)) + with server.get_document_store("Lab") as store: + ... # store a document + +# Later run against the SAME data_directory: the document is still there. +with EmbeddedServer() as server: + server.start_server(options(data_directory, logs_path)) + with server.get_document_store("Lab") as store: + ... # load it back +``` + +## Notes + +- The default `data_directory` is a folder under the package location, which is fine for tests + but not what you want for real data. Set it explicitly to a path you control. +- `get_document_store("Lab")` reuses the existing database on the second run; it only creates one + when it is missing. + +## Takeaway + +Embedded is not in-memory. Give it a stable `data_directory` and it behaves like a normal local +RavenDB whose data outlives the process. diff --git a/labs/05_embedded_persistent.py b/labs/05_embedded_persistent.py new file mode 100644 index 0000000..a29c5b7 --- /dev/null +++ b/labs/05_embedded_persistent.py @@ -0,0 +1,51 @@ +"""Lab 05: Persistent data directory (data survives restarts). + +For: using the embedded server as a durable local database, not a throwaway. Point +`data_directory` at a fixed folder; the databases and documents live there and are still present +the next time you start the server against the same folder. + +Run: python labs/05_embedded_persistent.py +""" + +import shutil +import tempfile +from pathlib import Path + +from ravendb_embedded import EmbeddedServer, ServerOptions + + +def _options(data_directory, logs_path): + options = ServerOptions() + options.data_directory = data_directory # the fixed folder we reuse across restarts + options.logs_path = logs_path + return options + + +def main() -> None: + root = tempfile.mkdtemp() + data_directory = str(Path(root, "RavenDB")) + logs_path = str(Path(root, "Logs")) + try: + # First run: write a document, then shut the server down (context exit stops it). + with EmbeddedServer() as server: + server.start_server(_options(data_directory, logs_path)) + with server.get_document_store("Lab") as store: + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() + + # Second run against the SAME data_directory: the document is still there. + with EmbeddedServer() as server: + server.start_server(_options(data_directory, logs_path)) + with server.get_document_store("Lab") as store: + with store.open_session() as session: + loaded = session.load("people/1", dict) + assert loaded is not None and loaded["name"] == "Ayende", loaded + finally: + shutil.rmtree(root, ignore_errors=True) + + print("Lab 05 OK: the document written in the first run survived a full server restart.") + + +if __name__ == "__main__": + main() diff --git a/labs/README.md b/labs/README.md index 0524100..9626b4a 100644 --- a/labs/README.md +++ b/labs/README.md @@ -8,6 +8,8 @@ portable. Each lab ships a script next to it, so you can run the exact code the | [01](01-embedded-zero-config.md) | Embedded, zero-config (the default) | Yes (.NET 10 for 7.2.x) | | [02](02-embedded-external-server.md) | External self-contained server you provide | No | | [03](03-on-demand-server.md) | On-demand cached self-contained download (exploration) | No | +| [04](04-embedded-secured.md) | Secured embedded server (HTTPS + client certificate) | Yes | +| [05](05-embedded-persistent.md) | Persistent data directory (data survives restarts) | Yes | RavenDB version to .NET mapping: **7.1.x needs .NET 8, 7.2.x needs .NET 10.** The bundled server decides this, so it can change on a minor bump; check the lab for your version. From 0c1a1ab95f2e1da37aea69eedbebe80313113c20 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 05:02:29 +0200 Subject: [PATCH 15/23] RavenDB-27069 Implement on-demand self-contained server as a real feature (ServerOptions.with_on_demand_server) --- .github/workflows/tests.yml | 3 + README.rst | 13 +++- labs/03-on-demand-server.md | 59 ++++++----------- labs/03_on_demand_server.py | 38 +++++++++++ labs/README.md | 2 +- labs/on_demand_server.py | 85 ------------------------- ravendb_embedded/__init__.py | 1 + ravendb_embedded/on_demand.py | 81 +++++++++++++++++++++++ ravendb_embedded/options.py | 5 ++ ravendb_embedded/raven_server_runner.py | 3 +- tests/test_on_demand.py | 26 ++++++++ 11 files changed, 184 insertions(+), 132 deletions(-) create mode 100644 labs/03_on_demand_server.py delete mode 100644 labs/on_demand_server.py create mode 100644 ravendb_embedded/on_demand.py create mode 100644 tests/test_on_demand.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e7c7259..58341d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -92,3 +92,6 @@ jobs: run: | SERVER="$(find server -type d -name Server | head -1)" RAVENDB_SELF_CONTAINED_SERVER="$SERVER" python labs/02_embedded_external_server.py + + - name: Run Lab 03 (on-demand cached self-contained, no system .NET) + run: python labs/03_on_demand_server.py diff --git a/README.rst b/README.rst index cc6f81f..7899b50 100644 --- a/README.rst +++ b/README.rst @@ -105,9 +105,16 @@ apphost directly, never calling ``dotnet``. Download self-contained builds from the RavenDB downloads page (one archive per platform); the server files live in the archive's ``Server/`` folder. Runnable walkthrough: -`labs/02-embedded-external-server.md `_. An exploratory -helper that downloads and caches a build on first use is in -`labs/03-on-demand-server.md `_. +`labs/02-embedded-external-server.md `_. + +Or skip the manual download and let the driver fetch and cache one for you on first use: + +.. code-block:: python + + options = ServerOptions() + options.with_on_demand_server() # downloads + caches a self-contained server, no .NET needed + +Walkthrough: `labs/03-on-demand-server.md `_. Don't manage a server at all (tests) ------------------------------------- diff --git a/labs/03-on-demand-server.md b/labs/03-on-demand-server.md index 8026636..7feb549 100644 --- a/labs/03-on-demand-server.md +++ b/labs/03-on-demand-server.md @@ -1,55 +1,34 @@ -# Lab 03: On-demand, cached self-contained server (exploration) +# Lab 03: On-demand, cached self-contained server (no .NET) -**Status:** exploration. This is NOT wired into the package default. It shows how a future -"just works, no .NET" acquisition path could look, in the spirit of how Playwright fetches its -browsers on first use. - -**For:** anyone who wants Lab 02 (no system .NET) without manually downloading and extracting a -server. The helper fetches a self-contained build for the current platform on first use, caches -it, and reuses the cached copy every time after that. - -## The idea - -Lab 02 needs a self-contained `Server/` folder that you download and extract yourself. This lab -automates that one step: - -1. On first use, download the self-contained build for this OS and architecture. -2. Extract it into a cache directory and remember it. -3. On every later run, reuse the cached copy: no re-download, no `dotnet`. +**For:** the no-.NET experience of Lab 02 without downloading and extracting a server yourself. +Call `with_on_demand_server()`; on first use the driver fetches a self-contained build for this +platform, caches it, and reuses the cache from then on. A self-contained build bundles its own +runtime, so the server runs its native apphost and never calls `dotnet`. ## Run it ```bash pip install ravendb-embedded -python labs/on_demand_server.py +python labs/03_on_demand_server.py ``` -The complete example is [`on_demand_server.py`](on_demand_server.py). The core is: +The complete example is [`03_on_demand_server.py`](03_on_demand_server.py). The core is: ```python from ravendb_embedded import EmbeddedServer, ServerOptions -from on_demand_server import ensure_server -server_dir = ensure_server() # download+cache on first use, cache hit afterwards options = ServerOptions() -options.with_external_server(server_dir) # a self-contained build, so no dotnet +options.with_on_demand_server() # download + cache a self-contained server on first use + with EmbeddedServer() as server: server.start_server(options) with server.get_document_store("Lab") as store: ... # ordinary RavenDB client code, with no .NET on the machine ``` -## Caching, by design - -`ensure_server()` keys the cache on version + platform label and looks for -`Raven.Server.dll` under the cache directory. If it is present the download step is skipped -entirely, so the second run (and every run after) is offline and instant. The default cache -root is `~/.cache/ravendb-embedded`; pass `cache_root=...` to override it (the CI-style pattern -is to point it at a directory the CI cache restores between runs). - -Because the downloaded build is self-contained, the run path is identical to Lab 02: -`ExternalServerProvider` sees `includedFrameworks` in the runtime config, runs the native -apphost (`Raven.Server.exe` on Windows, `Raven.Server` elsewhere), and never calls `dotnet`. +`with_on_demand_server(version=None, cache_root=None)` defaults the version to the installed +package's RavenDB line and caches under `~/.cache/ravendb-embedded`. Pass `cache_root` to point +it at a directory your CI restores between runs. ## Why pulling `latest` is fine (on purpose) @@ -60,15 +39,13 @@ against, which is exactly what makes "grab latest and run" safe here (a framewor could not make that promise). The cache then freezes whatever you first pulled, so later runs stay stable without any extra pinning. -## Why it is still only an exploration +## Cost to know about -- A self-contained build is large (100 MB+), so the first-use download and the on-disk cache - footprint are real costs. -- Making this the default changes the install story (a network fetch on first use) and needs a - decision on where the cache lives, how big it may grow, and when it is invalidated. +The first use downloads a self-contained build (100 MB+) and the cache keeps it on disk. Every +run after that is offline and instant. If disk or first-run latency matters, prefer Lab 02 (you +provide the server) or Lab 01 (bundled server, needs .NET). ## Takeaway -The acquisition step from Lab 02 can be automated and cached, giving a no-.NET experience with no -manual download. The mechanism works (this lab runs it); making it the package default is a -product decision about the install story and cache policy, not a code gap. +`with_on_demand_server()` gives the no-.NET path of Lab 02 with zero manual steps: one call, then +ordinary client code. diff --git a/labs/03_on_demand_server.py b/labs/03_on_demand_server.py new file mode 100644 index 0000000..a5042bf --- /dev/null +++ b/labs/03_on_demand_server.py @@ -0,0 +1,38 @@ +"""Lab 03: On-demand, cached self-contained server (no manual download, no .NET). + +For: Lab 02 (no system .NET) without downloading and extracting a server yourself. Call +`with_on_demand_server()`; on first use the driver fetches a self-contained build for this +platform, caches it, and reuses the cache next time. Because the build is self-contained it runs +its native apphost and never calls `dotnet`. + +Run: python labs/03_on_demand_server.py +""" + +import tempfile +from pathlib import Path + +from ravendb_embedded import EmbeddedServer, ServerOptions + + +def main() -> None: + with tempfile.TemporaryDirectory() as work: + options = ServerOptions() + options.with_on_demand_server() # download + cache a self-contained server on first use + options.target_server_location = str(Path(work, "server")) + options.data_directory = str(Path(work, "data")) + options.logs_path = str(Path(work, "logs")) + + with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("Lab") as store: + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() + with store.open_session() as session: + assert session.load("people/1", dict)["name"] == "Ayende" + + print("Lab 03 OK: on-demand self-contained server, cached and run with no system .NET.") + + +if __name__ == "__main__": + main() diff --git a/labs/README.md b/labs/README.md index 9626b4a..78f2b3c 100644 --- a/labs/README.md +++ b/labs/README.md @@ -7,7 +7,7 @@ portable. Each lab ships a script next to it, so you can run the exact code the |-----|--------|--------------------| | [01](01-embedded-zero-config.md) | Embedded, zero-config (the default) | Yes (.NET 10 for 7.2.x) | | [02](02-embedded-external-server.md) | External self-contained server you provide | No | -| [03](03-on-demand-server.md) | On-demand cached self-contained download (exploration) | No | +| [03](03-on-demand-server.md) | On-demand cached self-contained download (no manual steps) | No | | [04](04-embedded-secured.md) | Secured embedded server (HTTPS + client certificate) | Yes | | [05](05-embedded-persistent.md) | Persistent data directory (data survives restarts) | Yes | diff --git a/labs/on_demand_server.py b/labs/on_demand_server.py deleted file mode 100644 index f5d8424..0000000 --- a/labs/on_demand_server.py +++ /dev/null @@ -1,85 +0,0 @@ -"""Lab 03 (exploration): on-demand, cached self-contained server download. - -A Playwright-style acquisition prototype: fetch a self-contained RavenDB server for the current -platform on first use, cache it, and reuse the cached copy next time (no re-download, no .NET). -This is exploratory and is NOT wired into the package default; it shows how a future -"just works, no .NET" acquisition path could look. - -Run: python labs/on_demand_server.py -""" - -import platform -import tarfile -import tempfile -import urllib.request -import zipfile -from pathlib import Path - -RAVENDB_VERSION = "7.2" - - -def _platform_download(): - machine = platform.machine().lower() - arch = "arm64" if machine in ("arm64", "aarch64") else "x64" - system = platform.system() - if system == "Windows": - return f"RavenDB for Windows {arch}", "zip" - if system == "Darwin": - return f"RavenDB for OSX {arch}", "tar.bz2" - return f"RavenDB for Linux {arch}", "tar.bz2" - - -def ensure_server(version=RAVENDB_VERSION, cache_root=None): - """Return a local self-contained Server directory, downloading and caching on first use.""" - cache_root = Path(cache_root) if cache_root else Path.home() / ".cache" / "ravendb-embedded" - label, extension = _platform_download() - target = cache_root / version / label.replace(" ", "_") - - cached = next(target.rglob("Raven.Server.dll"), None) if target.is_dir() else None - if cached: # cache hit: never download again ("by design") - return str(cached.parent) - - target.mkdir(parents=True, exist_ok=True) - url = f"https://hibernatingrhinos.com/downloads/{label.replace(' ', '%20')}/latest?version={version}" - archive = target / f"ravendb.{extension}" - print(f"downloading {url}") - urllib.request.urlretrieve(url, archive) - if extension == "zip": - with zipfile.ZipFile(archive) as bundle: - bundle.extractall(target) - else: - with tarfile.open(archive) as bundle: - bundle.extractall(target) - archive.unlink() - - server = next(target.rglob("Raven.Server.dll"), None) - if not server: - raise RuntimeError(f"Server binaries not found under {target}") - return str(server.parent) - - -def main(): - from ravendb_embedded import EmbeddedServer, ServerOptions - - server_dir = ensure_server() - print("server ready at", server_dir) - - with tempfile.TemporaryDirectory() as work: - options = ServerOptions() - options.target_server_location = str(Path(work, "server")) - options.data_directory = str(Path(work, "data")) - options.logs_path = str(Path(work, "logs")) - options.dot_net_path = "__no_dotnet__" # proves the cached build needs no system .NET - options.with_external_server(server_dir) - with EmbeddedServer() as server: - server.start_server(options) - with server.get_document_store("Lab") as store: - with store.open_session() as session: - session.store({"name": "on-demand"}, "people/1") - session.save_changes() - - print("Lab 03 OK: downloaded + cached a self-contained server and ran it with no system .NET.") - - -if __name__ == "__main__": - main() diff --git a/ravendb_embedded/__init__.py b/ravendb_embedded/__init__.py index f211727..dd51b5f 100644 --- a/ravendb_embedded/__init__.py +++ b/ravendb_embedded/__init__.py @@ -1,5 +1,6 @@ from ravendb_embedded.embedded_server import EmbeddedServer from ravendb_embedded.options import DatabaseOptions, ServerOptions, SecurityOptions +from ravendb_embedded.on_demand import OnDemandServerProvider, ensure_server from ravendb_embedded.provide import ( CopyServerFromNugetProvider, CopyServerProvider, diff --git a/ravendb_embedded/on_demand.py b/ravendb_embedded/on_demand.py new file mode 100644 index 0000000..cb6ba28 --- /dev/null +++ b/ravendb_embedded/on_demand.py @@ -0,0 +1,81 @@ +import platform +import tarfile +import urllib.request +import zipfile +from pathlib import Path + +from ravendb_embedded.provide import CopyServerProvider, ProvideRavenDBServer + +_DOWNLOAD_BASE = "https://hibernatingrhinos.com/downloads" + + +def _default_version_line() -> str: + # Match the installed package's RavenDB line (e.g. 7.2.5 -> "7.2"); fall back if unknown. + try: + from importlib.metadata import version + + return ".".join(version("ravendb-embedded").split(".")[:2]) + except Exception: + return "7.2" + + +def _platform_download() -> tuple: + machine = platform.machine().lower() + arch = "arm64" if machine in ("arm64", "aarch64") else "x64" + system = platform.system() + if system == "Windows": + return f"RavenDB for Windows {arch}", "zip" + if system == "Darwin": + return f"RavenDB for OSX {arch}", "tar.bz2" + return f"RavenDB for Linux {arch}", "tar.bz2" + + +def ensure_server(version: str = None, cache_root: str = None) -> str: + """Return a local self-contained Server directory, downloading and caching on first use. + + The download is a self-contained build (bundles its own .NET), so it runs with no system + .NET. Pulling `latest` for the version line is intentional: a self-contained build never has + to match anything on the host. The cache is keyed on version + platform, so later runs reuse + it and never re-download. + """ + version = version or _default_version_line() + cache_root = Path(cache_root) if cache_root else Path.home() / ".cache" / "ravendb-embedded" + label, extension = _platform_download() + target = cache_root / version / label.replace(" ", "_") + + cached = next(target.rglob("Raven.Server.dll"), None) if target.is_dir() else None + if cached: + return str(cached.parent) + + target.mkdir(parents=True, exist_ok=True) + url = f"{_DOWNLOAD_BASE}/{label.replace(' ', '%20')}/latest?version={version}" + archive = target / f"ravendb.{extension}" + urllib.request.urlretrieve(url, archive) + if extension == "zip": + with zipfile.ZipFile(archive) as bundle: + bundle.extractall(target) + else: + with tarfile.open(archive) as bundle: + bundle.extractall(target) + archive.unlink() + + server = next(target.rglob("Raven.Server.dll"), None) + if not server: + raise RuntimeError(f"Server binaries not found under {target}") + return str(server.parent) + + +class OnDemandServerProvider(ProvideRavenDBServer): + """Download (once) and cache a self-contained server, then run it with no system .NET. + + The self-contained build bundles its own runtime, so the server runs via its native apphost; + `dot_net_path` is never used. The download happens on first `start_server()` and is cached. + """ + + def __init__(self, version: str = None, cache_root: str = None): + self.version = version + self.cache_root = cache_root + self.is_single_file_app = True + + def provide(self, target_directory: str) -> None: + CopyServerProvider(ensure_server(self.version, self.cache_root)).provide(target_directory) diff --git a/ravendb_embedded/options.py b/ravendb_embedded/options.py index d483837..d567954 100644 --- a/ravendb_embedded/options.py +++ b/ravendb_embedded/options.py @@ -14,6 +14,7 @@ ExternalServerProvider, CopyServerFromNugetProvider, ) +from ravendb_embedded.on_demand import OnDemandServerProvider class DatabaseOptions: @@ -96,3 +97,7 @@ def secured( def with_external_server(self, server_location: str) -> None: self.provider = ExternalServerProvider(server_location) + + def with_on_demand_server(self, version: str = None, cache_root: str = None) -> None: + # Download (once) and cache a self-contained server, then run it with no system .NET. + self.provider = OnDemandServerProvider(version, cache_root) diff --git a/ravendb_embedded/raven_server_runner.py b/ravendb_embedded/raven_server_runner.py index fd27b72..30923df 100644 --- a/ravendb_embedded/raven_server_runner.py +++ b/ravendb_embedded/raven_server_runner.py @@ -6,7 +6,6 @@ from cryptography.hazmat.primitives import hashes from ravendb.exceptions.raven_exceptions import RavenException -from ravendb_embedded.provide import ExternalServerProvider from ravendb_embedded.options import ServerOptions from ravendb_embedded.runtime_framework_version_matcher import ( RuntimeFrameworkVersionMatcher, @@ -25,7 +24,7 @@ def run(options: ServerOptions) -> subprocess.Popen: if not options.logs_path.strip(): raise ValueError("logs_path cannot be None or whitespace") - is_sfa = isinstance(options.provider, ExternalServerProvider) and options.provider.is_single_file_app + is_sfa = getattr(options.provider, "is_single_file_app", False) if is_sfa: # Self-contained / single-file build: run the native apphost, no `dotnet`. file_name = "Raven.Server.exe" if os.name == "nt" else "Raven.Server" diff --git a/tests/test_on_demand.py b/tests/test_on_demand.py new file mode 100644 index 0000000..8b7dd7e --- /dev/null +++ b/tests/test_on_demand.py @@ -0,0 +1,26 @@ +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +from ravendb_embedded.on_demand import ensure_server, _platform_download + + +class TestOnDemand(unittest.TestCase): + def test_cache_hit_skips_download(self): + # A populated cache must be reused without any network call ("by design"). + with tempfile.TemporaryDirectory() as cache_root: + label, _ = _platform_download() + server_dir = Path(cache_root, "7.2", label.replace(" ", "_"), "Server") + server_dir.mkdir(parents=True) + (server_dir / "Raven.Server.dll").write_bytes(b"stub") + + with mock.patch("urllib.request.urlretrieve", side_effect=AssertionError("cache hit must not download")): + resolved = ensure_server(version="7.2", cache_root=cache_root) + + self.assertEqual(str(server_dir), resolved) + + def test_platform_download_targets_a_known_os(self): + label, extension = _platform_download() + self.assertTrue(label.startswith("RavenDB for ")) + self.assertIn(extension, ("zip", "tar.bz2")) From 035c7bdb462b39a8ecbef3473728f2e598a2b7fa Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Thu, 23 Jul 2026 05:09:22 +0200 Subject: [PATCH 16/23] RavenDB-27069 Consolidate to a single markdown README (stale README.md was shadowing README.rst on GitHub); drop redundant requirements.txt --- MANIFEST.in | 2 +- README.md | 195 ++++++++++++++++++++++++++++++----------------- README.rst | 175 ------------------------------------------ requirements.txt | 2 - setup.py | 3 +- 5 files changed, 126 insertions(+), 251 deletions(-) delete mode 100644 README.rst delete mode 100644 requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index aaf6e1b..6460748 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ graft ravendb_embedded/target/nuget -include README.rst +include README.md recursive-exclude tests * \ No newline at end of file diff --git a/README.md b/README.md index 9f70d56..e901d5c 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,157 @@ +# ravendb-embedded -## Overview -ravendb-embedded is a RavenDB package for running RavenDB in embedded mode. +`ravendb-embedded` runs a real RavenDB server from inside your Python program. You `pip install` +it, start the server in-process, and talk to it with the normal `ravendb` client. There is no +separate server to install, configure, or keep running: the server's lifetime follows your +process. + +Reach for it when you want: + +- **Local development** without setting up a standalone RavenDB. +- **Integration tests** against a real server instead of a mock (see also `ravendb-test-driver`). +- **Small or self-contained apps** that ship the database alongside the code. ```python from ravendb_embedded import EmbeddedServer -EmbeddedServer().start_server() -with EmbeddedServer().get_document_store("Embedded") as store: - with store.open_session() as session: - session.store(User(name="Ilay", age=4)) - session.save_changes() -``` +with EmbeddedServer() as server: + server.start_server() + with server.get_document_store("Embedded") as store: + with store.open_session() as session: + session.store({"name": "Ayende"}, "people/1") + session.save_changes() +``` ## Installation -Install from [PyPi](https://pypi.python.org/pypi), as [ravendb-embedded](https://pypi.python.org/project/ravendb-embedded). + ```bash pip install ravendb-embedded ``` -Install ravendb-embedded from pip will provide you with a copy of RavenDB server binaries files as well. + +The install includes a copy of the RavenDB server binaries. Python 3.10+ is required. + +## The .NET requirement (read this) + +The bundled server is a .NET application, so a matching **.NET runtime** must be on the machine. +The required version tracks the bundled server: + +| `ravendb-embedded` version | Required runtime | +|----------------------------|------------------| +| 7.2.x | .NET 10 | +| 7.1.x | .NET 8 | + +Check what is installed with `dotnet --list-runtimes` (look for `Microsoft.NETCore.App`). Because +the requirement follows the bundled server, it can change on a minor upgrade, so re-check it when +you bump versions. + +If the machine cannot or should not have .NET, use the self-contained path under +[Run without installing .NET](#run-without-installing-net) below. ## Usage -#### Start a server -To start RavenDB server, call `start_server()` method from `EmbeddedServer` instance. -```python -from ravendb_embedded import EmbeddedServer -ravendb_server = EmbeddedServer() -ravendb_server.start_server() -``` -To be more in control about your server `start_server` method can take `server_options`. +The three sections below are the ways people actually use this package. Pick the one that matches +your environment; each links to a runnable walkthrough in `labs/`. + +### Run it (the default, needs .NET) +Start the server and get a document store. This is the zero-config path and uses the system .NET +described above. Pass a `ServerOptions` when you want to control where data lives, the bind URL, +and so on. -#### ServerOptions -* **framework_version** - The framework version to run the server with. -* **data_directory** - Where to save the database data (if None the files will be saved in RavenDB folder in the base folder). -* **server_url** - The url the server will be opened if None the server will open on local host. -* **dotnet_path** - Where dotnet.exe is located if dotnet in the PATH nothing needed here (If .net core is not installed in your machine -you can download [dotnet binaries](https://www.microsoft.com/net/download/windows) and just put the path to it) -* **command_line_args** - A list of all [server command args](https://ravendb.net/docs/article-page/6.0/csharp/server/configuration/command-line-arguments). ```python from ravendb_embedded import EmbeddedServer, ServerOptions -server_options = ServerOptions(data_directory="MYPATH/RavenDBDataDir") -EmbeddedServer().start_server(server_options) +options = ServerOptions() +options.data_directory = "MYPATH/RavenDBDataDir" # optional; defaults to a local RavenDB folder + +with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("MyDb") as store: + ... # ordinary ravendb client code ``` ---- -##### Security -There are options to make ravendb secured in ravendb-embedded:
- -`secured(server_pfx_certificate_path, client_pem_certificate_path, server_pfx_certificate_password=None, ca_certificate_path = None)` -- For this option you will put path to a .pfx and .pem files and a password/ca cert if you have one. -- Server certificate password and CA cert file are optional arguments. Minimal setup requires both .pfx server and .pem client certificates. - ```python - from ravendb_embedded import EmbeddedServer, ServerOptions - - server_options = ServerOptions() - server_options.secured("PATH_TO_SERVER_PFX_CERT_FILE", "PATH_TO_CLIENT_PEM_CERT") - EmbeddedServer.start_server(server_options) - ``` ---- -#### Get Document Store -After initialize and start the server we can use `get_document_store` method to be able to get a DocumentStore -and start work with RavenDB as normal. + +Runnable walkthrough: [`labs/01-embedded-zero-config.md`](labs/01-embedded-zero-config.md). + +### Run without installing .NET + +On locked-down hosts or minimal CI images where you do not want a system .NET, bring a +**self-contained** RavenDB build (it bundles its own runtime). Point the server at the extracted +`Server` folder: the driver detects the bundled runtime and launches the server's native apphost +directly, never calling `dotnet`. ```python -from ravendb_embedded import EmbeddedServer +from ravendb_embedded import EmbeddedServer, ServerOptions -ravendb_server = EmbeddedServer() -ravendb_server.start_server() +options = ServerOptions() +options.with_external_server("/path/to/extracted/Server") # a self-contained build -with ravendb_server.get_document_store("Test") as store: -# Your code here +with EmbeddedServer() as server: + server.start_server(options) + with server.get_document_store("MyDb") as store: + ... ``` ---- -##### DatabaseOptions -* **database_name** - The name of the database -* **skip_creating_database** - `get_document_store` will create a new database if the database is not exists, -if this option if True we won't create the database (Default False). +Download self-contained builds from the RavenDB downloads page (one archive per platform); the +server files live in the archive's `Server/` folder. Runnable walkthrough: +[`labs/02-embedded-external-server.md`](labs/02-embedded-external-server.md). + +Or skip the manual download and let the driver fetch and cache one for you on first use: ```python -# In this example we won't create the Test database if not exists will raise an exception -from ravendb_embedded import EmbeddedServer, DatabaseOptions +options = ServerOptions() +options.with_on_demand_server() # downloads + caches a self-contained server, no .NET needed +``` -ravendb_server = EmbeddedServer() -ravendb_server.start_server() +Walkthrough: [`labs/03-on-demand-server.md`](labs/03-on-demand-server.md). -database_options = DatabaseOptions.from_database_name("Test") -database_options.skip_creating_database = True +### Don't manage a server at all (tests) -with ravendb_server.get_document_store_from_options(database_options) as store: -# Your code here -``` +For test suites that should not touch .NET or embedded startup, `ravendb-test-driver` can attach +to a RavenDB you run yourself (Docker, testcontainers, a shared CI service) while still giving +each test its own database. See the +[`ravendb-python-testdriver`](https://github.com/ravendb/ravendb-python-testdriver) repository. -#### Open RavenDB studio in the browser -To open RavenDB studio from ravendb-embedded you can use `open_studio_in_browser` method and the studio will open automatically -one your default browser. +## Configuration -```python -from ravendb_embedded import EmbeddedServer -ravendb_server = EmbeddedServer() -ravendb_server.start_server() +### `ServerOptions` + +Create `ServerOptions()` and set attributes: + +- `data_directory`: where database data is stored (defaults to a local `RavenDB` folder). Set a + stable path for data that outlives the process, see + [`labs/05-embedded-persistent.md`](labs/05-embedded-persistent.md). +- `server_url`: the URL to bind (defaults to localhost on a free port). +- `dot_net_path`: path to `dotnet` when it is not on `PATH` (ignored on the self-contained path). +- `command_line_args`: extra [server command-line arguments](https://ravendb.net/docs/article-page/latest/csharp/server/configuration/command-line-arguments). +- `framework_version`: pin an exact .NET version (advanced; leave empty to autodetect the installed runtime). -ravendb_server.open_studio_in_browser() +### Security + +Secure the server with `ServerOptions.secured()`: + +```python +options = ServerOptions() +options.secured( + server_pfx_certificate_path, # server certificate (.pfx), required + client_pem_certificate_path, # client certificate (.pem) + server_pfx_certificate_password=None, + ca_certificate_path=None, +) ``` + +Runnable example (HTTPS + client-certificate auth): +[`labs/04-embedded-secured.md`](labs/04-embedded-secured.md). + +### Working with data + +`get_document_store(database_name)` returns a `DocumentStore` you use like any RavenDB client. For +finer control, build a `DatabaseOptions` (via `DatabaseOptions.from_database_name`) and call +`get_document_store_from_options`; set `skip_creating_database=True` to not auto-create the +database. + +Call `open_studio_in_browser()` to open RavenDB Studio in your default browser. + +## Labs + +The `labs/` folder holds runnable, self-checking guides, one per usage case above. Start at +[`labs/README.md`](labs/README.md). diff --git a/README.rst b/README.rst deleted file mode 100644 index 7899b50..0000000 --- a/README.rst +++ /dev/null @@ -1,175 +0,0 @@ -======== -Overview -======== - -``ravendb-embedded`` runs a real RavenDB server from inside your Python program. You -``pip install`` it, start the server in-process, and talk to it with the normal ``ravendb`` -client. There is no separate server to install, configure, or keep running: the server's -lifetime follows your process. - -Reach for it when you want: - -- **Local development** without setting up a standalone RavenDB. -- **Integration tests** against a real server instead of a mock (see also ``ravendb-test-driver``). -- **Small or self-contained apps** that ship the database alongside the code. - -.. code-block:: python - - from ravendb_embedded import EmbeddedServer - - with EmbeddedServer() as server: - server.start_server() - with server.get_document_store("Embedded") as store: - with store.open_session() as session: - session.store({"name": "Ayende"}, "people/1") - session.save_changes() - -============ -Installation -============ - -.. code-block:: bash - - pip install ravendb-embedded - -The install includes a copy of the RavenDB server binaries. Python 3.10+ is required. - -================================ -The .NET requirement (read this) -================================ - -The bundled server is a .NET application, so a matching **.NET runtime** must be on the machine. -The required version tracks the bundled server: - -============================ ================== -``ravendb-embedded`` version Required runtime -============================ ================== -7.2.x .NET 10 -7.1.x .NET 8 -============================ ================== - -Check what is installed with ``dotnet --list-runtimes`` (look for ``Microsoft.NETCore.App``). -Because the requirement follows the bundled server, it can change on a minor upgrade, so -re-check it when you bump versions. - -If the machine cannot or should not have .NET, use the self-contained path under -`Run without installing .NET`_ below. - -===== -Usage -===== - -The three sections below are the ways people actually use this package. Pick the one that -matches your environment; each links to a runnable walkthrough in ``labs/``. - -Run it (the default, needs .NET) --------------------------------- - -Start the server and get a document store. This is the zero-config path and uses the system -.NET described above. Pass a ``ServerOptions`` when you want to control where data lives, the -bind URL, and so on. - -.. code-block:: python - - from ravendb_embedded import EmbeddedServer, ServerOptions - - options = ServerOptions() - options.data_directory = "MYPATH/RavenDBDataDir" # optional; defaults to a local RavenDB folder - - with EmbeddedServer() as server: - server.start_server(options) - with server.get_document_store("MyDb") as store: - ... # ordinary ravendb client code - -Runnable walkthrough: `labs/01-embedded-zero-config.md `_. - -Run without installing .NET ---------------------------- - -On locked-down hosts or minimal CI images where you do not want a system .NET, bring a -**self-contained** RavenDB build (it bundles its own runtime). Point the server at the extracted -``Server`` folder: the driver detects the bundled runtime and launches the server's native -apphost directly, never calling ``dotnet``. - -.. code-block:: python - - from ravendb_embedded import EmbeddedServer, ServerOptions - - options = ServerOptions() - options.with_external_server("/path/to/extracted/Server") # a self-contained build - - with EmbeddedServer() as server: - server.start_server(options) - with server.get_document_store("MyDb") as store: - ... - -Download self-contained builds from the RavenDB downloads page (one archive per platform); the -server files live in the archive's ``Server/`` folder. Runnable walkthrough: -`labs/02-embedded-external-server.md `_. - -Or skip the manual download and let the driver fetch and cache one for you on first use: - -.. code-block:: python - - options = ServerOptions() - options.with_on_demand_server() # downloads + caches a self-contained server, no .NET needed - -Walkthrough: `labs/03-on-demand-server.md `_. - -Don't manage a server at all (tests) -------------------------------------- - -For test suites that should not touch .NET or embedded startup, ``ravendb-test-driver`` can -attach to a RavenDB you run yourself (Docker, testcontainers, a shared CI service) while still -giving each test its own database. See the ``ravendb-python-testdriver`` repository. - -============= -Configuration -============= - -``ServerOptions`` ------------------ - -Create ``ServerOptions()`` and set attributes: - -- ``data_directory``: where database data is stored (defaults to a local ``RavenDB`` folder). Set a - stable path for data that outlives the process, see `labs/05-embedded-persistent.md `_. -- ``server_url``: the URL to bind (defaults to localhost on a free port). -- ``dot_net_path``: path to ``dotnet`` when it is not on ``PATH`` (ignored on the self-contained path). -- ``command_line_args``: extra `server command-line arguments `_. -- ``framework_version``: pin an exact .NET version (advanced; leave empty to autodetect the installed runtime). - -Security --------- - -Secure the server with ``ServerOptions.secured()``: - -.. code-block:: python - - options = ServerOptions() - options.secured( - server_pfx_certificate_path, # server certificate (.pfx), required - client_pem_certificate_path, # client certificate (.pem) - server_pfx_certificate_password=None, - ca_certificate_path=None, - ) - -Runnable example (HTTPS + client-certificate auth): -`labs/04-embedded-secured.md `_. - -Working with data ------------------ - -``get_document_store(database_name)`` returns a ``DocumentStore`` you use like any RavenDB -client. For finer control, build a ``DatabaseOptions`` (via ``DatabaseOptions.from_database_name``) -and call ``get_document_store_from_options``; set ``skip_creating_database=True`` to not -auto-create the database. - -Call ``open_studio_in_browser()`` to open RavenDB Studio in your default browser. - -==== -Labs -==== - -The ``labs/`` folder holds runnable, self-checking guides, one per usage case above. Start at -`labs/README.md `_. diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6f4b66e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -ravendb~=7.2.3 -cryptography>=42.0.0 \ No newline at end of file diff --git a/setup.py b/setup.py index acfc511..0bd42ed 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,8 @@ def run(self): packages=["ravendb_embedded"], package_dir={"ravendb_embedded": "ravendb_embedded"}, include_package_data=True, - long_description=open("README.rst").read(), + long_description=open("README.md").read(), + long_description_content_type="text/markdown", version="7.2.5", description="RavenDB Embedded library to run RavenDB in an embedded way", author="RavenDB", From 19ca7bb68652e6f6954ead9c35360cef3d140f3d Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 28 Jul 2026 02:19:16 +0200 Subject: [PATCH 17/23] RavenDB-27069 Docs: point users to the official downloads page instead of the raw download endpoint --- README.md | 4 ++-- labs/02-embedded-external-server.md | 9 +++------ labs/02_embedded_external_server.py | 6 ++---- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index e901d5c..fd4917e 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ with EmbeddedServer() as server: ... ``` -Download self-contained builds from the RavenDB downloads page (one archive per platform); the -server files live in the archive's `Server/` folder. Runnable walkthrough: +Download the Server package for your platform from the [RavenDB downloads page](https://ravendb.net/downloads); +the server files live in the archive's `Server/` folder. Runnable walkthrough: [`labs/02-embedded-external-server.md`](labs/02-embedded-external-server.md). Or skip the manual download and let the driver fetch and cache one for you on first use: diff --git a/labs/02-embedded-external-server.md b/labs/02-embedded-external-server.md index 192a0f5..184796a 100644 --- a/labs/02-embedded-external-server.md +++ b/labs/02-embedded-external-server.md @@ -6,12 +6,9 @@ never calls `dotnet`. ## Get a self-contained server -Download and extract a server build from ravendb.net (one per platform), for example: - -- Linux x64: `https://hibernatingrhinos.com/downloads/RavenDB%20for%20Linux%20x64/latest?version=7.2` -- Windows x64: `https://hibernatingrhinos.com/downloads/RavenDB%20for%20Windows%20x64/latest?version=7.2` - -The server files live in the `Server/` subfolder of the extracted archive. +Download the Server package for your platform from the RavenDB downloads page: +. You get an archive; extract it, and the server files are in its +`Server/` subfolder. ## Run it diff --git a/labs/02_embedded_external_server.py b/labs/02_embedded_external_server.py index d6eff0c..8242075 100644 --- a/labs/02_embedded_external_server.py +++ b/labs/02_embedded_external_server.py @@ -4,10 +4,8 @@ server build (it bundles the .NET runtime), and the driver runs its native apphost directly, never calling `dotnet`. -Get a self-contained server from ravendb.net downloads, for example: - Linux x64: https://hibernatingrhinos.com/downloads/RavenDB%20for%20Linux%20x64/latest?version=7.2 - Windows x64: https://hibernatingrhinos.com/downloads/RavenDB%20for%20Windows%20x64/latest?version=7.2 -Extract it; the server files live in the `Server/` subfolder. +Get the Server package for your platform from the RavenDB downloads page +(https://ravendb.net/downloads), extract it; the server files live in the `Server/` subfolder. Run: RAVENDB_SELF_CONTAINED_SERVER=/path/to/extracted/Server python labs/02_embedded_external_server.py From af43ee40a04011dff0333cf75cccb7bdaaa0988c Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 28 Jul 2026 02:23:56 +0200 Subject: [PATCH 18/23] RavenDB-27069 Rename with_on_demand_server -> with_auto_downloaded_server (clearer); provider renamed to match --- README.md | 2 +- labs/03-on-demand-server.md | 8 ++++---- labs/03_on_demand_server.py | 4 ++-- ravendb_embedded/__init__.py | 2 +- ravendb_embedded/on_demand.py | 2 +- ravendb_embedded/options.py | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fd4917e..6e0477e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Or skip the manual download and let the driver fetch and cache one for you on fi ```python options = ServerOptions() -options.with_on_demand_server() # downloads + caches a self-contained server, no .NET needed +options.with_auto_downloaded_server() # downloads + caches a self-contained server, no .NET needed ``` Walkthrough: [`labs/03-on-demand-server.md`](labs/03-on-demand-server.md). diff --git a/labs/03-on-demand-server.md b/labs/03-on-demand-server.md index 7feb549..6054476 100644 --- a/labs/03-on-demand-server.md +++ b/labs/03-on-demand-server.md @@ -1,7 +1,7 @@ # Lab 03: On-demand, cached self-contained server (no .NET) **For:** the no-.NET experience of Lab 02 without downloading and extracting a server yourself. -Call `with_on_demand_server()`; on first use the driver fetches a self-contained build for this +Call `with_auto_downloaded_server()`; on first use the driver fetches a self-contained build for this platform, caches it, and reuses the cache from then on. A self-contained build bundles its own runtime, so the server runs its native apphost and never calls `dotnet`. @@ -18,7 +18,7 @@ The complete example is [`03_on_demand_server.py`](03_on_demand_server.py). The from ravendb_embedded import EmbeddedServer, ServerOptions options = ServerOptions() -options.with_on_demand_server() # download + cache a self-contained server on first use +options.with_auto_downloaded_server() # download + cache a self-contained server on first use with EmbeddedServer() as server: server.start_server(options) @@ -26,7 +26,7 @@ with EmbeddedServer() as server: ... # ordinary RavenDB client code, with no .NET on the machine ``` -`with_on_demand_server(version=None, cache_root=None)` defaults the version to the installed +`with_auto_downloaded_server(version=None, cache_root=None)` defaults the version to the installed package's RavenDB line and caches under `~/.cache/ravendb-embedded`. Pass `cache_root` to point it at a directory your CI restores between runs. @@ -47,5 +47,5 @@ provide the server) or Lab 01 (bundled server, needs .NET). ## Takeaway -`with_on_demand_server()` gives the no-.NET path of Lab 02 with zero manual steps: one call, then +`with_auto_downloaded_server()` gives the no-.NET path of Lab 02 with zero manual steps: one call, then ordinary client code. diff --git a/labs/03_on_demand_server.py b/labs/03_on_demand_server.py index a5042bf..19c6874 100644 --- a/labs/03_on_demand_server.py +++ b/labs/03_on_demand_server.py @@ -1,7 +1,7 @@ """Lab 03: On-demand, cached self-contained server (no manual download, no .NET). For: Lab 02 (no system .NET) without downloading and extracting a server yourself. Call -`with_on_demand_server()`; on first use the driver fetches a self-contained build for this +`with_auto_downloaded_server()`; on first use the driver fetches a self-contained build for this platform, caches it, and reuses the cache next time. Because the build is self-contained it runs its native apphost and never calls `dotnet`. @@ -17,7 +17,7 @@ def main() -> None: with tempfile.TemporaryDirectory() as work: options = ServerOptions() - options.with_on_demand_server() # download + cache a self-contained server on first use + options.with_auto_downloaded_server() # download + cache a self-contained server on first use options.target_server_location = str(Path(work, "server")) options.data_directory = str(Path(work, "data")) options.logs_path = str(Path(work, "logs")) diff --git a/ravendb_embedded/__init__.py b/ravendb_embedded/__init__.py index dd51b5f..d44ba44 100644 --- a/ravendb_embedded/__init__.py +++ b/ravendb_embedded/__init__.py @@ -1,6 +1,6 @@ from ravendb_embedded.embedded_server import EmbeddedServer from ravendb_embedded.options import DatabaseOptions, ServerOptions, SecurityOptions -from ravendb_embedded.on_demand import OnDemandServerProvider, ensure_server +from ravendb_embedded.on_demand import AutoDownloadedServerProvider, ensure_server from ravendb_embedded.provide import ( CopyServerFromNugetProvider, CopyServerProvider, diff --git a/ravendb_embedded/on_demand.py b/ravendb_embedded/on_demand.py index cb6ba28..3d3df65 100644 --- a/ravendb_embedded/on_demand.py +++ b/ravendb_embedded/on_demand.py @@ -65,7 +65,7 @@ def ensure_server(version: str = None, cache_root: str = None) -> str: return str(server.parent) -class OnDemandServerProvider(ProvideRavenDBServer): +class AutoDownloadedServerProvider(ProvideRavenDBServer): """Download (once) and cache a self-contained server, then run it with no system .NET. The self-contained build bundles its own runtime, so the server runs via its native apphost; diff --git a/ravendb_embedded/options.py b/ravendb_embedded/options.py index d567954..409dbab 100644 --- a/ravendb_embedded/options.py +++ b/ravendb_embedded/options.py @@ -14,7 +14,7 @@ ExternalServerProvider, CopyServerFromNugetProvider, ) -from ravendb_embedded.on_demand import OnDemandServerProvider +from ravendb_embedded.on_demand import AutoDownloadedServerProvider class DatabaseOptions: @@ -98,6 +98,6 @@ def secured( def with_external_server(self, server_location: str) -> None: self.provider = ExternalServerProvider(server_location) - def with_on_demand_server(self, version: str = None, cache_root: str = None) -> None: + def with_auto_downloaded_server(self, version: str = None, cache_root: str = None) -> None: # Download (once) and cache a self-contained server, then run it with no system .NET. - self.provider = OnDemandServerProvider(version, cache_root) + self.provider = AutoDownloadedServerProvider(version, cache_root) From 3d0c7aad8a48fc6df33877ebeaaff5460d322026 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 28 Jul 2026 02:53:22 +0200 Subject: [PATCH 19/23] RavenDB-27069 Trim remaining redundant inline comments to critical one-liners --- labs/04_embedded_secured.py | 2 +- labs/05_embedded_persistent.py | 2 +- ravendb_embedded/provide.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/labs/04_embedded_secured.py b/labs/04_embedded_secured.py index cb835d7..4e64295 100644 --- a/labs/04_embedded_secured.py +++ b/labs/04_embedded_secured.py @@ -90,7 +90,7 @@ def main() -> None: server_pfx, client_pem, ca_crt = _demo_certificates(work) options = ServerOptions() - options.secured(server_pfx, client_pem, ca_certificate_path=ca_crt) # HTTPS + client-cert auth + options.secured(server_pfx, client_pem, ca_certificate_path=ca_crt) options.data_directory = str(Path(work, "RavenDB")) options.logs_path = str(Path(work, "Logs")) diff --git a/labs/05_embedded_persistent.py b/labs/05_embedded_persistent.py index a29c5b7..18de0d0 100644 --- a/labs/05_embedded_persistent.py +++ b/labs/05_embedded_persistent.py @@ -16,7 +16,7 @@ def _options(data_directory, logs_path): options = ServerOptions() - options.data_directory = data_directory # the fixed folder we reuse across restarts + options.data_directory = data_directory options.logs_path = logs_path return options diff --git a/ravendb_embedded/provide.py b/ravendb_embedded/provide.py index c19aacd..b199284 100644 --- a/ravendb_embedded/provide.py +++ b/ravendb_embedded/provide.py @@ -84,7 +84,6 @@ def __init__(self, server_location: str): if not os.path.exists(file_server_location): raise ValueError(f"Server location doesn't exist: {server_location}") - # Check if target is a file - assuming it is a zip file if os.path.isfile(file_server_location): self.inner_provider = ExtractFromZipServerProvider(server_location) return From 53c499547407d0e4dd672411e37a9ace56b1bcac Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 28 Jul 2026 02:53:22 +0200 Subject: [PATCH 20/23] RavenDB-27069 Run a ready external/self-contained server in place; fixes naive usage that needed a manual target_server_location (README examples were broken); drop now-redundant provider --- labs/02_embedded_external_server.py | 1 - labs/03_on_demand_server.py | 3 +-- ravendb_embedded/__init__.py | 2 +- ravendb_embedded/on_demand.py | 18 ------------------ ravendb_embedded/options.py | 8 ++++++-- 5 files changed, 8 insertions(+), 24 deletions(-) diff --git a/labs/02_embedded_external_server.py b/labs/02_embedded_external_server.py index 8242075..a004c02 100644 --- a/labs/02_embedded_external_server.py +++ b/labs/02_embedded_external_server.py @@ -26,7 +26,6 @@ def main() -> None: with tempfile.TemporaryDirectory() as work: options = ServerOptions() - options.target_server_location = str(Path(work, "server")) options.data_directory = str(Path(work, "data")) options.logs_path = str(Path(work, "logs")) # Bogus on purpose: a self-contained build must never call `dotnet`, so if it still boots the no-.NET path works. diff --git a/labs/03_on_demand_server.py b/labs/03_on_demand_server.py index 19c6874..a1811d4 100644 --- a/labs/03_on_demand_server.py +++ b/labs/03_on_demand_server.py @@ -17,8 +17,7 @@ def main() -> None: with tempfile.TemporaryDirectory() as work: options = ServerOptions() - options.with_auto_downloaded_server() # download + cache a self-contained server on first use - options.target_server_location = str(Path(work, "server")) + options.with_auto_downloaded_server() options.data_directory = str(Path(work, "data")) options.logs_path = str(Path(work, "logs")) diff --git a/ravendb_embedded/__init__.py b/ravendb_embedded/__init__.py index d44ba44..faac0e1 100644 --- a/ravendb_embedded/__init__.py +++ b/ravendb_embedded/__init__.py @@ -1,6 +1,6 @@ from ravendb_embedded.embedded_server import EmbeddedServer from ravendb_embedded.options import DatabaseOptions, ServerOptions, SecurityOptions -from ravendb_embedded.on_demand import AutoDownloadedServerProvider, ensure_server +from ravendb_embedded.on_demand import ensure_server from ravendb_embedded.provide import ( CopyServerFromNugetProvider, CopyServerProvider, diff --git a/ravendb_embedded/on_demand.py b/ravendb_embedded/on_demand.py index 3d3df65..86c9aac 100644 --- a/ravendb_embedded/on_demand.py +++ b/ravendb_embedded/on_demand.py @@ -4,8 +4,6 @@ import zipfile from pathlib import Path -from ravendb_embedded.provide import CopyServerProvider, ProvideRavenDBServer - _DOWNLOAD_BASE = "https://hibernatingrhinos.com/downloads" @@ -63,19 +61,3 @@ def ensure_server(version: str = None, cache_root: str = None) -> str: if not server: raise RuntimeError(f"Server binaries not found under {target}") return str(server.parent) - - -class AutoDownloadedServerProvider(ProvideRavenDBServer): - """Download (once) and cache a self-contained server, then run it with no system .NET. - - The self-contained build bundles its own runtime, so the server runs via its native apphost; - `dot_net_path` is never used. The download happens on first `start_server()` and is cached. - """ - - def __init__(self, version: str = None, cache_root: str = None): - self.version = version - self.cache_root = cache_root - self.is_single_file_app = True - - def provide(self, target_directory: str) -> None: - CopyServerProvider(ensure_server(self.version, self.cache_root)).provide(target_directory) diff --git a/ravendb_embedded/options.py b/ravendb_embedded/options.py index 409dbab..dbf0769 100644 --- a/ravendb_embedded/options.py +++ b/ravendb_embedded/options.py @@ -14,7 +14,7 @@ ExternalServerProvider, CopyServerFromNugetProvider, ) -from ravendb_embedded.on_demand import AutoDownloadedServerProvider +from ravendb_embedded.on_demand import ensure_server class DatabaseOptions: @@ -97,7 +97,11 @@ def secured( def with_external_server(self, server_location: str) -> None: self.provider = ExternalServerProvider(server_location) + # A directory is already a runnable server: run it in place, so we neither copy it nor + # collide with the bundled server sitting at the default target location. + if os.path.isdir(server_location): + self.target_server_location = server_location def with_auto_downloaded_server(self, version: str = None, cache_root: str = None) -> None: # Download (once) and cache a self-contained server, then run it with no system .NET. - self.provider = AutoDownloadedServerProvider(version, cache_root) + self.with_external_server(ensure_server(version, cache_root)) From 01260767e99214f40fc66c5d306d9630e02b5651 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 28 Jul 2026 03:14:37 +0200 Subject: [PATCH 21/23] RavenDB-27069 Harden on-demand download: reject archive path-traversal, atomic cache, download timeout + error wrap (review) --- ravendb_embedded/on_demand.py | 74 ++++++++++++++++++++++++++++------- tests/test_on_demand.py | 30 +++++++++++++- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/ravendb_embedded/on_demand.py b/ravendb_embedded/on_demand.py index 86c9aac..0b89f19 100644 --- a/ravendb_embedded/on_demand.py +++ b/ravendb_embedded/on_demand.py @@ -1,10 +1,14 @@ +import os import platform +import shutil import tarfile +import tempfile import urllib.request import zipfile from pathlib import Path _DOWNLOAD_BASE = "https://hibernatingrhinos.com/downloads" +_DOWNLOAD_TIMEOUT_SECONDS = 30 def _default_version_line() -> str: @@ -28,34 +32,76 @@ def _platform_download() -> tuple: return f"RavenDB for Linux {arch}", "tar.bz2" +def _extract_safely(archive: Path, dest: Path, extension: str) -> None: + # Guard against archive path-traversal (zip/tar slip): every member must resolve inside dest, + # and tar links are refused. (The stdlib default extraction is unfiltered before Python 3.12.) + dest = dest.resolve() + + def _inside(name: str) -> bool: + return (dest / name).resolve() == dest or dest in (dest / name).resolve().parents + + if extension == "zip": + with zipfile.ZipFile(archive) as bundle: + for name in bundle.namelist(): + if not _inside(name): + raise RuntimeError(f"Refusing to extract unsafe archive path: {name}") + bundle.extractall(dest) + else: + with tarfile.open(archive) as bundle: + for member in bundle.getmembers(): + if member.issym() or member.islnk(): + raise RuntimeError(f"Refusing to extract a link member from archive: {member.name}") + if not _inside(member.name): + raise RuntimeError(f"Refusing to extract unsafe archive path: {member.name}") + bundle.extractall(dest) + + def ensure_server(version: str = None, cache_root: str = None) -> str: """Return a local self-contained Server directory, downloading and caching on first use. The download is a self-contained build (bundles its own .NET), so it runs with no system .NET. Pulling `latest` for the version line is intentional: a self-contained build never has - to match anything on the host. The cache is keyed on version + platform, so later runs reuse - it and never re-download. + to match anything on the host. A completed cache entry (keyed on version + platform) is reused + and never re-downloaded. Download and extraction happen in a private temp directory that is + moved into place only once complete, so an interrupted or concurrent first run never leaves a + half-populated cache. """ version = version or _default_version_line() - cache_root = Path(cache_root) if cache_root else Path.home() / ".cache" / "ravendb-embedded" + root = Path(cache_root) if cache_root else Path.home() / ".cache" / "ravendb-embedded" label, extension = _platform_download() - target = cache_root / version / label.replace(" ", "_") + line_dir = root / version + target = line_dir / label.replace(" ", "_") cached = next(target.rglob("Raven.Server.dll"), None) if target.is_dir() else None if cached: return str(cached.parent) - target.mkdir(parents=True, exist_ok=True) + line_dir.mkdir(parents=True, exist_ok=True) url = f"{_DOWNLOAD_BASE}/{label.replace(' ', '%20')}/latest?version={version}" - archive = target / f"ravendb.{extension}" - urllib.request.urlretrieve(url, archive) - if extension == "zip": - with zipfile.ZipFile(archive) as bundle: - bundle.extractall(target) - else: - with tarfile.open(archive) as bundle: - bundle.extractall(target) - archive.unlink() + work = Path(tempfile.mkdtemp(prefix="download-", dir=line_dir)) + try: + archive = work / f"ravendb.{extension}" + try: + with urllib.request.urlopen(url, timeout=_DOWNLOAD_TIMEOUT_SECONDS) as response, open(archive, "wb") as out: + shutil.copyfileobj(response, out) + except OSError as error: + raise RuntimeError(f"Failed to download a self-contained RavenDB server from {url}: {error}") from error + + extracted = work / "server" + extracted.mkdir() + _extract_safely(archive, extracted, extension) + if not next(extracted.rglob("Raven.Server.dll"), None): + raise RuntimeError(f"Downloaded archive did not contain a RavenDB server: {url}") + + try: + os.replace(extracted, target) # atomic on the same filesystem + except OSError: + # A concurrent first run may have populated the cache first; prefer a complete entry. + if not next(target.rglob("Raven.Server.dll"), None): + shutil.rmtree(target, ignore_errors=True) + os.replace(extracted, target) + finally: + shutil.rmtree(work, ignore_errors=True) server = next(target.rglob("Raven.Server.dll"), None) if not server: diff --git a/tests/test_on_demand.py b/tests/test_on_demand.py index 8b7dd7e..b3d0f23 100644 --- a/tests/test_on_demand.py +++ b/tests/test_on_demand.py @@ -1,9 +1,12 @@ +import io +import tarfile import tempfile import unittest +import zipfile from pathlib import Path from unittest import mock -from ravendb_embedded.on_demand import ensure_server, _platform_download +from ravendb_embedded.on_demand import _extract_safely, _platform_download, ensure_server class TestOnDemand(unittest.TestCase): @@ -15,7 +18,7 @@ def test_cache_hit_skips_download(self): server_dir.mkdir(parents=True) (server_dir / "Raven.Server.dll").write_bytes(b"stub") - with mock.patch("urllib.request.urlretrieve", side_effect=AssertionError("cache hit must not download")): + with mock.patch("urllib.request.urlopen", side_effect=AssertionError("cache hit must not download")): resolved = ensure_server(version="7.2", cache_root=cache_root) self.assertEqual(str(server_dir), resolved) @@ -24,3 +27,26 @@ def test_platform_download_targets_a_known_os(self): label, extension = _platform_download() self.assertTrue(label.startswith("RavenDB for ")) self.assertIn(extension, ("zip", "tar.bz2")) + + def test_extract_rejects_path_traversal(self): + # A tampered archive must not be able to write outside the destination (zip/tar slip). + with tempfile.TemporaryDirectory() as work: + dest = Path(work, "out") + dest.mkdir() + + bad_tar = Path(work, "bad.tar.bz2") + with tarfile.open(bad_tar, "w:bz2") as tar: + payload = b"x" + info = tarfile.TarInfo("../escaped.txt") + info.size = len(payload) + tar.addfile(info, io.BytesIO(payload)) + with self.assertRaises(RuntimeError): + _extract_safely(bad_tar, dest, "tar.bz2") + + bad_zip = Path(work, "bad.zip") + with zipfile.ZipFile(bad_zip, "w") as archive: + archive.writestr("../escaped.txt", "x") + with self.assertRaises(RuntimeError): + _extract_safely(bad_zip, dest, "zip") + + self.assertFalse((Path(work) / "escaped.txt").exists()) From 585ac50ca7002ab5ebfab789b1ae20e0268b2624 Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 28 Jul 2026 03:14:37 +0200 Subject: [PATCH 22/23] RavenDB-27069 check_dotnet_requirement: clean message + int exit when no dotnet on PATH (review) --- scripts/check_dotnet_requirement.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/check_dotnet_requirement.py b/scripts/check_dotnet_requirement.py index c59eefe..c6dfb5a 100644 --- a/scripts/check_dotnet_requirement.py +++ b/scripts/check_dotnet_requirement.py @@ -30,19 +30,27 @@ def required_dotnet_major(runtime_config: Path) -> str: return options.get("tfm", "").removeprefix("net").split(".")[0] # e.g. "net10.0" -> "10" -def main() -> object: +def main() -> int: if not RUNTIME_CONFIG.exists(): - return f"Server runtimeconfig not found at {RUNTIME_CONFIG}; fetch the server first (python setup.py sdist)." + print(f"Server runtimeconfig not found at {RUNTIME_CONFIG}; fetch the server first (python setup.py sdist).") + return 1 major = required_dotnet_major(RUNTIME_CONFIG) if not major: - return f"Could not determine the required .NET version from {RUNTIME_CONFIG}." + print(f"Could not determine the required .NET version from {RUNTIME_CONFIG}.") + return 1 + + try: + installed = subprocess.run(["dotnet", "--list-runtimes"], capture_output=True, text=True).stdout + except FileNotFoundError: + print(f"No 'dotnet' found on PATH; the bundled server requires .NET major {major}.") + return 1 - installed = subprocess.run(["dotnet", "--list-runtimes"], capture_output=True, text=True).stdout matched = f"Microsoft.NETCore.App {major}." in installed print(f"Bundled server requires .NET major {major}; matching runtime installed: {matched}") if not matched: - return f"Installed .NET runtimes do not include major {major}, which the bundled server requires." + print(f"Installed .NET runtimes do not include major {major}, which the bundled server requires.") + return 1 return 0 From 0e43533d38515a347859e7b8dc9a442099b7ccbf Mon Sep 17 00:00:00 2001 From: Gracjan Sadowicz Date: Tue, 28 Jul 2026 03:14:37 +0200 Subject: [PATCH 23/23] RavenDB-27069 Run a ready server in place explicitly; never clear the user's own server dir (review) --- ravendb_embedded/options.py | 1 + ravendb_embedded/provide.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/ravendb_embedded/options.py b/ravendb_embedded/options.py index dbf0769..9cf4d5b 100644 --- a/ravendb_embedded/options.py +++ b/ravendb_embedded/options.py @@ -101,6 +101,7 @@ def with_external_server(self, server_location: str) -> None: # collide with the bundled server sitting at the default target location. if os.path.isdir(server_location): self.target_server_location = server_location + self.clear_target_server_location = False # never wipe the user's own server directory def with_auto_downloaded_server(self, version: str = None, cache_root: str = None) -> None: # Download (once) and cache a self-contained server, then run it with no system .NET. diff --git a/ravendb_embedded/provide.py b/ravendb_embedded/provide.py index b199284..ecffbb3 100644 --- a/ravendb_embedded/provide.py +++ b/ravendb_embedded/provide.py @@ -20,6 +20,8 @@ def __init__(self, server_files: str): self.server_files = server_files def provide(self, target_directory: str) -> None: + if os.path.abspath(self.server_files) == os.path.abspath(target_directory): + return # already in place: run the server where it is, nothing to copy try: shutil.copytree(self.server_files, target_directory) except FileExistsError: