Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6b0dbf2
Add unit and integration tests for edit distance service
synapp009 Jul 21, 2026
69b4d39
Enhance edit distance service: update dependencies, improve README, a…
synapp009 Jul 21, 2026
0e2a22e
Refactor Dockerfile to install dependencies and build wheel for edit …
synapp009 Jul 21, 2026
99b978b
Update README.md: enhance input data formats for text and graph edit …
synapp009 Jul 21, 2026
c1677e4
Update README.md: translate German sections to English for consistenc…
synapp009 Jul 22, 2026
c4fa83d
remove GED GET/DELETE endpoints; return GED results inline; update do…
synapp009 Jul 22, 2026
777c6cc
refactor(edit-distance-service): backend auto-selection, drop user-fa…
synapp009 Jul 27, 2026
9117016
refactor(edit-distance-service): delete cli.py, consolidate GED backe…
synapp009 Jul 28, 2026
39fc111
refactor(edit-distance-service): delete cli.py, consolidate GED backe…
synapp009 Jul 28, 2026
d6f7757
fix(edit-distance-service): enforce strict typing and fix workflow
synapp009 Jul 28, 2026
77c161e
refactor(edit-distance-service): unify route paths and add comprehens…
synapp009 Jul 28, 2026
dab7a22
fixed some lint errors
synapp009 Jul 28, 2026
0bc1df9
more fix for lint
synapp009 Jul 28, 2026
4061348
fix test
synapp009 Jul 28, 2026
b98fedc
fix(edit-distance): resolve 12 code-review findings
synapp009 Jul 29, 2026
9dfdd9f
fix(edit-distance): remove 'status' field assertion from GED smoke tests
synapp009 Jul 29, 2026
a19af05
feat(edit-distance): add CLI, add gmatch4py to Docker, audit & simpli…
synapp009 Jul 29, 2026
4b58eb6
feat(models): type-safe input models and multi-format graph parsing
synapp009 Jul 29, 2026
1afaae4
fix lint error
synapp009 Jul 29, 2026
0b775f6
update agent md
synapp009 Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions .github/workflows/service-edit-distance-service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: edit-distance-service

on:
push:
branches: ["master"]
paths:
- "services/edit-distance-service/**"
pull_request:
branches: ["master"]
paths:
- "services/edit-distance-service/**"

defaults:
run:
working-directory: services/edit-distance-service

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('services/edit-distance-service/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -e ".[dev]"
- name: Ruff linting
run: make lint

test:
name: Test
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('services/edit-distance-service/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -e ".[dev]"
- name: Run tests
run: pytest -v --tb=short
- name: Run smoke tests
run: |
set -e
# Start server in background
uvicorn src.main:app --host 0.0.0.0 --port 8000 &
SERVER_PID=$!

# Wait for server to be ready
for i in $(seq 1 30); do
if curl -s http://localhost:8000/health | grep -q '"status":"ok"'; then
echo "Server is ready"
break
fi
echo "Waiting for server... ($i/30)"
sleep 1
done

# Smoke test: health endpoint
echo "=== Health check ==="
curl -sf http://localhost:8000/health | python -c "import sys,json; d=json.load(sys.stdin); assert d['status']=='ok', f'Health failed: {d}'"
echo "Health: OK"

# Smoke test: text algorithms discovery
echo "=== Text algorithms ==="
curl -sf http://localhost:8000/v1/text/algorithms | python -c "import sys,json; d=json.load(sys.stdin); assert len(d)>0, 'No algorithms'; print(f'Found {len(d)} text algorithms')"

# Smoke test: GED algorithms discovery
echo "=== GED algorithms ==="
curl -sf http://localhost:8000/v1/graphs/algorithms | python -c "import sys,json; d=json.load(sys.stdin); assert len(d)>0, 'No GED algorithms'; print(f'Found {len(d)} GED algorithms')"

# Smoke test: Levenshtein compute
echo "=== Levenshtein compute ==="
curl -sf -X POST http://localhost:8000/v1/text/distance \
-H 'Content-Type: application/json' \
-d '{"algorithm":"levenshtein","params":{},"inputs":[{"id":"p1","a":"kitten","b":"sitting"}]}' \
| python -c "import sys,json; d=json.load(sys.stdin); assert d['algorithm']=='levenshtein'; assert len(d['results'])==1; print('Levenshtein: OK')"

# Smoke test: GED compute
echo "=== GED compute ==="
curl -sf -X POST http://localhost:8000/v1/graphs/distance \
-H 'Content-Type: application/json' \
-d '{"algorithm":"ged_astar","params":{"mode":"exact","timeout_ms":5000},"graphs":[{"id":"p1","g1":{"nodes":[{"id":"A"},{"id":"B"}],"edges":[{"source":"A","target":"B"}]},"g2":{"nodes":[{"id":"A"},{"id":"B"},{"id":"C"}],"edges":[{"source":"A","target":"B"},{"source":"B","target":"C"}]}}]}' \
| python -c "import sys,json; d=json.load(sys.stdin); assert d['algorithm']=='ged_astar'; assert len(d['results'])==1; print('GED compute: OK')"

echo "=== All smoke tests passed ==="

# Stop the server
kill $SERVER_PID 2>/dev/null || true
wait $SERVER_PID 2>/dev/null || true

generate-openapi:
name: Generate OpenAPI Spec
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Cache dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('services/edit-distance-service/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -e ".[dev]"
- name: Generate OpenAPI spec
run: make generate-openapi
- name: Upload OpenAPI spec
uses: actions/upload-artifact@v4
with:
name: openapi-spec-edit-distance-service
path: services/edit-distance-service/edit-distance-service.openapi.json

build:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: generate-openapi
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/edit-distance-service
tags: |
type=sha,prefix=sha-
type=raw,value=latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
dockerfile: services/edit-distance-service/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
63 changes: 63 additions & 0 deletions services/edit-distance-service/.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Edit Distance Service

## Location
`/home/user/fernuni_job/ALADIN-Services/services/edit-distance-service/`

## Architecture
- **Language**: Python 3.11+ (FastAPI)
- **Pattern**: Mirrors `noise-generation-service` (Rust/Axum) in Python
- **Framework**: FastAPI with uvicorn
- **CLI**: Click-based (src/cli.py) + run_cli() in main.py
- **OpenAPI**: Auto-generated via FastAPI's get_openapi()

## API Endpoints
### Text ED (Part A)
- `GET /v1/text/algorithms` — Discovery
- `POST /v1/text/distance` — Compute (synchronous)

### Graph ED (Part B)
- `GET /v1/graphs/algorithms` — Discovery
- `POST /v1/graphs/distance` — Compute (synchronous, returns 200 with result inline)
(Note: GET/DELETE by result id are not exposed; results are returned inline by POST.)

## Libraries
### Text ED (15 algorithm families)
- **Tier 1** (87%): RapidFuzz + textdistance + jellyfish
- **Tier 2** (100%): + edlib + diff-match-patch

### Graph ED (10 algorithm families)
- **Tier 1** (80%): NetworkX + GEDLIB (via gedlibpy)
- **Tier 2** (100%): + GMatch4py

## Key Files
- `src/main.py` — FastAPI app with all routes
- `src/models.py` — Pydantic models (request/response schemas):
- `InputPair(id, a, b)` / `InputPhonetic(id, text)` — text input types
- `GraphRef(nodes, edges, format, links, matrix, node_labels)` — graph input (3 formats)
- `GraphPair(id, g1, g2)` — a pair of GraphRefs
- `TextCompareRequest(inputs: list[InputPair | InputPhonetic], ...)` — typed union
- `GedComputeRequest(graphs: list[GraphPair], ...)` — typed GraphPair
- `src/text/__init__.py` — Text ED implementations (dispatcher pattern)
- `src/graph/__init__.py` — Graph ED implementations (dispatcher pattern); `_graph_ref_to_nx()` handles 3 formats: default (nodes+edges), `node_link` (networkx JSON), `adjacency_matrix`
- `src/cli.py` — Click CLI (list, compare, serve, openapi commands)
- `tests/test_text_compare.py` — Unit tests for text algorithms
- `tests/test_graph_compare.py` — Unit tests for graph algorithms
- `tests/test_integration.py` — FastAPI TestClient integration tests (incl. adjacency_matrix + node_link format tests)
- `tests/test_smoke.sh` — Bash smoke test script

## Build/Deploy
- `make prep` — pip install -e ".[dev]"
- `make test` — pytest -v
- `make start` — uvicorn with --reload
- `make generate-openapi` — generates edit-distance-service.openapi.json
- `make docker-build` — multi-stage Docker build
- `Dockerfile` — Multi-stage (builder + runtime)
- `docker-compose.dev.yml` — Dev with volume mount
- `docker-compose.prod.yml` — Production

## Design Patterns
- Discriminated union request body (algorithm field as discriminator)
- Backend is explicit (different libs can produce different results)
- Discovery endpoint exposes full catalog at runtime
- Response is discriminated union on result_type
- All compute endpoints accept arrays (batching)
23 changes: 23 additions & 0 deletions services/edit-distance-service/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Git
.git
.gitignore

# Docker
docker-compose*.yml
Dockerfile
README.md

# Build artifacts
*.openapi.json

# Python
__pycache__/
*.pyc
*.pyo
.venv
venv
*.egg-info/
.pytest_cache/

# Node (monorepo noise)
node_modules/
10 changes: 10 additions & 0 deletions services/edit-distance-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
.venv/
venv/
env/
*.openapi.json
dist/
build/
53 changes: 53 additions & 0 deletions services/edit-distance-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Stage 1: Build
FROM python:3.12-slim AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
g++ \
python3-dev \
libpython3.12-dev \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY services/edit-distance-service/pyproject.toml .
COPY services/edit-distance-service/src/ src/

# Install base dependencies and build wheel
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir build && \
python -m build --wheel && \
pip install --no-cache-dir dist/*.whl && \
rm -rf dist

# Install optional GED library (gedlibpy) from monorepo third_party/
# (not on PyPI — ships with the ALADIN monorepo)
COPY third_party/gedlibpy/ /tmp/third_party/gedlibpy/
RUN pip install --no-cache-dir Cython && \
cd /tmp/third_party/gedlibpy && \
python setup.py build_ext --inplace && \
pip install --no-cache-dir . && \
rm -rf /tmp/third_party

# Install optional GED library (gmatch4py) from PyPI
# (ged_hausdorff, ged_greedy, ged_heuristic backends)
RUN pip install --no-cache-dir gmatch4py

# Stage 2: Runtime
FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
libssl3 \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY services/edit-distance-service/src/ src/
COPY services/edit-distance-service/pyproject.toml .

# Set the user
USER nobody

# Start the service
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
34 changes: 34 additions & 0 deletions services/edit-distance-service/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.PHONY: prep build test lint start clean docker-build generate-openapi dev prod

prep:
pip install -e ".[dev]"

build:
pip install -e ".[dev]"

test:
pytest -v

lint:
ruff check src/
ruff format --check src/

start:
uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload

clean:
rm -rf __pycache__ src/__pycache__ src/**/__pycache__
rm -rf .venv venv
rm -rf *.egg-info

generate-openapi:
python -c "from src.main import app; import json; from fastapi.openapi.utils import get_openapi; spec = get_openapi(title=app.title, version=app.version, openapi_version=app.openapi_version, description=app.description, routes=app.routes); print(json.dumps(spec, indent=2))" > edit-distance-service.openapi.json

docker-build:
docker build -f Dockerfile -t edit-distance-service ../..

dev:
docker compose -f docker-compose.dev.yml up --build

prod:
docker compose -f docker-compose.prod.yml up --build
Loading
Loading