diff --git a/Containerfile b/Containerfile
index b8458f9..8cc7fbc 100644
--- a/Containerfile
+++ b/Containerfile
@@ -1,16 +1,16 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
-# peo: neo4j-browser on top of PostgreSQL + Apache AGE.
+# boltage: neo4j-browser on top of PostgreSQL + Apache AGE.
#
# Two images can be built from this file:
#
# full (the default): the Apache AGE image (PostgreSQL with the AGE extension)
-# plus peo, neo4j-browser and pgAdmin, all in one container.
-# slim (--target slim): only peo and neo4j-browser, for connecting to a
+# plus boltage, neo4j-browser and pgAdmin, all in one container.
+# slim (--target slim): only boltage and neo4j-browser, for connecting to a
# separate PostgreSQL server with AGE; see compose.yaml.
#
-# peo is the Python package that speaks the Bolt protocol to the browser and
+# boltage is the Python package that speaks the Bolt protocol to the browser and
# translates Cypher into AGE `cypher()` calls. The browser stage builds the
# neo4j-browser static frontend from source for both images.
@@ -35,7 +35,7 @@ RUN yarn install --frozen-lockfile --network-timeout 600000
RUN yarn build && test -f dist/index.html
-# Builds peo's sdist from the git repository, because verri derives the version
+# Builds boltage's sdist from the git repository, because verri derives the version
# from the git history (see version.py). The unpacked sdist carries that version
# in PKG-INFO, so the images below can be built without git.
FROM ${SLIM_BASE_IMAGE} AS sdist
@@ -46,8 +46,8 @@ RUN apt-get update \
COPY . /src
RUN python3 -m build --sdist --outdir /dist /src \
- && mkdir /peo \
- && tar -xzf /dist/peo-*.tar.gz --strip-components=1 -C /peo
+ && mkdir /boltage \
+ && tar -xzf /dist/boltage-*.tar.gz --strip-components=1 -C /boltage
FROM ${SLIM_BASE_IMAGE} AS slim
@@ -56,32 +56,32 @@ RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 python3-psycopg python3-venv \
&& rm -rf /var/lib/apt/lists/*
-# peo itself, using Debian's psycopg and the other dependencies from PyPI. License
+# boltage itself, using Debian's psycopg and the other dependencies from PyPI. License
# notices: see THIRD_PARTY_NOTICES.md.
COPY container/collect-python-licenses.py /tmp/
COPY container/licenses/ /tmp/licenses/
-COPY --from=sdist /peo /tmp/peo
-RUN python3 -m venv --system-site-packages /opt/peo/venv \
- && /opt/peo/venv/bin/pip install --no-cache-dir "/tmp/peo[browser]" \
- && /opt/peo/venv/bin/python /tmp/collect-python-licenses.py \
- /usr/share/doc/peo/peo-python-licenses /tmp/licenses \
- && rm -r /tmp/peo /tmp/collect-python-licenses.py /tmp/licenses
-COPY LICENSE THIRD_PARTY_NOTICES.md /usr/share/doc/peo/
+COPY --from=sdist /boltage /tmp/boltage
+RUN python3 -m venv --system-site-packages /opt/boltage/venv \
+ && /opt/boltage/venv/bin/pip install --no-cache-dir "/tmp/boltage[browser]" \
+ && /opt/boltage/venv/bin/python /tmp/collect-python-licenses.py \
+ /usr/share/doc/boltage/boltage-python-licenses /tmp/licenses \
+ && rm -r /tmp/boltage /tmp/collect-python-licenses.py /tmp/licenses
+COPY LICENSE THIRD_PARTY_NOTICES.md /usr/share/doc/boltage/
COPY --from=browser /src/LICENSE /src/NOTICE.txt /src/LICENSES.txt /neo4j-browser-source.tar.gz \
- /usr/share/doc/peo/neo4j-browser/
-COPY --from=browser /src/dist /opt/peo/browser
+ /usr/share/doc/boltage/neo4j-browser/
+COPY --from=browser /src/dist /opt/boltage/browser
-RUN useradd --system --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin peo
-USER peo
+RUN useradd --system --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin boltage
+USER boltage
-ENV PEO_BROWSER_DIR=/opt/peo/browser \
- PEO_HTTP_PORT=7474 \
- PEO_BOLT_PORT=7687
+ENV BOLTAGE_BROWSER_DIR=/opt/boltage/browser \
+ BOLTAGE_HTTP_PORT=7474 \
+ BOLTAGE_BOLT_PORT=7687
# 7474: neo4j-browser (HTTP), 7687: Bolt
EXPOSE 7474 7687
-CMD ["/opt/peo/venv/bin/peo"]
+CMD ["/opt/boltage/venv/bin/boltage"]
FROM ${AGE_IMAGE} AS full
@@ -105,39 +105,39 @@ RUN python3 -m venv /opt/pgadmin \
COPY container/collect-python-licenses.py /tmp/
COPY container/licenses/ /tmp/licenses/
RUN /opt/pgadmin/bin/python /tmp/collect-python-licenses.py \
- /usr/share/doc/peo/pgadmin-python-licenses /tmp/licenses
+ /usr/share/doc/boltage/pgadmin-python-licenses /tmp/licenses
-# peo itself, using Debian's psycopg and the other dependencies from PyPI.
-COPY --from=sdist /peo /tmp/peo
-RUN python3 -m venv --system-site-packages /opt/peo/venv \
- && /opt/peo/venv/bin/pip install --no-cache-dir "/tmp/peo[browser]" \
- && /opt/peo/venv/bin/python /tmp/collect-python-licenses.py \
- /usr/share/doc/peo/peo-python-licenses /tmp/licenses \
- && rm -r /tmp/peo /tmp/collect-python-licenses.py /tmp/licenses
-COPY LICENSE THIRD_PARTY_NOTICES.md /usr/share/doc/peo/
+# boltage itself, using Debian's psycopg and the other dependencies from PyPI.
+COPY --from=sdist /boltage /tmp/boltage
+RUN python3 -m venv --system-site-packages /opt/boltage/venv \
+ && /opt/boltage/venv/bin/pip install --no-cache-dir "/tmp/boltage[browser]" \
+ && /opt/boltage/venv/bin/python /tmp/collect-python-licenses.py \
+ /usr/share/doc/boltage/boltage-python-licenses /tmp/licenses \
+ && rm -r /tmp/boltage /tmp/collect-python-licenses.py /tmp/licenses
+COPY LICENSE THIRD_PARTY_NOTICES.md /usr/share/doc/boltage/
COPY --from=browser /src/LICENSE /src/NOTICE.txt /src/LICENSES.txt /neo4j-browser-source.tar.gz \
- /usr/share/doc/peo/neo4j-browser/
+ /usr/share/doc/boltage/neo4j-browser/
ADD https://raw.githubusercontent.com/apache/age/${AGE_SOURCE_REF}/LICENSE \
https://raw.githubusercontent.com/apache/age/${AGE_SOURCE_REF}/NOTICE \
- /usr/share/doc/peo/apache-age/
+ /usr/share/doc/boltage/apache-age/
-COPY --from=browser /src/dist /opt/peo/browser
+COPY --from=browser /src/dist /opt/boltage/browser
COPY container/initdb/ /docker-entrypoint-initdb.d/
-COPY container/pgadmin/config_distro.py /opt/peo/pgadmin/config_distro.py
-COPY container/peo-entrypoint.sh /usr/local/bin/peo-entrypoint.sh
+COPY container/pgadmin/config_distro.py /opt/boltage/pgadmin/config_distro.py
+COPY container/boltage-entrypoint.sh /usr/local/bin/boltage-entrypoint.sh
-ENV PEO_DEFAULT_GRAPH=neo4j \
- PEO_BROWSER_DIR=/opt/peo/browser \
- PEO_HTTP_PORT=7474 \
- PEO_BOLT_PORT=7687 \
- PEO_PG_HOST=127.0.0.1 \
- PEO_PG_PORT=5432 \
- PEO_PGADMIN=true \
- PEO_PGADMIN_PORT=5050 \
- CONFIG_DISTRO_FILE_PATH=/opt/peo/pgadmin/config_distro.py
+ENV BOLTAGE_DEFAULT_GRAPH=neo4j \
+ BOLTAGE_BROWSER_DIR=/opt/boltage/browser \
+ BOLTAGE_HTTP_PORT=7474 \
+ BOLTAGE_BOLT_PORT=7687 \
+ BOLTAGE_PG_HOST=127.0.0.1 \
+ BOLTAGE_PG_PORT=5432 \
+ BOLTAGE_PGADMIN=true \
+ BOLTAGE_PGADMIN_PORT=5050 \
+ CONFIG_DISTRO_FILE_PATH=/opt/boltage/pgadmin/config_distro.py
# 7474: neo4j-browser (HTTP), 7687: Bolt, 5050: pgAdmin, 5432: PostgreSQL
EXPOSE 7474 7687 5050 5432
-ENTRYPOINT ["/usr/local/bin/peo-entrypoint.sh"]
+ENTRYPOINT ["/usr/local/bin/boltage-entrypoint.sh"]
CMD ["postgres", "-c", "shared_preload_libraries=age"]
diff --git a/README.md b/README.md
index c31188b..6e395e6 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,16 @@
-# peo
+# boltage
[neo4j-browser](https://github.com/neo4j/neo4j-browser), cypher-shell and the Neo4j
drivers on top of PostgreSQL, using [Apache AGE](https://github.com/apache/age) as the
graph engine.
-peo is a small Python server that speaks Neo4j's Bolt protocol and translates Cypher
+boltage is a small Python server that speaks Neo4j's Bolt protocol and translates Cypher
into AGE `cypher()` calls. It can also serve neo4j-browser. A [container
image](#container-images) bundles everything, together with
[pgAdmin](https://www.pgadmin.org/) for working with the same data in SQL.
```
- neo4j-browser (static files) peo (Python) PostgreSQL + AGE
+ neo4j-browser (static files) boltage (Python) PostgreSQL + AGE
┌──────────────────────┐ Bolt ┌────────────────────────-──┐ SQL ┌────────────────┐
│ in your web browser │───────▶│ Bolt server (TCP + WS) │──────▶│ cypher('g',$$ │
│ served on :7474 │ :7687 │ Cypher → AGE translation │ :5432 │ ... $$) │
@@ -20,63 +20,63 @@ image](#container-images) bundles everything, together with
## Installation
-peo needs Python 3.11 or later and a PostgreSQL server with the AGE extension installed
+boltage needs Python 3.11 or later and a PostgreSQL server with the AGE extension installed
and loaded (`shared_preload_libraries=age`).
```sh
-pip install 'peo[binary]'
-PEO_PG_HOST=localhost PEO_PG_DATABASE=postgres peo
+pip install 'boltage[binary]'
+BOLTAGE_PG_HOST=localhost BOLTAGE_PG_DATABASE=postgres boltage
```
The `binary` extra pulls in psycopg's prebuilt wheels; leave it out to use a psycopg
-built against your system's libpq. `python -m peo` works the same as `peo`.
+built against your system's libpq. `python -m boltage` works the same as `boltage`.
Neo4j clients can now connect to `neo4j://localhost:7687` with a PostgreSQL user name
and password.
-peo does not include neo4j-browser itself. To serve it, install the `browser` extra
+boltage does not include neo4j-browser itself. To serve it, install the `browser` extra
([Starlette](https://www.starlette.io/) and [uvicorn](https://www.uvicorn.org/)),
-build neo4j-browser and point `PEO_BROWSER_DIR` at its `dist` directory; it is then
+build neo4j-browser and point `BOLTAGE_BROWSER_DIR` at its `dist` directory; it is then
served on . The [container images](#container-images) do all of
this for you.
```sh
-pip install 'peo[binary,browser]'
-PEO_PG_HOST=localhost PEO_PG_DATABASE=postgres PEO_BROWSER_DIR=/path/to/neo4j-browser/dist peo
+pip install 'boltage[binary,browser]'
+BOLTAGE_PG_HOST=localhost BOLTAGE_PG_DATABASE=postgres BOLTAGE_BROWSER_DIR=/path/to/neo4j-browser/dist boltage
```
## Configuration
-peo is configured through environment variables:
+boltage is configured through environment variables:
| Variable | Default | Purpose |
| --- | --- | --- |
-| `PEO_PG_HOST` | `127.0.0.1` | PostgreSQL host. |
-| `PEO_PG_PORT` | `5432` | PostgreSQL port. |
-| `PEO_PG_DATABASE` | `$POSTGRES_DB`, else `$POSTGRES_USER`, else `postgres` | PostgreSQL database holding the graphs. |
-| `PEO_DEFAULT_GRAPH` | `neo4j` | Graph used when a client does not pick a database. |
-| `PEO_BOLT_PORT` | `7687` | Port the Bolt server listens on (TCP and WebSocket). |
-| `PEO_HTTP_PORT` | `7474` | Port neo4j-browser is served on, when `PEO_BROWSER_DIR` is set. |
-| `PEO_BROWSER_DIR` | (unset) | neo4j-browser `dist` directory to serve; needs the `browser` extra. |
-| `PEO_ADVERTISED_BOLT_PORT` | `$PEO_BOLT_PORT` | Bolt port advertised to the browser; set it when clients reach the Bolt port on another port. |
-| `PEO_ADVERTISED_HOST` | `localhost` | Host reported in `SHOW DATABASES` and to clients that do not send a routing address. |
-| `PEO_LOG_LEVEL` | `INFO` | `DEBUG` logs every generated SQL statement. |
+| `BOLTAGE_PG_HOST` | `127.0.0.1` | PostgreSQL host. |
+| `BOLTAGE_PG_PORT` | `5432` | PostgreSQL port. |
+| `BOLTAGE_PG_DATABASE` | `$POSTGRES_DB`, else `$POSTGRES_USER`, else `postgres` | PostgreSQL database holding the graphs. |
+| `BOLTAGE_DEFAULT_GRAPH` | `neo4j` | Graph used when a client does not pick a database. |
+| `BOLTAGE_BOLT_PORT` | `7687` | Port the Bolt server listens on (TCP and WebSocket). |
+| `BOLTAGE_HTTP_PORT` | `7474` | Port neo4j-browser is served on, when `BOLTAGE_BROWSER_DIR` is set. |
+| `BOLTAGE_BROWSER_DIR` | (unset) | neo4j-browser `dist` directory to serve; needs the `browser` extra. |
+| `BOLTAGE_ADVERTISED_BOLT_PORT` | `$BOLTAGE_BOLT_PORT` | Bolt port advertised to the browser; set it when clients reach the Bolt port on another port. |
+| `BOLTAGE_ADVERTISED_HOST` | `localhost` | Host reported in `SHOW DATABASES` and to clients that do not send a routing address. |
+| `BOLTAGE_LOG_LEVEL` | `INFO` | `DEBUG` logs every generated SQL statement. |
## How it works
- **Databases are AGE graphs.** Every AGE graph shows up as a Neo4j database, and
- `:use ` switches between them. Clients that do not pick a database get `PEO_DEFAULT_GRAPH`
+ `:use ` switches between them. Clients that do not pick a database get `BOLTAGE_DEFAULT_GRAPH`
(`neo4j`); the full container image and the compose setup create that graph on first start. `CREATE DATABASE x` and `DROP DATABASE x` on the `system`
database call `create_graph` / `drop_graph`; AGE requires names of at least 3 characters.
- **Queries** are sent to AGE as `SELECT * FROM cypher('', $$ $$) AS (...)`.
- peo works out the result columns from the final `RETURN` clause and inlines
+ boltage works out the result columns from the final `RETURN` clause and inlines
query parameters as Cypher literals.
- **Neo4j procedures** that neo4j-browser relies on (`dbms.components`, `SHOW DATABASES`,
`db.labels`, `db.schema.visualization`, `SHOW PROCEDURES`, ...) are emulated from the
AGE catalog.
- **Workarounds for AGE**: neo4j-browser's "expand node" query uses a pattern
comprehension, and its relationship lookup uses `id(n) IN [...]`, which AGE rejects
- for lists of more than one element. peo rewrites both into equivalent Cypher
+ for lists of more than one element. boltage rewrites both into equivalent Cypher
that AGE accepts.
- **Authentication** is passed straight through: every Bolt connection opens its own
PostgreSQL connection with the credentials you log in with.
@@ -283,7 +283,7 @@ things to keep in mind:
- This only works in one direction. AGE writes label tables without firing triggers, so
a Cypher `CREATE`, `SET` or `DELETE` on a mirrored label does not reach the tables,
and the next change to the row overwrites it. AGE does check table privileges, so
- grant the users that log in through peo only `SELECT` on the mirrored label tables.
+ grant the users that log in through boltage only `SELECT` on the mirrored label tables.
The triggers are `SECURITY DEFINER`, so relational writes still reach the graph.
Superusers bypass privileges.
- Vertex ids need a `bigint` primary key below 248. Other keys, such as
@@ -295,23 +295,23 @@ things to keep in mind:
The [`Containerfile`](Containerfile) builds two images:
-- **full** (the default): PostgreSQL with AGE, peo, neo4j-browser and pgAdmin in one
+- **full** (the default): PostgreSQL with AGE, boltage, neo4j-browser and pgAdmin in one
container.
-- **slim** (`--target slim`): only peo and neo4j-browser, for a PostgreSQL server
+- **slim** (`--target slim`): only boltage and neo4j-browser, for a PostgreSQL server
with AGE that runs elsewhere. [`compose.yaml`](compose.yaml) runs it next to
separate PostgreSQL and pgAdmin containers.
Any OCI-compatible build tool works. Docker does not pick up a `Containerfile` by
-itself, so pass it explicitly: `docker build -f Containerfile -t peo .`. `docker`
+itself, so pass it explicitly: `docker build -f Containerfile -t boltage .`. `docker`
takes the same options as `podman` in the commands below.
### Everything in one container
```sh
-podman build -t peo .
-podman run -d --name peo -e POSTGRES_PASSWORD=secret \
+podman build -t boltage .
+podman run -d --name boltage -e POSTGRES_PASSWORD=secret \
-p 7474:7474 -p 7687:7687 -p 5050:5050 -p 5432:5432 \
- -v peo-data:/var/lib/postgresql -v peo-pgadmin:/var/lib/pgadmin peo
+ -v boltage-data:/var/lib/postgresql -v boltage-pgadmin:/var/lib/pgadmin boltage
```
| Port | Service | Log in with |
@@ -321,41 +321,41 @@ podman run -d --name peo -e POSTGRES_PASSWORD=secret \
| 5050 | pgAdmin: | `admin@example.com` and `POSTGRES_PASSWORD` |
| 5432 | PostgreSQL, for `psql` and other SQL clients | same as neo4j-browser |
-pgAdmin comes with the bundled PostgreSQL server already registered as "peo";
+pgAdmin comes with the bundled PostgreSQL server already registered as "boltage";
it asks for the PostgreSQL password the first time you open it. AGE stores each
graph as a schema of the same name, with one table per label.
-Besides peo's own variables above, the image uses these:
+Besides boltage's own variables above, the image uses these:
| Variable | Default | Purpose |
| --- | --- | --- |
| `POSTGRES_PASSWORD` | (required) | Password of the PostgreSQL superuser, see the [postgres image docs](https://hub.docker.com/_/postgres). `POSTGRES_USER` and `POSTGRES_DB` work too. |
-| `PEO_DEFAULT_GRAPH` | `neo4j` | Also the graph created when the database is initialised. |
+| `BOLTAGE_DEFAULT_GRAPH` | `neo4j` | Also the graph created when the database is initialised. |
| `PGADMIN_DEFAULT_EMAIL` | `admin@example.com` | pgAdmin login, created on first start. |
| `PGADMIN_DEFAULT_PASSWORD` | `$POSTGRES_PASSWORD` | pgAdmin login password, set on first start. |
-| `PEO_PGADMIN` | `true` | Set to `false` to not start pgAdmin. |
-| `PEO_PGADMIN_PORT` | `5050` | Port pgAdmin listens on inside the container. |
+| `BOLTAGE_PGADMIN` | `true` | Set to `false` to not start pgAdmin. |
+| `BOLTAGE_PGADMIN_PORT` | `5050` | Port pgAdmin listens on inside the container. |
pgAdmin keeps its users and settings in `/var/lib/pgadmin`; changing
`PGADMIN_DEFAULT_*` after the first start has no effect unless that directory is empty.
-### peo with a separate PostgreSQL server
+### boltage with a separate PostgreSQL server
-The slim image runs peo and serves neo4j-browser, and is configured with the
+The slim image runs boltage and serves neo4j-browser, and is configured with the
variables in [Configuration](#configuration). Point it at any PostgreSQL server
that has AGE installed and loaded (`shared_preload_libraries=age`):
```sh
-podman build --target slim -t peo:slim .
-podman run -d --name peo -p 7474:7474 -p 7687:7687 \
- -e PEO_PG_HOST=db.example.com -e PEO_PG_DATABASE=graphs peo:slim
+podman build --target slim -t boltage:slim .
+podman run -d --name boltage -p 7474:7474 -p 7687:7687 \
+ -e BOLTAGE_PG_HOST=db.example.com -e BOLTAGE_PG_DATABASE=graphs boltage:slim
```
-peo creates no graphs of its own. If the database has none yet, run
+boltage creates no graphs of its own. If the database has none yet, run
`CREATE DATABASE neo4j` on the `system` database (`:use system` in neo4j-browser),
or `SELECT ag_catalog.create_graph('neo4j');` in SQL.
-### Compose: peo, PostgreSQL and pgAdmin as separate containers
+### Compose: boltage, PostgreSQL and pgAdmin as separate containers
[`compose.yaml`](compose.yaml) builds the slim image and runs it with
[`apache/age`](https://hub.docker.com/r/apache/age) for PostgreSQL and the official
@@ -368,10 +368,10 @@ podman compose up -d
It works with `podman compose` and `docker compose`, and publishes the same ports and
logins as the full image: neo4j-browser on 7474, Bolt on 7687, pgAdmin on 5050 and
-PostgreSQL on 5432. Set `PEO_POSTGRES_PORT` to publish PostgreSQL on another host
-port. The default graph (`PEO_DEFAULT_GRAPH`, `neo4j`) is created when the database
-is initialised, and pgAdmin comes with the server already registered as "peo".
-`PGADMIN_DEFAULT_EMAIL`, `PGADMIN_DEFAULT_PASSWORD` and `PEO_LOG_LEVEL` can be set
+PostgreSQL on 5432. Set `BOLTAGE_POSTGRES_PORT` to publish PostgreSQL on another host
+port. The default graph (`BOLTAGE_DEFAULT_GRAPH`, `neo4j`) is created when the database
+is initialised, and pgAdmin comes with the server already registered as "boltage".
+`PGADMIN_DEFAULT_EMAIL`, `PGADMIN_DEFAULT_PASSWORD` and `BOLTAGE_LOG_LEVEL` can be set
in `.env` too. Data is kept in the `postgres-data` and `pgadmin-data` volumes;
`podman compose down -v` removes them.
@@ -394,11 +394,11 @@ This is a first version. Things that do not work (yet):
- User and role management, `SHOW TRANSACTIONS`, `:sysinfo`.
- TLS (`bolt+s://`, HTTPS).
- Each query's full result is fetched from PostgreSQL before it is streamed to the
- client, so very large results use memory in peo.
+ client, so very large results use memory in boltage.
## Development
-peo depends on `psycopg` 3 and `websockets`, plus Starlette and uvicorn for the
+boltage depends on `psycopg` 3 and `websockets`, plus Starlette and uvicorn for the
`browser` extra. To work on it, install it in editable mode; `python -m build` (or
`uv build`) builds an sdist and wheel:
@@ -416,12 +416,12 @@ python tests/smoke_test.py neo4j://localhost:7687 postgres secret
## License
-peo is free software, licensed under the GNU General Public License v3.0 or
+boltage is free software, licensed under the GNU General Public License v3.0 or
later; see [LICENSE](LICENSE). The container images bundle third-party software
under their own licenses, including neo4j-browser (GPL-3.0), Apache AGE
(Apache-2.0), PostgreSQL and pgAdmin (PostgreSQL License). See
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for the full list and where
each license text and source archive is in the image.
-Neo4j is a trademark of Neo4j, Inc.; peo is not affiliated with or endorsed
+Neo4j is a trademark of Neo4j, Inc.; boltage is not affiliated with or endorsed
by Neo4j.
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index a598f53..76ae436 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -1,18 +1,18 @@
# Third-party notices
-peo's own code (the `peo/`, `container/` and `tests/` directories, the
+boltage's own code (the `boltage/`, `container/` and `tests/` directories, the
`Containerfile`, `compose.yaml` and `pyproject.toml`) is licensed under the GNU General Public
License v3.0 or later; see [LICENSE](LICENSE).
The container images built from this repository also contain the third-party
software below. None of it is stored in this repository: it is downloaded when
an image is built. Inside the images, license texts are collected under
-`/usr/share/doc/peo/`.
+`/usr/share/doc/boltage/`.
The full image (the default build target) contains everything in the table. The
slim image (`--target slim`) contains only neo4j-browser and the JavaScript
libraries bundled into it, the Debian base system with Python 3 and
-`python3-psycopg`, and the Python packages peo depends on.
+`python3-psycopg`, and the Python packages boltage depends on.
`compose.yaml` does not bundle PostgreSQL, AGE or pgAdmin: it runs the
`apache/age` and `dpage/pgadmin4` images from Docker Hub, which come with their
@@ -20,24 +20,24 @@ own license information.
| Component | License | Copyright | License text in the image |
| --- | --- | --- | --- |
-| [neo4j-browser](https://github.com/neo4j/neo4j-browser), built unmodified from the commit pinned in the `Containerfile` | GPL-3.0-or-later | Neo4j Sweden AB | `/usr/share/doc/peo/neo4j-browser/` (`LICENSE`, `NOTICE.txt`) |
-| JavaScript libraries bundled into neo4j-browser | various (MIT, Apache-2.0, BSD, ...) | their respective authors | `/usr/share/doc/peo/neo4j-browser/LICENSES.txt` |
-| [Apache AGE](https://github.com/apache/age), from the `apache/age` base image | Apache-2.0 | The Apache Software Foundation; portions Bitnine Co., Ltd. | `/usr/share/doc/peo/apache-age/` (`LICENSE`, `NOTICE`) |
+| [neo4j-browser](https://github.com/neo4j/neo4j-browser), built unmodified from the commit pinned in the `Containerfile` | GPL-3.0-or-later | Neo4j Sweden AB | `/usr/share/doc/boltage/neo4j-browser/` (`LICENSE`, `NOTICE.txt`) |
+| JavaScript libraries bundled into neo4j-browser | various (MIT, Apache-2.0, BSD, ...) | their respective authors | `/usr/share/doc/boltage/neo4j-browser/LICENSES.txt` |
+| [Apache AGE](https://github.com/apache/age), from the `apache/age` base image | Apache-2.0 | The Apache Software Foundation; portions Bitnine Co., Ltd. | `/usr/share/doc/boltage/apache-age/` (`LICENSE`, `NOTICE`) |
| [PostgreSQL](https://www.postgresql.org/), from the `apache/age` base image | PostgreSQL License | The PostgreSQL Global Development Group, The Regents of the University of California | `/usr/share/doc/postgresql-18/copyright` |
| Debian GNU/Linux base system and packages, including Python 3 and `python3-psycopg` (LGPL-3.0) | various | their respective authors | `/usr/share/doc//copyright` |
-| Python packages peo depends on, installed from PyPI: `websockets` (BSD-3-Clause), `starlette` (BSD-3-Clause), `uvicorn` (BSD-3-Clause) and their dependencies | various (BSD-3-Clause, MIT, PSF-2.0) | their respective authors | `*.dist-info/` under `/opt/peo/venv/lib/python3*/site-packages/`, or `/usr/share/doc/peo/peo-python-licenses/` for packages whose wheel lacks a license file |
+| Python packages boltage depends on, installed from PyPI: `websockets` (BSD-3-Clause), `starlette` (BSD-3-Clause), `uvicorn` (BSD-3-Clause) and their dependencies | various (BSD-3-Clause, MIT, PSF-2.0) | their respective authors | `*.dist-info/` under `/opt/boltage/venv/lib/python3*/site-packages/`, or `/usr/share/doc/boltage/boltage-python-licenses/` for packages whose wheel lacks a license file |
| [pgAdmin 4](https://www.pgadmin.org/), installed from PyPI | PostgreSQL License | The pgAdmin Development Team | `/opt/pgadmin/app/LICENSE` |
-| Python packages pgAdmin depends on, and `gunicorn` (MIT) | various | their respective authors | `*.dist-info/` under `/opt/pgadmin/lib/python3*/site-packages/`, or `/usr/share/doc/peo/pgadmin-python-licenses/` for packages whose wheel lacks a license file |
+| Python packages pgAdmin depends on, and `gunicorn` (MIT) | various | their respective authors | `*.dist-info/` under `/opt/pgadmin/lib/python3*/site-packages/`, or `/usr/share/doc/boltage/pgadmin-python-licenses/` for packages whose wheel lacks a license file |
## Source code
- **neo4j-browser** is licensed under the GPL, which requires the complete
corresponding source to accompany the built application. The image contains
- it as `/usr/share/doc/peo/neo4j-browser/neo4j-browser-source.tar.gz`, an
+ it as `/usr/share/doc/boltage/neo4j-browser/neo4j-browser-source.tar.gz`, an
archive of the exact commit that was built. Its dependencies are pinned by
the `yarn.lock` inside that archive, and the build steps are in this
repository's `Containerfile`.
-- **peo** itself ships as Python source code in the `/opt/peo/venv/`
+- **boltage** itself ships as Python source code in the `/opt/boltage/venv/`
virtual environment, next to the Python packages it depends on. This
repository has the rest.
- **Debian packages:** source is available from
@@ -45,7 +45,7 @@ own license information.
## Interoperability
-The peo bridge implements the Bolt protocol and PackStream format from their
+The boltage bridge implements the Bolt protocol and PackStream format from their
public specifications so that unmodified Neo4j clients can connect. It contains
no source code from Neo4j. To work with those clients it reports itself as
"Neo4j" with a Neo4j version number, and it recognises the exact query strings
@@ -57,5 +57,5 @@ strings to check compatibility.
Neo4j is a trademark of Neo4j, Inc. PostgreSQL is a trademark of the PostgreSQL
Community Association. Apache, Apache AGE and the Apache feather logo are
trademarks of The Apache Software Foundation. pgAdmin is a trademark of the
-pgAdmin Development Team. peo is not affiliated with or endorsed by any of
+pgAdmin Development Team. boltage is not affiliated with or endorsed by any of
them; the names are used only to identify the software it works with.
diff --git a/peo/__init__.py b/boltage/__init__.py
similarity index 52%
rename from peo/__init__.py
rename to boltage/__init__.py
index 62a0636..7377d03 100644
--- a/peo/__init__.py
+++ b/boltage/__init__.py
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
-"""peo: serves neo4j-browser and translates Bolt/Cypher to PostgreSQL + AGE."""
+"""boltage: serves neo4j-browser and translates Bolt/Cypher to PostgreSQL + AGE."""
from importlib.metadata import version
-__version__ = version("peo")
+__version__ = version("boltage")
diff --git a/peo/__main__.py b/boltage/__main__.py
similarity index 64%
rename from peo/__main__.py
rename to boltage/__main__.py
index cdb0af5..14f5295 100644
--- a/peo/__main__.py
+++ b/boltage/__main__.py
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
-"""peo: serves neo4j-browser and translates Bolt/Cypher to PostgreSQL + AGE."""
+"""boltage: serves neo4j-browser and translates Bolt/Cypher to PostgreSQL + AGE."""
import asyncio
import functools
@@ -17,30 +17,30 @@ def env(name, default):
async def main():
- logging.basicConfig(level=env("PEO_LOG_LEVEL", "INFO").upper(),
+ logging.basicConfig(level=env("BOLTAGE_LOG_LEVEL", "INFO").upper(),
format="%(asctime)s %(levelname)s %(name)s: %(message)s")
- log = logging.getLogger("peo")
+ log = logging.getLogger("boltage")
logging.getLogger("websockets").setLevel(logging.WARNING)
- browser_dir = env("PEO_BROWSER_DIR", "")
+ browser_dir = env("BOLTAGE_BROWSER_DIR", "")
if browser_dir:
try:
import uvicorn
from .web import create_app
except ImportError as e:
- sys.exit(f"peo: PEO_BROWSER_DIR is set, but serving neo4j-browser needs the browser extra "
- f"(pip install 'peo[browser]'): {e}")
+ sys.exit(f"boltage: BOLTAGE_BROWSER_DIR is set, but serving neo4j-browser needs the browser extra "
+ f"(pip install 'boltage[browser]'): {e}")
- bolt_port = int(env("PEO_BOLT_PORT", "7687"))
- http_port = int(env("PEO_HTTP_PORT", "7474"))
- advertised_port = env("PEO_ADVERTISED_BOLT_PORT", str(bolt_port))
+ bolt_port = int(env("BOLTAGE_BOLT_PORT", "7687"))
+ http_port = int(env("BOLTAGE_HTTP_PORT", "7474"))
+ advertised_port = env("BOLTAGE_ADVERTISED_BOLT_PORT", str(bolt_port))
config = Config(
- pg_host=env("PEO_PG_HOST", "127.0.0.1"),
- pg_port=int(env("PEO_PG_PORT", "5432")),
- pg_database=env("PEO_PG_DATABASE", env("POSTGRES_DB", env("POSTGRES_USER", "postgres"))),
- default_graph=env("PEO_DEFAULT_GRAPH", "neo4j"),
- advertised_bolt=f"{env('PEO_ADVERTISED_HOST', 'localhost')}:{advertised_port}",
+ pg_host=env("BOLTAGE_PG_HOST", "127.0.0.1"),
+ pg_port=int(env("BOLTAGE_PG_PORT", "5432")),
+ pg_database=env("BOLTAGE_PG_DATABASE", env("POSTGRES_DB", env("POSTGRES_USER", "postgres"))),
+ default_graph=env("BOLTAGE_DEFAULT_GRAPH", "neo4j"),
+ advertised_bolt=f"{env('BOLTAGE_ADVERTISED_HOST', 'localhost')}:{advertised_port}",
)
bolt = await asyncio.start_server(functools.partial(handle_client, config=config), "0.0.0.0", bolt_port)
diff --git a/peo/agtype.py b/boltage/agtype.py
similarity index 100%
rename from peo/agtype.py
rename to boltage/agtype.py
diff --git a/peo/backend.py b/boltage/backend.py
similarity index 97%
rename from peo/backend.py
rename to boltage/backend.py
index be7f328..8b47455 100644
--- a/peo/backend.py
+++ b/boltage/backend.py
@@ -21,14 +21,14 @@ from .cypher import (CypherError, expand_id_in_lists, first_keyword, inline_para
return_columns, rewrite_for_age, strip_statement)
from .packstream import Node, Relationship
-log = logging.getLogger("peo.backend")
+log = logging.getLogger("boltage.backend")
NEO4J_VERSION = "5.26.0"
SYSTEM_DB = "system"
SYSTEM_LABELS = ("_ag_label_vertex", "_ag_label_edge")
GRAPH_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.\-]{2,62}$")
-# Procedures emulated by peo, reported through SHOW PROCEDURES.
+# Procedures emulated by boltage, reported through SHOW PROCEDURES.
PROCEDURES = [
("db.labels", "List all node labels in the graph.", "db.labels() :: (label :: STRING)"),
("db.relationshipTypes", "List all relationship types in the graph.",
@@ -123,7 +123,7 @@ class Session:
self.conn = await psycopg.AsyncConnection.connect(
host=self.config.pg_host, port=self.config.pg_port, dbname=self.config.pg_database,
user=self.user, password=password, autocommit=True,
- application_name="peo")
+ application_name="boltage")
except psycopg.OperationalError as e:
msg = str(e)
if "password authentication failed" in msg or "role" in msg and "does not exist" in msg:
@@ -201,7 +201,7 @@ class Session:
if db == SYSTEM_DB:
raise BoltFailure("Neo.ClientError.Statement.NotSystemDatabaseCommand",
"This is an administration command and it should be executed "
- "against the system database: peo only supports a few of them.")
+ "against the system database: boltage only supports a few of them.")
result = await self._run_cypher(query, params or {}, db)
except CypherError as e:
raise BoltFailure(e.code, e.message) from e
@@ -217,9 +217,9 @@ class Session:
f"{keyword} is not supported by Apache AGE.")
text = expand_id_in_lists(inline_parameters(rewrite_for_age(text), params))
columns = return_columns(text)
- tag = "peo"
+ tag = "boltage"
while f"${tag}$" in text:
- tag = "peo_" + secrets.token_hex(4)
+ tag = "boltage_" + secrets.token_hex(4)
col_defs = ", ".join(f"c{i} ag_catalog.agtype" for i in range(max(len(columns), 1)))
stmt = (f"SELECT * FROM ag_catalog.cypher({sql.Literal(graph).as_string(self.conn)}, "
f"${tag}$ {text} ${tag}$) AS ({col_defs})")
@@ -309,7 +309,7 @@ class Session:
if m:
raise BoltFailure("Neo.ClientError.Procedure.ProcedureNotFound",
f"There is no procedure with the name `{m.group(0)[5:]}` registered "
- "for this database instance (peo).")
+ "for this database instance (boltage).")
return None
async def _show_databases(self, special, name):
@@ -366,7 +366,7 @@ class Session:
async def _cypher_values(self, graph, query):
rows = await self._sql(
f"SELECT * FROM ag_catalog.cypher({sql.Literal(graph).as_string(self.conn)}, "
- f"$peo$ {query} $peo$) AS (c0 ag_catalog.agtype)")
+ f"$boltage$ {query} $boltage$) AS (c0 ag_catalog.agtype)")
return [agtype.parse(r[0]) for r in rows]
async def _property_keys(self, graph):
diff --git a/peo/bolt.py b/boltage/bolt.py
similarity index 97%
rename from peo/bolt.py
rename to boltage/bolt.py
index d6df905..cec05c7 100644
--- a/peo/bolt.py
+++ b/boltage/bolt.py
@@ -20,7 +20,7 @@ from .backend import BoltFailure, Config, Session
from .discovery import discovery_document
from .packstream import Structure
-log = logging.getLogger("peo.bolt")
+log = logging.getLogger("boltage.bolt")
MAGIC = b"\x60\x60\xB0\x17"
SUPPORTED = [(5, 4), (5, 3), (5, 2), (5, 1), (5, 0), (4, 4)]
@@ -245,7 +245,7 @@ class BoltConnection:
except Exception as e: # noqa: BLE001 - report anything to the client
log.exception("%s: internal error", self.id)
self.results.clear()
- await self.failure("Neo.DatabaseError.General.UnknownError", f"peo internal error: {e}")
+ await self.failure("Neo.DatabaseError.General.UnknownError", f"boltage internal error: {e}")
return True
async def dispatch(self, tag, f):
@@ -278,7 +278,7 @@ class BoltConnection:
await self.success()
elif tag == COMMIT:
await self.require_session().commit()
- await self.success({"bookmark": "peo:bookmark"})
+ await self.success({"bookmark": "boltage:bookmark"})
elif tag == ROLLBACK:
await self.require_session().rollback()
await self.success()
@@ -293,7 +293,7 @@ class BoltConnection:
async def logon(self, auth):
if auth.get("scheme") != "basic":
raise BoltFailure("Neo.ClientError.Security.Unauthorized",
- "peo requires basic authentication with a PostgreSQL user name and password.")
+ "boltage requires basic authentication with a PostgreSQL user name and password.")
session = Session(self.config, auth.get("principal", ""))
await session.connect(auth.get("credentials", ""))
self.session = session
@@ -342,7 +342,7 @@ class BoltConnection:
del self.results[qid]
metadata = {"type": result.query_type, "t_last": 0, "db": result.db}
if not self.session.in_transaction:
- metadata["bookmark"] = "peo:bookmark"
+ metadata["bookmark"] = "boltage:bookmark"
await self.success(metadata)
diff --git a/peo/cypher.py b/boltage/cypher.py
similarity index 96%
rename from peo/cypher.py
rename to boltage/cypher.py
index 80e8c4d..e85e824 100644
--- a/peo/cypher.py
+++ b/boltage/cypher.py
@@ -112,7 +112,7 @@ def return_columns(text: str) -> list:
expr = text[start:end].strip()
if expr == "*":
raise CypherError("Neo.ClientError.Statement.FeatureNotSupported",
- "RETURN * is not supported by peo; list the returned variables explicitly.")
+ "RETURN * is not supported by boltage; list the returned variables explicitly.")
alias = None
for a_start, a_end in _top_level(s, r"(? str:
return "{" + ", ".join(
"`" + str(k).replace("`", "``") + "`: " + literal(x) for k, x in v.items()) + "}"
raise CypherError("Neo.ClientError.Statement.TypeError",
- f"Parameters of type {type(v).__name__} are not supported by peo")
+ f"Parameters of type {type(v).__name__} are not supported by boltage")
# neo4j-browser's "expand node" query uses a pattern comprehension, which AGE
@@ -207,8 +207,8 @@ _NEIGHBOUR_COUNT = re.compile(
def rewrite_for_age(text: str) -> str:
return _NEIGHBOUR_COUNT.sub(
- lambda m: f"OPTIONAL MATCH ({m.group(1)})-[peo_r]-() "
- f"WITH {m.group(1)}, count(peo_r) AS {m.group(2)}",
+ lambda m: f"OPTIONAL MATCH ({m.group(1)})-[boltage_r]-() "
+ f"WITH {m.group(1)}, count(boltage_r) AS {m.group(2)}",
text)
diff --git a/peo/discovery.py b/boltage/discovery.py
similarity index 91%
rename from peo/discovery.py
rename to boltage/discovery.py
index 750a2b3..96d942b 100644
--- a/peo/discovery.py
+++ b/boltage/discovery.py
@@ -15,7 +15,7 @@ def discovery_document(headers, bolt_port) -> bytes:
host = host[:host.index("]") + 1]
else:
host = host.split(":", 1)[0]
- port = os.environ.get("PEO_ADVERTISED_BOLT_PORT", str(bolt_port))
+ port = os.environ.get("BOLTAGE_ADVERTISED_BOLT_PORT", str(bolt_port))
return json.dumps({
"bolt_routing": f"neo4j://{host}:{port}",
"bolt_direct": f"bolt://{host}:{port}",
diff --git a/peo/packstream.py b/boltage/packstream.py
similarity index 100%
rename from peo/packstream.py
rename to boltage/packstream.py
diff --git a/peo/web.py b/boltage/web.py
similarity index 96%
rename from peo/web.py
rename to boltage/web.py
index 0e96f58..9efb4fa 100644
--- a/peo/web.py
+++ b/boltage/web.py
@@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
"""HTTP server for the neo4j-browser static files and Neo4j discovery.
-Needs the ``browser`` extra: ``pip install 'peo[browser]'``.
+Needs the ``browser`` extra: ``pip install 'boltage[browser]'``.
"""
from starlette.applications import Starlette
diff --git a/compose.yaml b/compose.yaml
index b216280..312ae0b 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,14 +1,14 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
#
-# peo, PostgreSQL with Apache AGE and pgAdmin as separate containers. peo runs
+# boltage, PostgreSQL with Apache AGE and pgAdmin as separate containers. boltage runs
# from the slim image (see Containerfile). Start with:
#
# POSTGRES_PASSWORD=secret podman compose up -d
#
# or put POSTGRES_PASSWORD in a .env file next to this one.
-name: peo
+name: boltage
services:
postgres:
@@ -16,13 +16,13 @@ services:
command: ["postgres", "-c", "shared_preload_libraries=age"]
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD, for example in a .env file}
- PEO_DEFAULT_GRAPH: ${PEO_DEFAULT_GRAPH:-neo4j}
+ BOLTAGE_DEFAULT_GRAPH: ${BOLTAGE_DEFAULT_GRAPH:-neo4j}
volumes:
- postgres-data:/var/lib/postgresql
# Creates the default graph when the database is initialised.
- - ./container/initdb/10-peo.sh:/docker-entrypoint-initdb.d/10-peo.sh:ro
+ - ./container/initdb/10-boltage.sh:/docker-entrypoint-initdb.d/10-boltage.sh:ro
ports:
- - "${PEO_POSTGRES_PORT:-5432}:5432"
+ - "${BOLTAGE_POSTGRES_PORT:-5432}:5432"
healthcheck:
# Over TCP, so the temporary server that runs the init scripts does not count.
test: ["CMD", "pg_isready", "--host", "127.0.0.1", "--username", "postgres"]
@@ -30,17 +30,17 @@ services:
timeout: 5s
retries: 20
- peo:
+ boltage:
build:
context: .
dockerfile: Containerfile
target: slim
- image: localhost/peo:slim
+ image: localhost/boltage:slim
environment:
- PEO_PG_HOST: postgres
- PEO_PG_DATABASE: postgres
- PEO_DEFAULT_GRAPH: ${PEO_DEFAULT_GRAPH:-neo4j}
- PEO_LOG_LEVEL: ${PEO_LOG_LEVEL:-INFO}
+ BOLTAGE_PG_HOST: postgres
+ BOLTAGE_PG_DATABASE: postgres
+ BOLTAGE_DEFAULT_GRAPH: ${BOLTAGE_DEFAULT_GRAPH:-neo4j}
+ BOLTAGE_LOG_LEVEL: ${BOLTAGE_LOG_LEVEL:-INFO}
ports:
- "7474:7474"
- "7687:7687"
diff --git a/container/peo-entrypoint.sh b/container/boltage-entrypoint.sh
similarity index 68%
rename from container/peo-entrypoint.sh
rename to container/boltage-entrypoint.sh
index e5508ad..978b0a5 100755
--- a/container/peo-entrypoint.sh
+++ b/container/boltage-entrypoint.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
-# Runs PostgreSQL (through the stock postgres entrypoint), the peo bridge and
+# Runs PostgreSQL (through the stock postgres entrypoint), the boltage bridge and
# pgAdmin side by side. The container stops when PostgreSQL or the bridge exits;
# pgAdmin is optional and its failure does not take the others down.
set -euo pipefail
@@ -10,7 +10,7 @@ if [ "${1:-}" != "postgres" ]; then
exec docker-entrypoint.sh "$@"
fi
-export PEO_PG_DATABASE="${PEO_PG_DATABASE:-${POSTGRES_DB:-${POSTGRES_USER:-postgres}}}"
+export BOLTAGE_PG_DATABASE="${BOLTAGE_PG_DATABASE:-${POSTGRES_DB:-${POSTGRES_USER:-postgres}}}"
start_pgadmin() {
local app=/opt/pgadmin/app
@@ -19,8 +19,8 @@ start_pgadmin() {
if [ ! -f /var/lib/pgadmin/pgadmin4.db ]; then
# First start: create the login user and register the bundled PostgreSQL.
cat > /var/lib/pgadmin/servers.json <&2
+if [ "${BOLTAGE_PGADMIN}" = "true" ]; then
+ start_pgadmin || echo "boltage: WARNING: pgAdmin setup failed, continuing without it" >&2
fi
shutdown() {
diff --git a/container/initdb/10-peo.sh b/container/initdb/10-boltage.sh
similarity index 88%
rename from container/initdb/10-peo.sh
rename to container/initdb/10-boltage.sh
index 0e4ccb9..9d4daa9 100755
--- a/container/initdb/10-peo.sh
+++ b/container/initdb/10-boltage.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
-# Prepares a freshly initialised cluster for peo.
+# Prepares a freshly initialised cluster for boltage.
set -euo pipefail
# The bridge connects over 127.0.0.1 with the credentials users log in with, so
@@ -12,7 +12,7 @@ sed -i -E 's/^(host[[:space:]]+[^[:space:]]+[[:space:]]+[^[:space:]]+[[:space:]]
# Create the default AGE graph that neo4j-browser opens on first connect.
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "${POSTGRES_DB:-$POSTGRES_USER}" \
- -v graph="${PEO_DEFAULT_GRAPH:-neo4j}" <<'EOSQL'
+ -v graph="${BOLTAGE_DEFAULT_GRAPH:-neo4j}" <<'EOSQL'
LOAD 'age';
SET search_path = ag_catalog, "$user", public;
SELECT ag_catalog.create_graph(:'graph')
diff --git a/container/pgadmin/compose-servers.json b/container/pgadmin/compose-servers.json
index 142e10a..5da845b 100644
--- a/container/pgadmin/compose-servers.json
+++ b/container/pgadmin/compose-servers.json
@@ -1,7 +1,7 @@
{
"Servers": {
"1": {
- "Name": "peo",
+ "Name": "boltage",
"Group": "Servers",
"Host": "postgres",
"Port": 5432,
diff --git a/container/pgadmin/config_distro.py b/container/pgadmin/config_distro.py
index 82f0912..5c2ccf8 100644
--- a/container/pgadmin/config_distro.py
+++ b/container/pgadmin/config_distro.py
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
-# pgAdmin settings for the peo image, loaded via CONFIG_DISTRO_FILE_PATH.
+# pgAdmin settings for the boltage image, loaded via CONFIG_DISTRO_FILE_PATH.
# Paths derived from DATA_DIR in pgAdmin's config.py are computed before this
# file is applied, so they are all set explicitly here.
diff --git a/pyproject.toml b/pyproject.toml
index 5b1c488..9dd5c70 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ requires = ["hatchling>=1.27", "hatch-fancy-pypi-readme>=24.1", "verri>=2026.10.
build-backend = "hatchling.build"
[project]
-name = "peo"
+name = "boltage"
dynamic = ["version", "readme"]
description = "neo4j-browser and Neo4j drivers on top of PostgreSQL, using Apache AGE as the graph engine"
license = "GPL-3.0-or-later"
@@ -26,10 +26,10 @@ binary = ["psycopg[binary]>=3.1"]
browser = ["starlette>=0.37", "uvicorn>=0.29"]
[project.urls]
-Source = "https://git.bartbroe.re/bartbroere/peo"
+Source = "https://git.bartbroe.re/bartbroere/boltage"
[project.scripts]
-peo = "peo.__main__:run"
+boltage = "boltage.__main__:run"
# The version is derived from the date of the HEAD commit, plus the number of
# commits made earlier that day, like 2026.10.21.0. See version.py.
@@ -39,10 +39,10 @@ path = "version.py"
expression = "strawberry()"
[tool.hatch.build.targets.sdist]
-include = ["/peo", "/README.md", "/LICENSE", "/version.py"]
+include = ["/boltage", "/README.md", "/LICENSE", "/version.py"]
[tool.hatch.build.targets.wheel]
-packages = ["peo"]
+packages = ["boltage"]
# The README links to other files in the repository with relative links, which
# do not work on PyPI. Make them absolute in the package metadata only.
@@ -54,8 +54,8 @@ path = "README.md"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '\]\(#'
-replacement = '](https://git.bartbroe.re/bartbroere/peo#'
+replacement = '](https://git.bartbroe.re/bartbroere/boltage#'
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '\]\((?![a-z]+:|#)([^)]+)\)'
-replacement = '](https://git.bartbroe.re/bartbroere/peo/src/branch/main/\1)'
+replacement = '](https://git.bartbroe.re/bartbroere/boltage/src/branch/main/\1)'
diff --git a/tests/smoke_test.py b/tests/smoke_test.py
index a178853..df20b54 100644
--- a/tests/smoke_test.py
+++ b/tests/smoke_test.py
@@ -1,6 +1,6 @@
# SPDX-FileCopyrightText: 2026 Bart Broere
# SPDX-License-Identifier: GPL-3.0-or-later
-"""End-to-end smoke test for a running peo bridge, using the official Neo4j driver.
+"""End-to-end smoke test for a running boltage bridge, using the official Neo4j driver.
Usage: python tests/smoke_test.py [bolt-uri] [user] [password]
Requires: pip install neo4j
@@ -14,7 +14,7 @@ from neo4j.graph import Node, Path, Relationship
uri = sys.argv[1] if len(sys.argv) > 1 else "neo4j://localhost:7687"
user = sys.argv[2] if len(sys.argv) > 2 else "postgres"
password = sys.argv[3] if len(sys.argv) > 3 else "postgres"
-GRAPH = "peo_smoke"
+GRAPH = "boltage_smoke"
def check(label, condition):
diff --git a/uv.lock b/uv.lock
index e4455b8..f11a267 100644
--- a/uv.lock
+++ b/uv.lock
@@ -43,7 +43,7 @@ wheels = [
]
[[package]]
-name = "peo"
+name = "boltage"
source = { editable = "." }
dependencies = [
{ name = "psycopg" },