Rename the project from peo to boltage
The name peo cannot be registered on PyPI. Rename the package, the command, the PEO_* environment variables (now BOLTAGE_*), the container paths and scripts, and the source URLs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5418473158
commit
5784de249d
20 files changed
+180
-180
No files matched your search
+47
-47
@@ -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"]
|
||||
@@ -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 <http://localhost:7474/>. 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 <name>` switches between them. Clients that do not pick a database get `PEO_DEFAULT_GRAPH`
|
||||
`:use <name>` 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('<graph>', $$ <query> $$) 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 2<sup>48</sup>. 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: <http://localhost:5050/> | `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.
|
||||
+12
-12
@@ -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/<package>/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.
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
File renamed without changes.
@@ -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):
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"(?<![\w.$])AS\s+(`[^`]*`|\w+)\s*$", start, end):
|
||||
alias = re.match(r"AS\s+(.*)", text[a_start:a_end].strip(), re.IGNORECASE | re.DOTALL).group(1)
|
||||
@@ -195,7 +195,7 @@ def literal(v) -> 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)
|
||||
|
||||
|
||||
@@ -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}",
|
||||
File renamed without changes.
@@ -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
|
||||
+11
-11
@@ -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"
|
||||
|
||||
@@ -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 <<EOF
|
||||
{"Servers": {"1": {"Name": "peo", "Group": "Servers", "Host": "127.0.0.1", "Port": 5432,
|
||||
"MaintenanceDB": "${PEO_PG_DATABASE}", "Username": "${POSTGRES_USER:-postgres}", "SSLMode": "prefer"}}}
|
||||
{"Servers": {"1": {"Name": "boltage", "Group": "Servers", "Host": "127.0.0.1", "Port": 5432,
|
||||
"MaintenanceDB": "${BOLTAGE_PG_DATABASE}", "Username": "${POSTGRES_USER:-postgres}", "SSLMode": "prefer"}}}
|
||||
EOF
|
||||
chown postgres:postgres /var/lib/pgadmin/servers.json
|
||||
PGADMIN_SETUP_EMAIL="$email" \
|
||||
@@ -31,21 +31,21 @@ EOF
|
||||
|| { rm -f /var/lib/pgadmin/pgadmin4.db; return 1; }
|
||||
fi
|
||||
|
||||
gosu postgres /opt/pgadmin/bin/gunicorn --bind "0.0.0.0:${PEO_PGADMIN_PORT}" \
|
||||
gosu postgres /opt/pgadmin/bin/gunicorn --bind "0.0.0.0:${BOLTAGE_PGADMIN_PORT}" \
|
||||
--workers 1 --threads 25 --timeout 0 --chdir "$app" --log-level warning pgAdmin4:app &
|
||||
pgadmin_pid=$!
|
||||
echo "peo: pgAdmin on http://localhost:${PEO_PGADMIN_PORT}/ (login: $email)"
|
||||
echo "boltage: pgAdmin on http://localhost:${BOLTAGE_PGADMIN_PORT}/ (login: $email)"
|
||||
}
|
||||
|
||||
docker-entrypoint.sh "$@" &
|
||||
pg_pid=$!
|
||||
|
||||
gosu postgres /opt/peo/venv/bin/peo &
|
||||
gosu postgres /opt/boltage/venv/bin/boltage &
|
||||
bridge_pid=$!
|
||||
|
||||
pgadmin_pid=
|
||||
if [ "${PEO_PGADMIN}" = "true" ]; then
|
||||
start_pgadmin || echo "peo: WARNING: pgAdmin setup failed, continuing without it" >&2
|
||||
if [ "${BOLTAGE_PGADMIN}" = "true" ]; then
|
||||
start_pgadmin || echo "boltage: WARNING: pgAdmin setup failed, continuing without it" >&2
|
||||
fi
|
||||
|
||||
shutdown() {
|
||||
@@ -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')
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Servers": {
|
||||
"1": {
|
||||
"Name": "peo",
|
||||
"Name": "boltage",
|
||||
"Group": "Servers",
|
||||
"Host": "postgres",
|
||||
"Port": 5432,
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+7
-7
@@ -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)'
|
||||
+2
-2
@@ -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):
|
||||
|
||||
Reference in new issue
Block a user