age-orm 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
age_orm-0.1.0/.envrc ADDED
@@ -0,0 +1,2 @@
1
+ export VIRTUAL_ENV="$PWD/.venv"
2
+ PATH_add "$VIRTUAL_ENV/bin"
age_orm-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Kashif Iftikhar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
age_orm-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: age-orm
3
+ Version: 0.1.0
4
+ Summary: A Python ORM for Apache AGE graph database
5
+ Project-URL: Homepage, https://age-forge.github.io/projects/age-orm
6
+ Project-URL: Documentation, https://age-forge.github.io/docs/
7
+ Project-URL: Repository, https://github.com/age-forge/age-orm
8
+ Project-URL: Issues, https://github.com/age-forge/age-orm/issues
9
+ Author-email: Kashif Iftikhar <kashif@compulife.com.pk>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Requires-Dist: psycopg[binary,pool]>=3.2
23
+ Requires-Dist: pydantic>=2.3.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: ipython>=9.10.0; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.8; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # age-orm
33
+
34
+ A Python ORM for Apache AGE, providing SQLAlchemy-like abstractions for graph database operations.
35
+
36
+ **Status:** v0.1.0 — Core implemented (models, CRUD, query builder, relationships, async)
37
+
38
+ ## Features
39
+
40
+ ### Core
41
+ - [x] Pydantic v2-based model definitions for vertices and edges
42
+ - [x] Graph, Vertex, and Edge abstractions
43
+ - [x] Automatic schema creation (labels, indexes)
44
+ - [x] CRUD operations (create, read, update, delete)
45
+ - [x] Connection pooling via psycopg3
46
+
47
+ ### Query Building
48
+ - [x] Fluent Cypher query builder (filter, sort, limit, skip)
49
+ - [x] Safe parameter substitution
50
+ - [x] Raw Cypher support
51
+ - [x] Bulk mutations (update/delete via query)
52
+
53
+ ### Advanced
54
+ - [x] Sync + Async support (psycopg3's unified API)
55
+ - [x] Relationship descriptors with lazy loading
56
+ - [x] Event system (pre/post hooks for add, update, delete)
57
+ - [x] Bulk import (direct SQL INSERT for performance)
58
+ - [x] Graph traversal helpers (expand, traverse)
59
+ - [x] Dirty tracking (only update changed fields)
60
+ - [ ] Migrations and schema versioning
61
+ - [ ] Integration with AGEFreighter for bulk loads
62
+
63
+ ## Installation
64
+
65
+ ```bash
66
+ # With uv
67
+ uv add age-orm
68
+
69
+ # With pip
70
+ pip install age-orm
71
+ ```
72
+
73
+ Requires Python 3.12+ and a running Apache AGE instance.
74
+
75
+ ## Quick Start
76
+
77
+ ```python
78
+ from age_orm import Database, Vertex, Edge, relationship
79
+
80
+ # Define models
81
+ class Person(Vertex):
82
+ __label__ = "Person"
83
+ name: str
84
+ age: int
85
+ email: str | None = None
86
+
87
+ class Knows(Edge):
88
+ __label__ = "KNOWS"
89
+ since: int
90
+ relationship_type: str = "friend"
91
+
92
+ # Connect
93
+ db = Database("postgresql://ageuser:agepassword@localhost:5433/agedb")
94
+ graph = db.graph("social", create=True)
95
+
96
+ # Create vertices
97
+ alice = Person(name="Alice", age=30)
98
+ bob = Person(name="Bob", age=25)
99
+ graph.add(alice)
100
+ graph.add(bob)
101
+
102
+ # Create edge
103
+ knows = Knows(since=2020)
104
+ graph.connect(alice, knows, bob)
105
+
106
+ # Query
107
+ people = graph.query(Person).filter("n.age > $min_age", min_age=20).sort("n.name").all()
108
+ alice = graph.query(Person).filter_by(name="Alice").one()
109
+
110
+ # Traverse
111
+ friends = graph.traverse(alice, "KNOWS", depth=2, target_class=Person)
112
+
113
+ # Raw Cypher
114
+ results = graph.cypher(
115
+ "MATCH (n:Person)-[:KNOWS]->(m) RETURN n.name, m.name",
116
+ columns=["a", "b"]
117
+ )
118
+
119
+ # Cleanup
120
+ db.close()
121
+ ```
122
+
123
+ ## Async Usage
124
+
125
+ ```python
126
+ from age_orm import AsyncDatabase
127
+
128
+ async with AsyncDatabase("postgresql://...") as db:
129
+ graph = await db.graph("social", create=True)
130
+ alice = Person(name="Alice", age=30)
131
+ await graph.add(alice)
132
+
133
+ q = await graph.query(Person)
134
+ people = await q.filter("n.age > $min", min=20).all()
135
+ ```
136
+
137
+ ## Relationships
138
+
139
+ ```python
140
+ class Person(Vertex):
141
+ __label__ = "Person"
142
+ name: str
143
+ friends: list["Person"] = relationship("Person", "KNOWS", direction="outbound")
144
+ employer: "Company" = relationship("Company", "WORKS_AT", uselist=False)
145
+ ```
146
+
147
+ Relationships are lazy-loaded on access when the entity is bound to a graph.
148
+
149
+ ## Event Hooks
150
+
151
+ ```python
152
+ from age_orm import listen, listens_for
153
+
154
+ @listens_for(Person, "pre_add")
155
+ def validate_person(target, event, **kwargs):
156
+ if target.age < 0:
157
+ raise ValueError("Age cannot be negative")
158
+ ```
159
+
160
+ ## Dependencies
161
+
162
+ - `psycopg[binary,pool] >= 3.2` — PostgreSQL driver with connection pooling
163
+ - `pydantic >= 2.3` — Data validation and models
164
+
165
+ ## Project Structure
166
+
167
+ ```
168
+ age_orm/
169
+ ├── __init__.py # Public API exports
170
+ ├── exceptions.py # Custom exceptions
171
+ ├── event.py # Event system (pre/post hooks)
172
+ ├── database.py # Connection + pool management
173
+ ├── graph.py # Graph class + CRUD + traversal
174
+ ├── references.py # Relationship descriptors
175
+ ├── models/
176
+ │ ├── base.py # AgeModel base class
177
+ │ ├── vertex.py # Vertex model
178
+ │ └── edge.py # Edge model
179
+ ├── query/
180
+ │ └── builder.py # Cypher query builder
181
+ └── utils/
182
+ └── serialization.py # Agtype serialization helpers
183
+ ```
184
+
185
+ ## License
186
+
187
+ MIT
@@ -0,0 +1,156 @@
1
+ # age-orm
2
+
3
+ A Python ORM for Apache AGE, providing SQLAlchemy-like abstractions for graph database operations.
4
+
5
+ **Status:** v0.1.0 — Core implemented (models, CRUD, query builder, relationships, async)
6
+
7
+ ## Features
8
+
9
+ ### Core
10
+ - [x] Pydantic v2-based model definitions for vertices and edges
11
+ - [x] Graph, Vertex, and Edge abstractions
12
+ - [x] Automatic schema creation (labels, indexes)
13
+ - [x] CRUD operations (create, read, update, delete)
14
+ - [x] Connection pooling via psycopg3
15
+
16
+ ### Query Building
17
+ - [x] Fluent Cypher query builder (filter, sort, limit, skip)
18
+ - [x] Safe parameter substitution
19
+ - [x] Raw Cypher support
20
+ - [x] Bulk mutations (update/delete via query)
21
+
22
+ ### Advanced
23
+ - [x] Sync + Async support (psycopg3's unified API)
24
+ - [x] Relationship descriptors with lazy loading
25
+ - [x] Event system (pre/post hooks for add, update, delete)
26
+ - [x] Bulk import (direct SQL INSERT for performance)
27
+ - [x] Graph traversal helpers (expand, traverse)
28
+ - [x] Dirty tracking (only update changed fields)
29
+ - [ ] Migrations and schema versioning
30
+ - [ ] Integration with AGEFreighter for bulk loads
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ # With uv
36
+ uv add age-orm
37
+
38
+ # With pip
39
+ pip install age-orm
40
+ ```
41
+
42
+ Requires Python 3.12+ and a running Apache AGE instance.
43
+
44
+ ## Quick Start
45
+
46
+ ```python
47
+ from age_orm import Database, Vertex, Edge, relationship
48
+
49
+ # Define models
50
+ class Person(Vertex):
51
+ __label__ = "Person"
52
+ name: str
53
+ age: int
54
+ email: str | None = None
55
+
56
+ class Knows(Edge):
57
+ __label__ = "KNOWS"
58
+ since: int
59
+ relationship_type: str = "friend"
60
+
61
+ # Connect
62
+ db = Database("postgresql://ageuser:agepassword@localhost:5433/agedb")
63
+ graph = db.graph("social", create=True)
64
+
65
+ # Create vertices
66
+ alice = Person(name="Alice", age=30)
67
+ bob = Person(name="Bob", age=25)
68
+ graph.add(alice)
69
+ graph.add(bob)
70
+
71
+ # Create edge
72
+ knows = Knows(since=2020)
73
+ graph.connect(alice, knows, bob)
74
+
75
+ # Query
76
+ people = graph.query(Person).filter("n.age > $min_age", min_age=20).sort("n.name").all()
77
+ alice = graph.query(Person).filter_by(name="Alice").one()
78
+
79
+ # Traverse
80
+ friends = graph.traverse(alice, "KNOWS", depth=2, target_class=Person)
81
+
82
+ # Raw Cypher
83
+ results = graph.cypher(
84
+ "MATCH (n:Person)-[:KNOWS]->(m) RETURN n.name, m.name",
85
+ columns=["a", "b"]
86
+ )
87
+
88
+ # Cleanup
89
+ db.close()
90
+ ```
91
+
92
+ ## Async Usage
93
+
94
+ ```python
95
+ from age_orm import AsyncDatabase
96
+
97
+ async with AsyncDatabase("postgresql://...") as db:
98
+ graph = await db.graph("social", create=True)
99
+ alice = Person(name="Alice", age=30)
100
+ await graph.add(alice)
101
+
102
+ q = await graph.query(Person)
103
+ people = await q.filter("n.age > $min", min=20).all()
104
+ ```
105
+
106
+ ## Relationships
107
+
108
+ ```python
109
+ class Person(Vertex):
110
+ __label__ = "Person"
111
+ name: str
112
+ friends: list["Person"] = relationship("Person", "KNOWS", direction="outbound")
113
+ employer: "Company" = relationship("Company", "WORKS_AT", uselist=False)
114
+ ```
115
+
116
+ Relationships are lazy-loaded on access when the entity is bound to a graph.
117
+
118
+ ## Event Hooks
119
+
120
+ ```python
121
+ from age_orm import listen, listens_for
122
+
123
+ @listens_for(Person, "pre_add")
124
+ def validate_person(target, event, **kwargs):
125
+ if target.age < 0:
126
+ raise ValueError("Age cannot be negative")
127
+ ```
128
+
129
+ ## Dependencies
130
+
131
+ - `psycopg[binary,pool] >= 3.2` — PostgreSQL driver with connection pooling
132
+ - `pydantic >= 2.3` — Data validation and models
133
+
134
+ ## Project Structure
135
+
136
+ ```
137
+ age_orm/
138
+ ├── __init__.py # Public API exports
139
+ ├── exceptions.py # Custom exceptions
140
+ ├── event.py # Event system (pre/post hooks)
141
+ ├── database.py # Connection + pool management
142
+ ├── graph.py # Graph class + CRUD + traversal
143
+ ├── references.py # Relationship descriptors
144
+ ├── models/
145
+ │ ├── base.py # AgeModel base class
146
+ │ ├── vertex.py # Vertex model
147
+ │ └── edge.py # Edge model
148
+ ├── query/
149
+ │ └── builder.py # Cypher query builder
150
+ └── utils/
151
+ └── serialization.py # Agtype serialization helpers
152
+ ```
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,24 @@
1
+ """age-orm: A Python ORM for Apache AGE graph database."""
2
+
3
+ from .models import Vertex, Edge
4
+ from .graph import Graph, AsyncGraph
5
+ from .database import Database, AsyncDatabase
6
+ from .query import Query, AsyncQuery
7
+ from .references import relationship
8
+ from .event import listen, listens_for
9
+
10
+ __version__ = "0.1.0"
11
+
12
+ __all__ = [
13
+ "Vertex",
14
+ "Edge",
15
+ "Graph",
16
+ "AsyncGraph",
17
+ "Database",
18
+ "AsyncDatabase",
19
+ "Query",
20
+ "AsyncQuery",
21
+ "relationship",
22
+ "listen",
23
+ "listens_for",
24
+ ]
@@ -0,0 +1,201 @@
1
+ """Database connection and pool management for Apache AGE."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ from psycopg import Connection
9
+ from psycopg_pool import ConnectionPool, AsyncConnectionPool
10
+
11
+ from age_orm.exceptions import GraphNotFoundError, GraphExistsError
12
+
13
+ if TYPE_CHECKING:
14
+ from age_orm.graph import Graph, AsyncGraph
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ def _configure_age_connection(conn: Connection) -> None:
20
+ """Configure a connection for AGE: load extension and set search path.
21
+
22
+ Uses autocommit to avoid leaving the connection in INTRANS state,
23
+ which would cause psycopg_pool to discard the connection.
24
+ """
25
+ conn.autocommit = True
26
+ conn.execute("LOAD 'age'")
27
+ conn.execute('SET search_path = ag_catalog, "$user", public')
28
+ conn.autocommit = False
29
+
30
+
31
+ class Database:
32
+ """Synchronous database connection manager for Apache AGE.
33
+
34
+ Manages a connection pool and provides graph-level operations.
35
+
36
+ Usage:
37
+ db = Database("postgresql://user:pass@localhost:5433/agedb")
38
+ graph = db.graph("my_graph", create=True)
39
+ # ... use graph ...
40
+ db.close()
41
+ """
42
+
43
+ def __init__(self, dsn: str, **pool_kwargs):
44
+ self._dsn = dsn
45
+ pool_kwargs.setdefault("min_size", 1)
46
+ pool_kwargs.setdefault("max_size", 10)
47
+ self._pool = ConnectionPool(
48
+ dsn,
49
+ configure=_configure_age_connection,
50
+ **pool_kwargs,
51
+ )
52
+
53
+ def graph(self, name: str, create: bool = False) -> "Graph":
54
+ """Get a Graph handle for the named graph.
55
+
56
+ Args:
57
+ name: The graph name.
58
+ create: If True, create the graph if it doesn't exist.
59
+ """
60
+ from age_orm.graph import Graph
61
+
62
+ if create and not self.graph_exists(name):
63
+ return self.create_graph(name)
64
+
65
+ if not self.graph_exists(name):
66
+ raise GraphNotFoundError(f"Graph '{name}' does not exist")
67
+
68
+ return Graph(name=name, db=self)
69
+
70
+ def create_graph(self, name: str) -> "Graph":
71
+ """Create a new graph and return a Graph handle."""
72
+ from age_orm.graph import Graph
73
+
74
+ if self.graph_exists(name):
75
+ raise GraphExistsError(f"Graph '{name}' already exists")
76
+
77
+ with self._pool.connection() as conn:
78
+ conn.execute("SELECT create_graph(%s)", (name,))
79
+ log.info("Created graph: %s", name)
80
+ return Graph(name=name, db=self)
81
+
82
+ def drop_graph(self, name: str, cascade: bool = True) -> None:
83
+ """Drop a graph."""
84
+ if not self.graph_exists(name):
85
+ raise GraphNotFoundError(f"Graph '{name}' does not exist")
86
+
87
+ with self._pool.connection() as conn:
88
+ conn.execute("SELECT drop_graph(%s, %s)", (name, cascade))
89
+ log.info("Dropped graph: %s", name)
90
+
91
+ def graph_exists(self, name: str) -> bool:
92
+ """Check if a graph exists."""
93
+ with self._pool.connection() as conn:
94
+ result = conn.execute(
95
+ "SELECT 1 FROM ag_catalog.ag_graph WHERE name = %s", (name,)
96
+ ).fetchone()
97
+ return result is not None
98
+
99
+ def list_graphs(self) -> list[str]:
100
+ """List all graph names."""
101
+ with self._pool.connection() as conn:
102
+ rows = conn.execute(
103
+ "SELECT name FROM ag_catalog.ag_graph"
104
+ ).fetchall()
105
+ return [row[0] for row in rows]
106
+
107
+ def close(self) -> None:
108
+ """Close the connection pool."""
109
+ self._pool.close()
110
+
111
+ def __enter__(self) -> "Database":
112
+ return self
113
+
114
+ def __exit__(self, *args) -> None:
115
+ self.close()
116
+
117
+
118
+ class AsyncDatabase:
119
+ """Asynchronous database connection manager for Apache AGE.
120
+
121
+ Usage:
122
+ async with AsyncDatabase("postgresql://...") as db:
123
+ graph = await db.graph("my_graph", create=True)
124
+ """
125
+
126
+ def __init__(self, dsn: str, **pool_kwargs):
127
+ self._dsn = dsn
128
+ pool_kwargs.setdefault("min_size", 1)
129
+ pool_kwargs.setdefault("max_size", 10)
130
+ self._pool = AsyncConnectionPool(
131
+ dsn,
132
+ configure=self._configure_connection,
133
+ **pool_kwargs,
134
+ )
135
+
136
+ @staticmethod
137
+ async def _configure_connection(conn) -> None:
138
+ """Configure a connection for AGE (async version)."""
139
+ await conn.set_autocommit(True)
140
+ await conn.execute("LOAD 'age'")
141
+ await conn.execute('SET search_path = ag_catalog, "$user", public')
142
+ await conn.set_autocommit(False)
143
+
144
+ async def graph(self, name: str, create: bool = False) -> "AsyncGraph":
145
+ """Get an AsyncGraph handle for the named graph."""
146
+ from age_orm.graph import AsyncGraph
147
+
148
+ if create and not await self.graph_exists(name):
149
+ return await self.create_graph(name)
150
+
151
+ if not await self.graph_exists(name):
152
+ raise GraphNotFoundError(f"Graph '{name}' does not exist")
153
+
154
+ return AsyncGraph(name=name, db=self)
155
+
156
+ async def create_graph(self, name: str) -> "AsyncGraph":
157
+ """Create a new graph and return an AsyncGraph handle."""
158
+ from age_orm.graph import AsyncGraph
159
+
160
+ if await self.graph_exists(name):
161
+ raise GraphExistsError(f"Graph '{name}' already exists")
162
+
163
+ async with self._pool.connection() as conn:
164
+ await conn.execute("SELECT create_graph(%s)", (name,))
165
+ log.info("Created graph: %s", name)
166
+ return AsyncGraph(name=name, db=self)
167
+
168
+ async def drop_graph(self, name: str, cascade: bool = True) -> None:
169
+ """Drop a graph."""
170
+ if not await self.graph_exists(name):
171
+ raise GraphNotFoundError(f"Graph '{name}' does not exist")
172
+
173
+ async with self._pool.connection() as conn:
174
+ await conn.execute("SELECT drop_graph(%s, %s)", (name, cascade))
175
+ log.info("Dropped graph: %s", name)
176
+
177
+ async def graph_exists(self, name: str) -> bool:
178
+ """Check if a graph exists."""
179
+ async with self._pool.connection() as conn:
180
+ result = await conn.execute(
181
+ "SELECT 1 FROM ag_catalog.ag_graph WHERE name = %s", (name,)
182
+ )
183
+ row = await result.fetchone()
184
+ return row is not None
185
+
186
+ async def list_graphs(self) -> list[str]:
187
+ """List all graph names."""
188
+ async with self._pool.connection() as conn:
189
+ result = await conn.execute("SELECT name FROM ag_catalog.ag_graph")
190
+ rows = await result.fetchall()
191
+ return [row[0] for row in rows]
192
+
193
+ async def close(self) -> None:
194
+ """Close the connection pool."""
195
+ await self._pool.close()
196
+
197
+ async def __aenter__(self) -> "AsyncDatabase":
198
+ return self
199
+
200
+ async def __aexit__(self, *args) -> None:
201
+ await self.close()
@@ -0,0 +1,31 @@
1
+ """Event system for pre/post hooks on graph operations."""
2
+
3
+ from collections import defaultdict
4
+
5
+ _registrars: dict = defaultdict(lambda: defaultdict(list))
6
+
7
+
8
+ def dispatch(target, event: str, *args, **kwargs):
9
+ """Fire given event for all registered handlers matching the target's type."""
10
+ by_event = _registrars[event]
11
+ for target_class in by_event:
12
+ if isinstance(target, target_class):
13
+ for fn in by_event[target_class]:
14
+ fn(target, event, *args, **kwargs)
15
+
16
+
17
+ def listen(target, event: str | list[str], fn):
18
+ """Register fn to listen for event(s) on target class."""
19
+ events = [event] if isinstance(event, str) else event
20
+ for ev in events:
21
+ _registrars[ev][target].append(fn)
22
+
23
+
24
+ def listens_for(target, event: str | list[str]):
25
+ """Decorator to register fn to listen for event(s) on target class."""
26
+
27
+ def decorator(fn):
28
+ listen(target, event, fn)
29
+ return fn
30
+
31
+ return decorator
@@ -0,0 +1,29 @@
1
+ """Custom exceptions for age-orm."""
2
+
3
+
4
+ class AgeORMError(Exception):
5
+ """Base exception for all age-orm errors."""
6
+
7
+
8
+ class GraphNotFoundError(AgeORMError):
9
+ """Raised when a graph does not exist."""
10
+
11
+
12
+ class GraphExistsError(AgeORMError):
13
+ """Raised when trying to create a graph that already exists."""
14
+
15
+
16
+ class LabelNotFoundError(AgeORMError):
17
+ """Raised when a vertex/edge label does not exist."""
18
+
19
+
20
+ class DetachedInstanceError(AgeORMError):
21
+ """Raised when accessing a relationship on an entity not bound to a database."""
22
+
23
+
24
+ class EntityNotFoundError(AgeORMError):
25
+ """Raised when an entity lookup returns no results."""
26
+
27
+
28
+ class MultipleResultsError(AgeORMError):
29
+ """Raised when a single-result query returns multiple results."""