Metadata-Version: 2.4
Name: superseed
Version: 0.2.0
Summary: Declarative Neo4j fixtures for pytest
Project-URL: Documentation, https://github.com/angelmota/superseed#readme
Project-URL: Source, https://github.com/angelmota/superseed
License: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: neo4j>=5
Requires-Dist: pyyaml>=6
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Requires-Dist: testcontainers[neo4j]>=4; extra == 'dev'
Requires-Dist: types-pyyaml>=6; extra == 'dev'
Provides-Extra: neo4j
Requires-Dist: neo4j>=5; extra == 'neo4j'
Description-Content-Type: text/markdown

# SuperSeed

Declarative Neo4j fixtures for pytest. Seed **real graph data** before each test, run your services against it, then tear it down automatically.

> **This is not mocking.** SuperSeed does not patch repositories or stub the driver. It loads YAML scenarios, compiles them to Cypher, `MERGE`s nodes and relationships into Neo4j, and deletes everything tagged with a per-run ID when the test finishes.

**Requirements:** Python 3.11+, Neo4j 5+, pytest

---

## Table of contents

1. [How it works](#how-it-works)
2. [Quick start — PyPI install](#quick-start--pypi-install)
3. [Quick start — clone the monorepo demo](#quick-start--clone-the-monorepo-demo)
4. [Writing tests with `@super_seed`](#writing-tests-with-super_seed)
5. [`superseed.yaml` reference](#superseedyaml-reference)
6. [`*.schema.yaml` reference](#schemayaml-reference)
7. [Cleanup model (`testRunId`)](#cleanup-model-testrunid)
8. [CLI](#cli)
9. [Full walkthrough repo](#full-walkthrough-repo)
10. [Contributing](#contributing)

---

## How it works

```mermaid
sequenceDiagram
    autonumber
    participant Dev as Your test
    participant Plugin as SuperSeed plugin
    participant YAML as superseed.yaml
    participant Compiler as compiler
    participant Neo4j as Neo4j
    participant App as Service / repository

    Dev->>Plugin: @super_seed("matrix_cast")
    Plugin->>YAML: find config + load scenario
    Plugin->>Compiler: compile_scenario(run_id)
    Compiler-->>Plugin: CypherSeedPlan (MERGE statements)
    Plugin->>Neo4j: seed — MERGE nodes & relationships
    Note over Neo4j: every node tagged with testRunId
    Plugin->>Dev: run test body
    Dev->>App: call business logic
    App->>Neo4j: real driver queries
    Neo4j-->>App: graph results
    App-->>Dev: assertions pass / fail
    Plugin->>Neo4j: cleanup(testRunId)
    Note over Neo4j: DETACH DELETE seeded nodes
```

```text
superseed.yaml scenario
        │
        ▼
   compile to Cypher (MERGE nodes + relationships)
        │
        ▼
   seed Neo4j (tag every node with testRunId)
        │
        ▼
   your pytest runs (real service / repository code)
        │
        ▼
   cleanup (DELETE all nodes with that testRunId)
```

1. You decorate a test with `@super_seed("scenario_name")`.
2. The pytest plugin finds `superseed.yaml` (searches upward from the test file).
3. It compiles the named scenario to parameterized Cypher statements.
4. Before the test body runs, it seeds Neo4j and tags created nodes with a unique `testRunId`.
5. After the test (pass or fail), it deletes every node with that `testRunId`.

Your tests talk to **real** Neo4j through your **real** application code — the same code you run in production.

---

## Quick start — PyPI install

Use this path when SuperSeed is a dependency in your own project.

### 1. Install

```bash
pip install superseed
# or
uv add superseed
```

You also need a running Neo4j instance. Minimal Docker setup:

```bash
docker run -d \
  --name neo4j \
  -p 7474:7474 -p 7687:7687 \
  -e NEO4J_AUTH=neo4j/your-password \
  neo4j:5
```

### 2. Add `superseed.yaml` beside your tests

```yaml
neo4j:
  uri: ${NEO4J_URI:-bolt://localhost:7687}
  user: neo4j
  password: ${NEO4J_PASSWORD:-your-password}

defaults:
  Movie:
    released: 1999

scenarios:
  matrix_cast:
    description: "The Matrix with Neo and Trinity"
    parameters:
      movie_title: "The Matrix"
    nodes:
      - label: Movie
        key: {title: "${movie_title}"}
        props: {released: 1999}
      - label: Actor
        key: {name: "Keanu Reeves"}
      - label: Actor
        key: {name: "Carrie-Anne Moss"}
        props: {born: 1967}
    relationships:
      - type: ACTED_IN
        from: {label: Actor, key: {name: "Keanu Reeves"}}
        to: {label: Movie, key: {title: "${movie_title}"}}
        props: {role: "Neo"}
      - type: ACTED_IN
        from: {label: Actor, key: {name: "Carrie-Anne Moss"}}
        to: {label: Movie, key: {title: "${movie_title}"}}
        props: {role: "Trinity"}
```

Set environment variables for your Neo4j connection:

```bash
export NEO4J_URI=bolt://localhost:7687
export NEO4J_PASSWORD=your-password
```

### 3. Write a test

```python
from superseed import super_seed

@super_seed("matrix_cast")
def test_get_cast(movie_service):
    cast = movie_service.get_cast_for_movie("The Matrix")
    assert len(cast) == 2
```

The plugin loads automatically when pytest discovers SuperSeed (via the `pytest11` entry point). No `pytest_plugins` line required if `superseed` is installed.

### 4. Run

```bash
pytest tests/ -v
superseed validate -c tests/superseed.yaml   # optional CI check
```

---

## Quick start — clone the monorepo demo

Use this path to explore the reference app and dogfood the library locally.

```bash
git clone <repo-url> superseed && cd superseed
cp .env.example .env
docker compose up --build -d    # Neo4j + movie-api

# Package dev setup
uv sync --extra dev

# Run the demo integration tests (Neo4j only — movie-api container optional)
export NEO4J_URI=bolt://localhost:7687
export NEO4J_PASSWORD=superseed-dev
cd examples/movie-api && uv sync --extra dev && uv run pytest -v
```

| URL | What |
|-----|------|
| http://localhost:7474 | Neo4j Browser (`neo4j` / `superseed-dev`) |
| http://localhost:8000/docs | Movie API (FastAPI demo) |
| `bolt://localhost:7687` | Host pytest / CLI |

After running a seeded test you can inspect the graph in Neo4j Browser or hit the API:

```bash
uv run pytest -k matrix -v
curl http://localhost:8000/movies/The%20Matrix/actors
```

---

## Writing tests with `@super_seed`

### Basic usage

```python
from superseed import super_seed

@super_seed("matrix_cast")
def test_get_cast_for_matrix(movie_service):
    cast = movie_service.get_cast_for_movie("The Matrix")
    assert len(cast) == 2
```

### Parameter overrides

Override scenario parameters per test without duplicating YAML:

```python
@super_seed("matrix_cast", movie_title="Inception")
def test_custom_title(movie_service):
    ...
```

In YAML, reference parameters with lowercase placeholders: `${movie_title}`.

### Config discovery

By default the plugin searches upward from the test file for `superseed.yaml`. Override with:

```bash
pytest --superseed-config path/to/superseed.yaml
```

Optional schema validation:

```bash
pytest --superseed-schema path/to/movies.schema.yaml
```

If omitted, the plugin looks for `movies.schema.yaml` or `superseed.schema.yaml` next to the config file.

### What you should **not** do

```python
# Before SuperSeed — don't do this anymore
def test_old_way(neo4j_session, movie_service):
    neo4j_session.run("CREATE (m:Movie ...)")  # hand-written seed
    ...
    neo4j_session.run("MATCH (n) DETACH DELETE n")  # manual cleanup
```

SuperSeed replaces inline `CREATE` blocks and manual teardown.

---

## `superseed.yaml` reference

Top-level keys:

| Key | Required | Description |
|-----|----------|-------------|
| `neo4j` | yes | Connection settings (`uri`, `user`, `password`) |
| `defaults` | no | Default properties per label, merged into every node |
| `scenarios` | yes | Named scenario definitions |

### Environment substitution

Use uppercase env vars in config values:

```yaml
neo4j:
  uri: ${NEO4J_URI:-bolt://localhost:7687}
  password: ${NEO4J_PASSWORD:-superseed-dev}
```

Syntax: `${VAR}` or `${VAR:-default}`.

### Scenario fields

| Field | Required | Description |
|-------|----------|-------------|
| `description` | no | Human-readable summary (shown by `superseed list`) |
| `linked_repository` | no | Dotted repo method path for future auto-detection, e.g. `MovieRepository.find_actors_for_movie` |
| `parameters` | no | Default parameter values; overridable via `@super_seed(..., param=value)` |
| `required_labels` | no | Documentation / scan metadata |
| `required_relationships` | no | Documentation / scan metadata |
| `nodes` | yes | Nodes to `MERGE` |
| `relationships` | no | Relationships to `MERGE` between matched nodes |

### Node entry

```yaml
- label: Movie
  key: {title: "${movie_title}"}   # MERGE identity — must be unique in the graph
  props: {released: 1999}          # additional SET properties
```

- **`key`** — business identity used in `MERGE (n:Label {key...})`
- **`props`** — extra properties merged after defaults and schema defaults
- **`${parameter}`** — lowercase placeholders resolved at compile time from `parameters` + decorator overrides

### Relationship entry

```yaml
- type: ACTED_IN
  from: {label: Actor, key: {name: "Keanu Reeves"}}
  to: {label: Movie, key: {title: "${movie_title}"}}
  props: {role: "Neo"}
```

The compiler `MATCH`es both endpoints by key, then `MERGE`s the relationship.

---

## `*.schema.yaml` reference

Optional label contract validated before compile. Place beside `superseed.yaml` (e.g. `movies.schema.yaml`).

```yaml
Movie:
  required: [title]
  optional: [released, id]
  defaults:
    released: 1999

Actor:
  required: [name]
  optional: [born, id]
  defaults:
    born: 1964
```

| Key | Description |
|-----|-------------|
| `required` | Properties that must appear in `key` + `props` (after defaults) |
| `optional` | Allowed extra properties |
| `defaults` | Applied before scenario `props` |

Run `superseed validate -c tests/superseed.yaml` to catch schema violations in CI.

---

## Cleanup model (`testRunId`)

Every seeded node gets a property:

```text
testRunId = "<uuid per test run>"
```

Cleanup query (run automatically after each `@super_seed` test):

```cypher
MATCH (n {testRunId: $run_id}) DETACH DELETE n
```

**Why this works:**

- Each test run gets a fresh UUID — no collisions between parallel or sequential tests.
- `DETACH DELETE` removes relationships automatically when nodes are deleted.
- Tests are isolated: one test's graph never leaks into the next.
- Failed tests still clean up (teardown runs regardless of pass/fail).

Relationships are not tagged; they disappear when their endpoint nodes are deleted.

---

## CLI

```bash
superseed validate -c path/to/superseed.yaml   # parse YAML + schema; exit 1 on error
superseed list     -c path/to/superseed.yaml   # print scenario names + descriptions
superseed scan     path/to/repositories        # suggest scenario stubs from repo Cypher
```

### `superseed scan`

Walks `*repository*.py` files, extracts Cypher via AST, and writes:

```text
.superseed/
├── suggested/
│   ├── find_actors_for_movie.yaml   # merge into superseed.yaml
│   └── ...
└── repo_index.json                  # machine-readable index for tooling
```

Each suggested stub includes `linked_repository`, detected labels, and relationship types. Edit the stub, copy the scenario into `superseed.yaml`, and run `superseed validate`.

Example:

```bash
superseed scan app/repositories -o .superseed
superseed validate -c tests/superseed.yaml
```

---

## Full walkthrough repo

The git monorepo includes a complete reference app that dogfoods SuperSeed:

```text
examples/movie-api/
├── app/
│   ├── repositories/movie_repository.py   # real Cypher strings (scan targets these)
│   ├── services/movie_service.py
│   └── main.py                            # FastAPI demo
└── tests/
    ├── superseed.yaml                     # scenarios
    ├── movies.schema.yaml                 # label contract
    └── test_movie_service.py              # @super_seed tests — no inline CREATE
```

See [`examples/movie-api/tests/test_movie_service.py`](examples/movie-api/tests/test_movie_service.py) and [`examples/movie-api/tests/superseed.yaml`](examples/movie-api/tests/superseed.yaml) for a working end-to-end example.

---

## Contributing

SuperSeed is developed in the open. Planning docs:

- [Future work (v0.3+ auto-detection)](docs/FUTURE.md)

Local development:

```bash
uv sync --extra dev
uv run pytest tests/ -v
uv run ruff check src tests
uv run mypy src
```

---

## License

MIT — see [LICENSE](LICENSE).
