Metadata-Version: 2.4
Name: orthograph
Version: 0.1.0
Summary: Pydantic-native graph data model definition and validation
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: OS Independent
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: graphglot>=0.9
Provides-Extra: networkx
Requires-Dist: networkx>=3.0; extra == "networkx"
Provides-Extra: cypher
Provides-Extra: neo4j
Requires-Dist: neo4j>=5.0; extra == "neo4j"
Provides-Extra: memgraph
Requires-Dist: neo4j>=5.0; extra == "memgraph"
Provides-Extra: gqlalchemy
Requires-Dist: gqlalchemy>=1.9; extra == "gqlalchemy"
Provides-Extra: all
Requires-Dist: orthograph[cypher,gqlalchemy,memgraph,neo4j,networkx]; extra == "all"
Provides-Extra: notebooks
Requires-Dist: ipykernel>=6.0; extra == "notebooks"
Requires-Dist: nbval>=0.11; extra == "notebooks"
Provides-Extra: notebooks-integration-extensions
Requires-Dist: fastapi>=0.100; extra == "notebooks-integration-extensions"
Requires-Dist: httpx2>=2.0; extra == "notebooks-integration-extensions"
Requires-Dist: dash>=4.3.0; extra == "notebooks-integration-extensions"
Requires-Dist: dash_bootstrap_components>=2.0.0; extra == "notebooks-integration-extensions"
Provides-Extra: docs
Requires-Dist: sphinx>=7.0; extra == "docs"
Requires-Dist: myst-nb>=1.0; extra == "docs"
Requires-Dist: furo>=2024.0; extra == "docs"
Requires-Dist: sphinx-design>=0.5; extra == "docs"
Requires-Dist: sphinxcontrib-mermaid>=0.9; extra == "docs"
Provides-Extra: dev
Requires-Dist: orthograph[all]; extra == "dev"
Requires-Dist: orthograph[docs]; extra == "dev"
Requires-Dist: orthograph[notebooks]; extra == "dev"
Requires-Dist: orthograph[notebooks_integration_extensions]; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: pytest-mock>=3.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: ruff>=0.9; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Requires-Dist: pre-commit>=3.0; extra == "dev"
Requires-Dist: bumpver>=2023.0; extra == "dev"
Requires-Dist: types-PyYAML>=6.0; extra == "dev"
Requires-Dist: types-networkx>=3.0; extra == "dev"
Requires-Dist: types-setuptools>=75.0; extra == "dev"
Dynamic: license-file

Orthograph
==========

Pydantic-native graph data governance: one declared contract for your property
graph, continuously checked against your data, your queries, and your live
database.

Orthograph is a **library** — not a platform, not an ORM — that gives a property
graph the thing it usually lacks: a single declared contract the application can
read, and an enforcement loop around it. You declare node types, relationship
types, properties, and cardinalities once in Python or YAML; Orthograph then
validates data against that contract, governs a typed Cypher query catalogue, and
detects drift between the contract and a live database. It sits *above* the
database, driver, and any ORM, and never owns a connection — the caller passes one
in when Orthograph needs it.

It is vendor-agnostic and works with Neo4j, Memgraph, NetworkX, and raw Cypher.

Full documentation: https://orthograph.readthedocs.io

.. contents::
   :local:
   :depth: 2


What it does
------------

Property graphs are schema-flexible by design. That flexibility is an asset
during exploration and a liability in production: properties get loosely typed,
cardinalities are assumed but never checked, queries are raw strings that keep
running after a label is renamed (returning wrong or empty results with no
error), and the live database drifts away from the model nobody wrote down.
The database's own constraints enforce only a subset, below the application, and
are not the same as the application's *intended* contract.

Orthograph closes that gap with four distinct capabilities:

**1. Define the contract**
   Declare node types, relationship types, properties, and cardinality
   constraints once — in Python or YAML — and use that declaration as the single
   source of truth across every validation and query path.

**2. Validate data**
   Validate in-memory graph data against the contract before it reaches the
   database. Produces a structured ``ValidationResult`` with typed,
   differentiable error codes.

**3. Govern queries**
   Register named Cypher queries in a typed ``QueryCatalogue``. Each query
   declares its parameter and output models; the catalogue validates
   parameter↔template alignment at registration and checks each query — *without
   executing it* — for Cypher language correctness and for domain match against
   the contract (labels, relationship types, properties, endpoints).

**4. Detect drift**
   Detect divergence across the three-layer stack. ``validate_catalogue()``
   compares a whole query set against the contract; ``compare`` inspects a live
   database into a profile and compares that profile against the contract;
   ``validate_catalogue_against_profile()`` checks the query set against both at
   once — so schema evolution never silently desynchronises your queries and your
   database from your declared truth.

Query governance and drift detection are separate concerns: governance keeps
*individual queries* honest against the contract at the moment you register them;
drift detection answers whether *whole sets* — the query catalogue, the live
database — have diverged from the contract over time.


Extensions
----------

The core library (contract definition, data validation, and Cypher query
authoring/validation) depends only on Pydantic, PyYAML, and the ``graphglot``
Cypher parser. Database-specific functionality ships as optional extras:

===============  ====================================================================
Extra            What it adds
===============  ====================================================================
``neo4j``        ``inspect_neo4j`` — inspect a live Neo4j database into a
                 ``GraphProfile`` (APOC / SCHEMA / Cypher strategies,
                 auto-detected).
``memgraph``     ``inspect_memgraph`` — same interface, using Memgraph's schema
                 procedures over the Bolt driver.
``networkx``     ``inspect_networkx`` — in-process inspection of a
                 ``nx.MultiDiGraph``.
``cypher``       Backward-compat alias. ``graphglot`` is now a core dependency, so
                 query authoring and validation are always available; this extra
                 adds no new packages.
``gqlalchemy``   GQLAlchemy OGM integration: codegen of ``Node`` /
                 ``Relationship`` classes and validated fluent queries.
===============  ====================================================================


Installation
------------

Create a dedicated Python environment first (Python 3.11+):

.. code-block:: shell

   python -m venv .venv && source .venv/bin/activate

Install the core library:

.. code-block:: shell

   pip install orthograph

Install with a specific extra:

.. code-block:: shell

   pip install "orthograph[neo4j]"
   pip install "orthograph[memgraph]"
   pip install "orthograph[networkx]"
   pip install "orthograph[gqlalchemy]"

Install everything:

.. code-block:: shell

   pip install "orthograph[all]"

For development (all extras + test/lint/docs tooling):

.. code-block:: shell

   git clone <repo-url>
   cd orthograph
   pip install -e ".[dev]"


Quick start
-----------

Define the contract:

.. code-block:: python

   from typing import Optional

   from orthograph.definition import (
       GraphDefinition,
       NodeModel,
       RelationshipModel,
       validate_data,
   )

   class Person(NodeModel):
       __label__ = "Person"
       __uid_field__ = "name"
       name: str
       born: Optional[int] = None

   class Movie(NodeModel):
       __label__ = "Movie"
       __uid_field__ = "title"
       title: str
       year: int

   class ActedIn(RelationshipModel):
       __label__ = "ACTED_IN"
       __source_label__ = "Person"
       __target_label__ = "Movie"
       role: str

   definition = GraphDefinition(
       name="Filmography",
       node_types=[Person, Movie],
       relationship_types=[ActedIn],
   )

Validate in-memory data against the contract before writing to the database:

.. code-block:: python

   nodes = [
       {"__label__": "Person", "name": "Alice", "born": 1985},
       {"__label__": "Movie", "title": "Inception", "year": 2010},
   ]
   relationships = [
       {"__label__": "ACTED_IN", "__source_uid__": "Alice",
        "__target_uid__": "Inception", "role": "Lead"},
   ]

   result = validate_data(definition, nodes, relationships)
   print(result.is_valid)        # True / False
   for issue in result.issues:
       print(issue.code, issue.message)   # structured, typed error codes

Govern a typed Cypher query — declared parameters, validated against the
contract without executing it:

.. code-block:: python

   from pydantic import BaseModel

   from orthograph.queries import new_catalogue, simple_query, validate_catalogue

   class FindPersonParams(BaseModel):
       name: str

   catalogue = new_catalogue()
   catalogue.register_cypher_query(
       simple_query(
           name="find_person_by_name",
           cypher_template="MATCH (p:Person {name: $name}) RETURN p",
           params=FindPersonParams,
       )
   )

   # Drift detection: is the whole query set still consistent with the contract?
   drift = validate_catalogue(catalogue, definition)
   print(drift.is_valid)

Detect drift against a live database (requires the ``neo4j`` extra):

.. code-block:: python

   from neo4j import GraphDatabase

   from orthograph.compare import profile_to_definition
   from orthograph.profile import inspect_neo4j

   driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
   profile = inspect_neo4j(driver)
   result = profile_to_definition(profile, definition)

   print(result.is_valid)
   for issue in result.issues:
       print(issue.code, issue.message)


Contributing
------------

Setup, the full test matrix (unit, in-process integration, live Neo4j/Memgraph
flags, credential handling, and running the reference notebooks) are documented in
`CONTRIBUTING.md <CONTRIBUTING.md>`_.
