# Welcome to Arango ORM (Python ORM Layer For ArangoDB)
**arango_orm** is a python ORM layer inspired by SQLAlchemy but aimed to work
with the multi-model database [ArangoDB](https://www.arangodb.com). It supports accessing both collections
and graphs using the ORM. The actual communication with the database is done
using [python-arango](https://docs.python-arango.com) (the database driver for accessing arangodb from
python) and object serialization, validation, etc is handled by [pydantic](https://docs.pydantic.dev/latest/).
## Installation
```shell
pip install arango-orm
```
## Connecting to a Database
```python
from arango import ArangoClient
from arango_orm import Database
client = ArangoClient(hosts='http://localhost:8529')
test_db = client.db('test', username='test', password='test')
db = Database(test_db)
```
## Define a Collection
```python
from datetime import date
from pydantic import Field
from arango_orm import Collection
class Student(Collection):
__collection__ = 'students'
name: str
dob: date
```
## Create Collection in the Database
```python
db.create_collection(Student)
```
## Drop a Collection
```python
db.drop_collection(Student)
```
## Check if a collection exists
```python
db.has_collection(Student)
db.has_collection('students')
```
## Add Records
```python
from datetime import date
s = Student(name='test', _key='12312', dob=date(year=2016, month=9, day=12))
db.add(s)
print(s._id) # students/12312
```
## Get Total Records in the Collection
```python
db.query(Student).count()
```
## Get Record By Key
```python
s = db.query(Student).by_key('12312')
```
## Update a Record
```python
s = db.query(Student).by_key('12312')
s.name = 'Anonymous'
db.update(s)
```
## Delete a Record
```python
s = db.query(Student).by_key('12312')
db.delete(s)
```
## Get All Records in a Collection
```python
students = db.query(Student).all()
```
## Get First Record Matching the Query
```python
first_student = db.query(Student).first()
```
## Filter Records
Using bind parameters (recommended)
```python
records = db.query(Student).filter("name==@name", name='Anonymous').all()
```
Using plain condition strings (not safe in case of unsanitized user supplied input)
```python
records = db.query(Student).filter("name=='Anonymous'").all()
```
## Filter Using OR
```python
# Get all documents where student name starts with A or B
records = db.query(Student).filter(
"LIKE(rec.name, 'A%')", prepend_rec_name=False).filter(
"LIKE(rec.name, 'B%')", prepend_rec_name=False, _or=True).all()
```
## Filter, Sort and Limit
```python
# Last 5 students with names starting with A
records = db.query(Student).filter(
"LIKE(rec.name, 'A%')", prepend_rec_name=False).sort("name DESC").limit(5).all()
# Query students with pagination (limit&offset)
page_num, per_page = 2, 10
page = db.query(Student).sort("name DESC").limit(per_page, start_from=(page_num - 1) * per_page)
```
## Fetch Only Some Fields
```python
c = db.query(Student).limit(2).returns('_key', 'name').first()
```
## Update Multiple Records
```python
db.query(Student).filter("name==@name", name='Anonymous').update(name='Mr. Anonymous')
```
## Delete Multiple Records
```python
db.query(Student).filter("LIKE(rec.name, 'test%')", prepend_rec_name=False).delete()
```
## Delete All Records
```python
db.query(Student).delete()
```
## Bulk Create Records
```python
s1 = Student(name='test1', _key='12345', dob=date(year=2016, month=9, day=12))
s2 = Student(name='test2', _key='22346', dob=date(year=2015, month=9, day=12)
car1 = Car(make="Honda", model="Fiat", year=2010)
car2 = Car(make="Honda", model="Skoda", year=2015)
db.bulk_add(entity_list=[p_ref_10, p_ref_11, car1, car2])
```
## Bulk Update Records
```python
p_ref1 = db.query(Person).by_key("12312")
p_ref2 = db.query(Person).by_key("12345")
p_ref1.name = "Bruce"
p_ref2.name = "Eliza"
db.bulk_update(entity_list=[p_ref1, p_ref2])
```
## Query Using AQL
```python
db.add(Student(name='test1', _key='12345', dob=date(year=2016, month=9, day=12)))
db.add(Student(name='test2', _key='22346', dob=date(year=2015, month=9, day=12)))
students = [Student._load(s) for s in db.aql.execute("FOR st IN students RETURN st")]
```
## Reference Fields
Reference fields allow linking documents from another collection class within a collection instance.
These are similar in functionality to SQLAlchemy's relationship function.
```python
from arango import ArangoClient
from arango_orm.database import Database
from arango_orm.fields import String
from arango_orm import Collection, Relation, Graph, GraphConnection
from arango_orm.references import relationship, graph_relationship
class Person(Collection):
__collection__ = 'persons'
_index = [{'type': 'hash', 'unique': False, 'fields': ['name']}]
_allow_extra_fields = False # prevent extra properties from saving into DB
_key = String(required=True)
name = String(required=True, allow_none=False)
cars = relationship(__name__ + ".Car", '_key', target_field='owner_key')
def __str__(self):
return "<Person(" + self.name + ")>"
class Car(Collection):
__collection__ = 'cars'
_allow_extra_fields = True
make = String(required=True)
model = String(required=True)
year = Integer(required=True)
owner_key = String()
owner = relationship(Person, 'owner_key', cache=False)
def __str__(self):
return "<Car({} - {} - {})>".format(self.make, self.model, self.year)
client = ArangoClient(hosts='http://localhost:8529')
test_db = client.db('test', username='test', password='test')
db = Database(test_db)
p = Person(_key='kashif', name='Kashif Iftikhar')
db.add(p)
p2 = Person(_key='azeen', name='Azeen Kashif')
db.add(p2)
c1 = Car(make='Honda', model='Civic', year=1984, owner_key='kashif')
db.add(c1)
c2 = Car(make='Mitsubishi', model='Lancer', year=2005, owner_key='kashif')
db.add(c2)
c3 = Car(make='Acme', model='Toy Racer', year=2016, owner_key='azeen')
db.add(c3)
print(c1.owner)
print(c1.owner.name)
print(c2.owner.name)
print(c3.owner.name)
print(p.cars)
print(p.cars[0].make)
print(p2.cars)
```
## Working With Graphs
Working with graphs involves creating collection classes and optionally Edge/Relation classes. Users can use the built-in Relation class for specifying relations but if relations need to contain extra attributes then it's required to create a sub-class of Relation class. Graph functionality is explain below with the help of a university graph example containing students, teachers, subjects and the areas where students and teachers reside in.
First we create some collections and relationships
```python
from typing import Literal
from arango_orm import Collection, Relation, Graph, GraphConnection
class Student(Collection):
__collection__ = 'students'
name: str
age: int | None = None
def __str__(self):
return "<Student({})>".format(self.name)
class Teacher(Collection):
__collection__ = 'teachers'
name: str
def __str__(self):
return "<Teacher({})>".format(self.name)
class Subject(Collection):
__collection__ = 'subjects'
name: str
credit_hours: int
has_labs: bool = True
def __str__(self):
return "<Subject({})>".format(self.name)
class Area(Collection):
__collection__ = 'areas'
class SpecializesIn(Relation):
__collection__ = 'specializes_in'
expertise_level: Literal["expert", "medium", "basic"]
def __str__(self):
return "<SpecializesIn(_key={}, expertise_level={}, _from={}, _to={})>".format(
self.key_, self.expertise_level, self._from, self._to)
```
Next we sub-class the Graph class to specify the relationships between the various collections
```python
class UniversityGraph(Graph):
__graph__ = 'university_graph'
graph_connections = [
# Using general Relation class for relationship
GraphConnection(Student, Relation("studies"), Subject),
GraphConnection(Teacher, Relation("teaches"), Subject),
# Using specific classes for vertex and edges
GraphConnection(Teacher, SpecializesIn, Subject),
GraphConnection([Teacher, Student], Relation("resides_in"), Area)
]
```
Now it's time to create the graph. Note that we don't need to create the collections individually, creating the graph will create all collections that it contains
```python
from arango import ArangoClient
from arango_orm.database import Database
client = ArangoClient(hosts='http://localhost:8529')
test_db = client.db('test', username='test', password='test')
db = Database(test_db)
uni_graph = UniversityGraph(connection=db)
db.create_graph(uni_graph)
```
Now the graph and all it's collections have been created, we can verify their existence:
```python
[c['name'] for c in db.collections()]
db.graphs()
```
Now let's insert some data into our graph:
```python
students_data = [
Student(_key='S1001', name='John Wayne', age=30),
Student(_key='S1002', name='Lilly Parker', age=22),
Student(_key='S1003', name='Cassandra Nix', age=25),
Student(_key='S1004', name='Peter Parker', age=20)
]
teachers_data = [
Teacher(_key='T001', name='Bruce Wayne'),
Teacher(_key='T002', name='Barry Allen'),
Teacher(_key='T003', name='Amanda Waller')
]
subjects_data = [
Subject(_key='ITP101', name='Introduction to Programming', credit_hours=4, has_labs=True),
Subject(_key='CS102', name='Computer History', credit_hours=3, has_labs=False),
Subject(_key='CSOOP02', name='Object Oriented Programming', credit_hours=3, has_labs=True),
]
areas_data = [
Area(_key="Gotham"),
Area(_key="Metropolis"),
Area(_key="StarCity")
]
for s in students_data:
db.add(s)
for t in teachers_data:
db.add(t)
for s in subjects_data:
db.add(s)
for a in areas_data:
db.add(a)
```
Next let's add some relations, we can add relations by manually adding the relation/edge record into the edge collection, like:
```python
db.add(SpecializesIn(_from="teachers/T001", _to="subjects/ITP101", expertise_level="medium"))
```
Or we can use the graph object's relation method to generate a relation document from given objects:
```python
gotham = db.query(Area).by_key("Gotham")
metropolis = db.query(Area).by_key("Metropolis")
star_city = db.query(Area).by_key("StarCity")
john_wayne = db.query(Student).by_key("S1001")
lilly_parker = db.query(Student).by_key("S1002")
cassandra_nix = db.query(Student).by_key("S1003")
peter_parker = db.query(Student).by_key("S1004")
intro_to_prog = db.query(Subject).by_key("ITP101")
comp_history = db.query(Subject).by_key("CS102")
oop = db.query(Subject).by_key("CSOOP02")
barry_allen = db.query(Teacher).by_key("T002")
bruce_wayne = db.query(Teacher).by_key("T001")
amanda_waller = db.query(Teacher).by_key("T003")
db.add(uni_graph.relation(peter_parker, Relation("studies"), oop))
db.add(uni_graph.relation(peter_parker, Relation("studies"), intro_to_prog))
db.add(uni_graph.relation(john_wayne, Relation("studies"), oop))
db.add(uni_graph.relation(john_wayne, Relation("studies"), comp_history))
db.add(uni_graph.relation(lilly_parker, Relation("studies"), intro_to_prog))
db.add(uni_graph.relation(lilly_parker, Relation("studies"), comp_history))
db.add(uni_graph.relation(cassandra_nix, Relation("studies"), oop))
db.add(uni_graph.relation(cassandra_nix, Relation("studies"), intro_to_prog))
db.add(uni_graph.relation(barry_allen, SpecializesIn(expertise_level="expert"), oop))
db.add(uni_graph.relation(barry_allen, SpecializesIn(expertise_level="expert"), intro_to_prog))
db.add(uni_graph.relation(bruce_wayne, SpecializesIn(expertise_level="medium"), oop))
db.add(uni_graph.relation(bruce_wayne, SpecializesIn(expertise_level="expert"), comp_history))
db.add(uni_graph.relation(amanda_waller, SpecializesIn(expertise_level="basic"), intro_to_prog))
db.add(uni_graph.relation(amanda_waller, SpecializesIn(expertise_level="medium"), comp_history))
db.add(uni_graph.relation(bruce_wayne, Relation("teaches"), oop))
db.add(uni_graph.relation(barry_allen, Relation("teaches"), intro_to_prog))
db.add(uni_graph.relation(amanda_waller, Relation("teaches"), comp_history))
db.add(uni_graph.relation(bruce_wayne, Relation("resides_in"), gotham))
db.add(uni_graph.relation(barry_allen, Relation("resides_in"), star_city))
db.add(uni_graph.relation(amanda_waller, Relation("resides_in"), metropolis))
db.add(uni_graph.relation(john_wayne, Relation("resides_in"), gotham))
db.add(uni_graph.relation(lilly_parker, Relation("resides_in"), metropolis))
db.add(uni_graph.relation(cassandra_nix, Relation("resides_in"), star_city))
db.add(uni_graph.relation(peter_parker, Relation("resides_in"), metropolis))
```
With our graph populated with some sample data, let's explore the ways we can work with the graph.
### Expanding Documents
We can expand any Collection (not Relation) object to access the data that is linked to it. We can sepcify which links ('inbound', 'outbound', 'any') to expand and the depth to which those should be expanded to. Let's see all immediate connections that Bruce Wayne has in our graph:
```python
bruce = db.query(Teacher).by_key("T001")
uni_graph.expand(bruce, depth=1, direction='any')
```
Graph expansion on an object adds a `_relations` dictionary that contains all the relations for the object according to the expansion criteria:
```python
bruce._relations
# Returns:
{
'resides_in': [<Relation(_key=4205290, _from=teachers/T001, _to=areas/Gotham)>],
'specializes_in': [<SpecializesIn(_key=4205114, expertise_level=medium, _from=teachers/T001, _to=subjects/ITP101)>,
<SpecializesIn(_key=4205271, expertise_level=expert, _from=teachers/T001, _to=subjects/CS102)>,
<SpecializesIn(_key=4205268, expertise_level=medium, _from=teachers/T001, _to=subjects/CSOOP02)>],
'teaches': [<Relation(_key=4205280, _from=teachers/T001, _to=subjects/CSOOP02)>]
}
```
We can use _from and _to of a relation object to access the id's for both sides of the link. We also have _object_from and _object_to to access the objects on both sides, for example:
```python
bruce._relations['resides_in'][0]._object_from.name
# 'Bruce Wayne'
bruce._relations['resides_in'][0]._object_to._key
# 'Gotham'
```
There is also a special attribute called `_next` that allows accessing the other side of the relationship irrespective of the relationship direction. For example, for outbound relationships the `_object_from` contains the source object while for inbound_relationships `_object_to` contains the source object. But if we're only interested in traversal of the graph then it's more useful at times to access the other side of the relationship w.r.t the current object irrespective of it's direction:
```python
bruce._relations['resides_in'][0]._next._key
# 'Gotham'
```
Let's expand the bruce object to 2 levels and see `_next` in more action:
```python
uni_graph.expand(bruce, depth=2)
# All relations of the area where bruce resides in
bruce._relations['resides_in'][0]._object_to._relations
# -> {'resides_in': [<Relation(_key=4205300, _from=students/S1001, _to=areas/Gotham)>]}
# Name of the student that resides in the same area as bruce
bruce._relations['resides_in'][0]._object_to._relations['resides_in'][0]._object_from.name
# 'John Wayne'
# The same action using _next without worrying about direction
bruce._relations['resides_in'][0]._next._relations['resides_in'][0]._next.name
# 'John Wayne'
# Get names of all people that reside in the same area and Bruce Wayne
[p._next.name for p in bruce._relations['resides_in'][0]._next._relations['resides_in']]
# ['John Wayne']
```
## Inheritance Mapping
For inheritance mapping, **arango_orm** offers you two ways to define it.
### 1. Discriminator field/mapping:
Discriminator field/mapping are defined at entity level:
```python
class Vehicle(Collection):
__collection__ = "vehicle"
_inheritance_field = "discr"
_inheritance_mapping = {
'Bike': 'moto',
'Truck': 'truck'
}
_key = String()
brand = String()
model = String()
# discr field match what you defined in _inheritance_field
# the field type depends on the values of your _inheritance_mapping
discr = String(required=True)
class Bike(Vehicle):
motor_size = Float()
class Truck(Vehicle):
traction_power = Float()
```
### 2. Inheritance mapping resolver:
The `inheritance_mapping_resolver` is a function defined at graph level; it allows you to make either a simple test
on a discriminator field, or complex inference
```python
class OwnershipGraph2(Graph):
__graph__ = "ownership_graph"
graph_connections = [
GraphConnection(Owner2, Own2, Vehicle2)
]
def inheritance_mapping_resolver(self, col_name: str, doc_dict: dict = {}):
if col_name == 'vehicle':
if 'traction_power' in doc_dict:
return Truck2
else:
return Bike2
return self.vertices[col_name]
```
## Graph Traversal Using AQL
The graph module also supports traversals using AQL, the results are converted to objects and have the
same structure as graph.expand method:
```python
obj = uni_graph.aql("FOR v, e, p IN 1..2 INBOUND 'areas/Gotham' GRAPH 'university_graph' RETURN p")
print(obj._key)
# Gotham
gotham_residents = [rel._next.name for rel in obj._relations['resides_in']]
print(gotham_residents)
# ['Bruce Wayne', 'John Wayne']
```
# For Developers
## Running the Test Cases
Set env variables in .env file, then load the env
```shell
set -a; source .env; set +a
```
Afterwards run tests
```
poetry run pytest
```
Raw data
{
"_id": null,
"home_page": "https://arango-orm.compulife.com.pk",
"name": "arango-orm",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.11",
"maintainer_email": null,
"keywords": null,
"author": "Kashif Iftikhar",
"author_email": "kashif@compulife.com.pk",
"download_url": "https://files.pythonhosted.org/packages/a5/df/4de504d0c72db24c7d94b8f595e7beabdf54058e3c8411d6504bbe1bd843/arango_orm-1.1.0.tar.gz",
"platform": null,
"description": "# Welcome to Arango ORM (Python ORM Layer For ArangoDB)\n\n**arango_orm** is a python ORM layer inspired by SQLAlchemy but aimed to work\nwith the multi-model database [ArangoDB](https://www.arangodb.com). It supports accessing both collections\nand graphs using the ORM. The actual communication with the database is done\nusing [python-arango](https://docs.python-arango.com) (the database driver for accessing arangodb from\npython) and object serialization, validation, etc is handled by [pydantic](https://docs.pydantic.dev/latest/).\n\n\n## Installation\n\n\n```shell\npip install arango-orm\n```\n\n\n## Connecting to a Database\n\n```python\nfrom arango import ArangoClient\nfrom arango_orm import Database\n\nclient = ArangoClient(hosts='http://localhost:8529')\ntest_db = client.db('test', username='test', password='test')\n\ndb = Database(test_db)\n```\n\n## Define a Collection\n\n```python\nfrom datetime import date\nfrom pydantic import Field\nfrom arango_orm import Collection\n\n\nclass Student(Collection):\n\n __collection__ = 'students'\n\n name: str\n dob: date\n```\n\n## Create Collection in the Database\n\n\n```python\ndb.create_collection(Student)\n```\n\n## Drop a Collection\n\n```python\ndb.drop_collection(Student)\n```\n\n## Check if a collection exists\n\n```python\ndb.has_collection(Student)\ndb.has_collection('students')\n```\n\n## Add Records\n\n```python\nfrom datetime import date\ns = Student(name='test', _key='12312', dob=date(year=2016, month=9, day=12))\ndb.add(s)\nprint(s._id) # students/12312\n```\n\n## Get Total Records in the Collection\n\n\n```python\ndb.query(Student).count()\n```\n\n## Get Record By Key\n\n\n```python\ns = db.query(Student).by_key('12312')\n```\n\n## Update a Record\n\n```python\ns = db.query(Student).by_key('12312')\ns.name = 'Anonymous'\ndb.update(s)\n```\n\n## Delete a Record\n\n```python\ns = db.query(Student).by_key('12312')\ndb.delete(s)\n```\n\n## Get All Records in a Collection\n\n```python\nstudents = db.query(Student).all()\n```\n\n## Get First Record Matching the Query\n\n```python\nfirst_student = db.query(Student).first()\n```\n\n## Filter Records\n\nUsing bind parameters (recommended)\n\n```python\nrecords = db.query(Student).filter(\"name==@name\", name='Anonymous').all()\n```\n\nUsing plain condition strings (not safe in case of unsanitized user supplied input)\n\n```python\nrecords = db.query(Student).filter(\"name=='Anonymous'\").all()\n```\n\n## Filter Using OR\n\n```python\n# Get all documents where student name starts with A or B\nrecords = db.query(Student).filter(\n \"LIKE(rec.name, 'A%')\", prepend_rec_name=False).filter(\n \"LIKE(rec.name, 'B%')\", prepend_rec_name=False, _or=True).all()\n```\n\n## Filter, Sort and Limit\n\n```python\n# Last 5 students with names starting with A\nrecords = db.query(Student).filter(\n \"LIKE(rec.name, 'A%')\", prepend_rec_name=False).sort(\"name DESC\").limit(5).all()\n\n# Query students with pagination (limit&offset)\npage_num, per_page = 2, 10\npage = db.query(Student).sort(\"name DESC\").limit(per_page, start_from=(page_num - 1) * per_page)\n```\n\n## Fetch Only Some Fields\n\n\n```python\nc = db.query(Student).limit(2).returns('_key', 'name').first()\n```\n\n## Update Multiple Records\n\n```python\ndb.query(Student).filter(\"name==@name\", name='Anonymous').update(name='Mr. Anonymous')\n```\n\n## Delete Multiple Records\n\n```python\ndb.query(Student).filter(\"LIKE(rec.name, 'test%')\", prepend_rec_name=False).delete()\n```\n\n## Delete All Records\n\n```python\ndb.query(Student).delete()\n```\n\n\n## Bulk Create Records\n\n```python\ns1 = Student(name='test1', _key='12345', dob=date(year=2016, month=9, day=12))\ns2 = Student(name='test2', _key='22346', dob=date(year=2015, month=9, day=12)\ncar1 = Car(make=\"Honda\", model=\"Fiat\", year=2010)\ncar2 = Car(make=\"Honda\", model=\"Skoda\", year=2015)\n\ndb.bulk_add(entity_list=[p_ref_10, p_ref_11, car1, car2])\n```\n\n## Bulk Update Records\n\n```python\np_ref1 = db.query(Person).by_key(\"12312\")\np_ref2 = db.query(Person).by_key(\"12345\")\np_ref1.name = \"Bruce\"\np_ref2.name = \"Eliza\"\ndb.bulk_update(entity_list=[p_ref1, p_ref2])\n```\n\n## Query Using AQL\n\n```python\ndb.add(Student(name='test1', _key='12345', dob=date(year=2016, month=9, day=12)))\ndb.add(Student(name='test2', _key='22346', dob=date(year=2015, month=9, day=12)))\n\nstudents = [Student._load(s) for s in db.aql.execute(\"FOR st IN students RETURN st\")]\n```\n\n## Reference Fields\n\nReference fields allow linking documents from another collection class within a collection instance.\nThese are similar in functionality to SQLAlchemy's relationship function.\n\n```python\nfrom arango import ArangoClient\nfrom arango_orm.database import Database\n\nfrom arango_orm.fields import String\nfrom arango_orm import Collection, Relation, Graph, GraphConnection\nfrom arango_orm.references import relationship, graph_relationship\n\n\nclass Person(Collection):\n\n __collection__ = 'persons'\n\n _index = [{'type': 'hash', 'unique': False, 'fields': ['name']}]\n _allow_extra_fields = False # prevent extra properties from saving into DB\n\n _key = String(required=True)\n name = String(required=True, allow_none=False)\n\n cars = relationship(__name__ + \".Car\", '_key', target_field='owner_key')\n\n def __str__(self):\n return \"<Person(\" + self.name + \")>\"\n\n\nclass Car(Collection):\n\n __collection__ = 'cars'\n _allow_extra_fields = True\n\n make = String(required=True)\n model = String(required=True)\n year = Integer(required=True)\n owner_key = String()\n\n owner = relationship(Person, 'owner_key', cache=False)\n\n def __str__(self):\n return \"<Car({} - {} - {})>\".format(self.make, self.model, self.year)\n\nclient = ArangoClient(hosts='http://localhost:8529')\ntest_db = client.db('test', username='test', password='test')\ndb = Database(test_db)\n\np = Person(_key='kashif', name='Kashif Iftikhar')\ndb.add(p)\np2 = Person(_key='azeen', name='Azeen Kashif')\ndb.add(p2)\n\nc1 = Car(make='Honda', model='Civic', year=1984, owner_key='kashif')\ndb.add(c1)\n\nc2 = Car(make='Mitsubishi', model='Lancer', year=2005, owner_key='kashif')\ndb.add(c2)\n\nc3 = Car(make='Acme', model='Toy Racer', year=2016, owner_key='azeen')\ndb.add(c3)\n\nprint(c1.owner)\nprint(c1.owner.name)\nprint(c2.owner.name)\nprint(c3.owner.name)\n\nprint(p.cars)\nprint(p.cars[0].make)\nprint(p2.cars)\n```\n\n## Working With Graphs\n\nWorking with graphs involves creating collection classes and optionally Edge/Relation classes. Users can use the built-in Relation class for specifying relations but if relations need to contain extra attributes then it's required to create a sub-class of Relation class. Graph functionality is explain below with the help of a university graph example containing students, teachers, subjects and the areas where students and teachers reside in.\n\nFirst we create some collections and relationships\n\n```python\nfrom typing import Literal\nfrom arango_orm import Collection, Relation, Graph, GraphConnection\n\n\nclass Student(Collection):\n\n __collection__ = 'students'\n\n name: str\n age: int | None = None\n\n def __str__(self):\n return \"<Student({})>\".format(self.name)\n\n\nclass Teacher(Collection):\n\n __collection__ = 'teachers'\n\n name: str\n\n def __str__(self):\n return \"<Teacher({})>\".format(self.name)\n\n\nclass Subject(Collection):\n\n __collection__ = 'subjects'\n\n\n name: str\n credit_hours: int\n has_labs: bool = True\n\n def __str__(self):\n return \"<Subject({})>\".format(self.name)\n\n\nclass Area(Collection):\n\n __collection__ = 'areas'\n\n\n\nclass SpecializesIn(Relation):\n\n __collection__ = 'specializes_in'\n\n expertise_level: Literal[\"expert\", \"medium\", \"basic\"]\n\n def __str__(self):\n return \"<SpecializesIn(_key={}, expertise_level={}, _from={}, _to={})>\".format(\n self.key_, self.expertise_level, self._from, self._to)\n```\n\nNext we sub-class the Graph class to specify the relationships between the various collections\n\n```python\nclass UniversityGraph(Graph):\n\n __graph__ = 'university_graph'\n\n graph_connections = [\n # Using general Relation class for relationship\n GraphConnection(Student, Relation(\"studies\"), Subject),\n GraphConnection(Teacher, Relation(\"teaches\"), Subject),\n\n # Using specific classes for vertex and edges\n GraphConnection(Teacher, SpecializesIn, Subject),\n GraphConnection([Teacher, Student], Relation(\"resides_in\"), Area)\n ]\n```\n\nNow it's time to create the graph. Note that we don't need to create the collections individually, creating the graph will create all collections that it contains\n\n```python\nfrom arango import ArangoClient\nfrom arango_orm.database import Database\n\nclient = ArangoClient(hosts='http://localhost:8529')\ntest_db = client.db('test', username='test', password='test')\n\ndb = Database(test_db)\n\nuni_graph = UniversityGraph(connection=db)\ndb.create_graph(uni_graph)\n```\n\nNow the graph and all it's collections have been created, we can verify their existence:\n\n```python\n[c['name'] for c in db.collections()]\ndb.graphs()\n```\n\nNow let's insert some data into our graph:\n\n```python\nstudents_data = [\n Student(_key='S1001', name='John Wayne', age=30),\n Student(_key='S1002', name='Lilly Parker', age=22),\n Student(_key='S1003', name='Cassandra Nix', age=25),\n Student(_key='S1004', name='Peter Parker', age=20)\n]\n\nteachers_data = [\n Teacher(_key='T001', name='Bruce Wayne'),\n Teacher(_key='T002', name='Barry Allen'),\n Teacher(_key='T003', name='Amanda Waller')\n]\n\nsubjects_data = [\n Subject(_key='ITP101', name='Introduction to Programming', credit_hours=4, has_labs=True),\n Subject(_key='CS102', name='Computer History', credit_hours=3, has_labs=False),\n Subject(_key='CSOOP02', name='Object Oriented Programming', credit_hours=3, has_labs=True),\n]\n\nareas_data = [\n Area(_key=\"Gotham\"),\n Area(_key=\"Metropolis\"),\n Area(_key=\"StarCity\")\n]\n\nfor s in students_data:\n db.add(s)\n\nfor t in teachers_data:\n db.add(t)\n\nfor s in subjects_data:\n db.add(s)\n\nfor a in areas_data:\n db.add(a)\n```\n\nNext let's add some relations, we can add relations by manually adding the relation/edge record into the edge collection, like:\n\n```python\ndb.add(SpecializesIn(_from=\"teachers/T001\", _to=\"subjects/ITP101\", expertise_level=\"medium\"))\n```\n\nOr we can use the graph object's relation method to generate a relation document from given objects:\n\n```python\ngotham = db.query(Area).by_key(\"Gotham\")\nmetropolis = db.query(Area).by_key(\"Metropolis\")\nstar_city = db.query(Area).by_key(\"StarCity\")\n\njohn_wayne = db.query(Student).by_key(\"S1001\")\nlilly_parker = db.query(Student).by_key(\"S1002\")\ncassandra_nix = db.query(Student).by_key(\"S1003\")\npeter_parker = db.query(Student).by_key(\"S1004\")\n\nintro_to_prog = db.query(Subject).by_key(\"ITP101\")\ncomp_history = db.query(Subject).by_key(\"CS102\")\noop = db.query(Subject).by_key(\"CSOOP02\")\n\nbarry_allen = db.query(Teacher).by_key(\"T002\")\nbruce_wayne = db.query(Teacher).by_key(\"T001\")\namanda_waller = db.query(Teacher).by_key(\"T003\")\n\ndb.add(uni_graph.relation(peter_parker, Relation(\"studies\"), oop))\ndb.add(uni_graph.relation(peter_parker, Relation(\"studies\"), intro_to_prog))\ndb.add(uni_graph.relation(john_wayne, Relation(\"studies\"), oop))\ndb.add(uni_graph.relation(john_wayne, Relation(\"studies\"), comp_history))\ndb.add(uni_graph.relation(lilly_parker, Relation(\"studies\"), intro_to_prog))\ndb.add(uni_graph.relation(lilly_parker, Relation(\"studies\"), comp_history))\ndb.add(uni_graph.relation(cassandra_nix, Relation(\"studies\"), oop))\ndb.add(uni_graph.relation(cassandra_nix, Relation(\"studies\"), intro_to_prog))\n\ndb.add(uni_graph.relation(barry_allen, SpecializesIn(expertise_level=\"expert\"), oop))\ndb.add(uni_graph.relation(barry_allen, SpecializesIn(expertise_level=\"expert\"), intro_to_prog))\ndb.add(uni_graph.relation(bruce_wayne, SpecializesIn(expertise_level=\"medium\"), oop))\ndb.add(uni_graph.relation(bruce_wayne, SpecializesIn(expertise_level=\"expert\"), comp_history))\ndb.add(uni_graph.relation(amanda_waller, SpecializesIn(expertise_level=\"basic\"), intro_to_prog))\ndb.add(uni_graph.relation(amanda_waller, SpecializesIn(expertise_level=\"medium\"), comp_history))\n\ndb.add(uni_graph.relation(bruce_wayne, Relation(\"teaches\"), oop))\ndb.add(uni_graph.relation(barry_allen, Relation(\"teaches\"), intro_to_prog))\ndb.add(uni_graph.relation(amanda_waller, Relation(\"teaches\"), comp_history))\n\ndb.add(uni_graph.relation(bruce_wayne, Relation(\"resides_in\"), gotham))\ndb.add(uni_graph.relation(barry_allen, Relation(\"resides_in\"), star_city))\ndb.add(uni_graph.relation(amanda_waller, Relation(\"resides_in\"), metropolis))\ndb.add(uni_graph.relation(john_wayne, Relation(\"resides_in\"), gotham))\ndb.add(uni_graph.relation(lilly_parker, Relation(\"resides_in\"), metropolis))\ndb.add(uni_graph.relation(cassandra_nix, Relation(\"resides_in\"), star_city))\ndb.add(uni_graph.relation(peter_parker, Relation(\"resides_in\"), metropolis))\n```\n\nWith our graph populated with some sample data, let's explore the ways we can work with the graph.\n\n\n### Expanding Documents\n\nWe can expand any Collection (not Relation) object to access the data that is linked to it. We can sepcify which links ('inbound', 'outbound', 'any') to expand and the depth to which those should be expanded to. Let's see all immediate connections that Bruce Wayne has in our graph:\n\n```python\nbruce = db.query(Teacher).by_key(\"T001\")\nuni_graph.expand(bruce, depth=1, direction='any')\n```\n\nGraph expansion on an object adds a `_relations` dictionary that contains all the relations for the object according to the expansion criteria:\n\n```python\nbruce._relations\n\n# Returns:\n\n{\n'resides_in': [<Relation(_key=4205290, _from=teachers/T001, _to=areas/Gotham)>],\n'specializes_in': [<SpecializesIn(_key=4205114, expertise_level=medium, _from=teachers/T001, _to=subjects/ITP101)>,\n <SpecializesIn(_key=4205271, expertise_level=expert, _from=teachers/T001, _to=subjects/CS102)>,\n <SpecializesIn(_key=4205268, expertise_level=medium, _from=teachers/T001, _to=subjects/CSOOP02)>],\n'teaches': [<Relation(_key=4205280, _from=teachers/T001, _to=subjects/CSOOP02)>]\n}\n```\n\nWe can use _from and _to of a relation object to access the id's for both sides of the link. We also have _object_from and _object_to to access the objects on both sides, for example:\n\n```python\nbruce._relations['resides_in'][0]._object_from.name\n# 'Bruce Wayne'\n\nbruce._relations['resides_in'][0]._object_to._key\n# 'Gotham'\n```\n\nThere is also a special attribute called `_next` that allows accessing the other side of the relationship irrespective of the relationship direction. For example, for outbound relationships the `_object_from` contains the source object while for inbound_relationships `_object_to` contains the source object. But if we're only interested in traversal of the graph then it's more useful at times to access the other side of the relationship w.r.t the current object irrespective of it's direction:\n\n```python\nbruce._relations['resides_in'][0]._next._key\n# 'Gotham'\n```\n\nLet's expand the bruce object to 2 levels and see `_next` in more action:\n\n```python\nuni_graph.expand(bruce, depth=2)\n\n# All relations of the area where bruce resides in\nbruce._relations['resides_in'][0]._object_to._relations\n# -> {'resides_in': [<Relation(_key=4205300, _from=students/S1001, _to=areas/Gotham)>]}\n\n# Name of the student that resides in the same area as bruce\nbruce._relations['resides_in'][0]._object_to._relations['resides_in'][0]._object_from.name\n# 'John Wayne'\n\n# The same action using _next without worrying about direction\nbruce._relations['resides_in'][0]._next._relations['resides_in'][0]._next.name\n# 'John Wayne'\n\n# Get names of all people that reside in the same area and Bruce Wayne\n[p._next.name for p in bruce._relations['resides_in'][0]._next._relations['resides_in']]\n# ['John Wayne']\n```\n\n\n## Inheritance Mapping\n\nFor inheritance mapping, **arango_orm** offers you two ways to define it.\n\n### 1. Discriminator field/mapping:\n\nDiscriminator field/mapping are defined at entity level:\n\n```python\nclass Vehicle(Collection):\n __collection__ = \"vehicle\"\n\n _inheritance_field = \"discr\"\n _inheritance_mapping = {\n 'Bike': 'moto',\n 'Truck': 'truck'\n }\n\n _key = String()\n brand = String()\n model = String()\n # discr field match what you defined in _inheritance_field\n # the field type depends on the values of your _inheritance_mapping\n discr = String(required=True)\n\n\nclass Bike(Vehicle):\n motor_size = Float()\n\n\nclass Truck(Vehicle):\n traction_power = Float()\n```\n\n### 2. Inheritance mapping resolver:\n\nThe `inheritance_mapping_resolver` is a function defined at graph level; it allows you to make either a simple test\non a discriminator field, or complex inference\n\n```python\nclass OwnershipGraph2(Graph):\n __graph__ = \"ownership_graph\"\n\n graph_connections = [\n GraphConnection(Owner2, Own2, Vehicle2)\n ]\n\n def inheritance_mapping_resolver(self, col_name: str, doc_dict: dict = {}):\n if col_name == 'vehicle':\n if 'traction_power' in doc_dict:\n return Truck2\n else:\n return Bike2\n\n return self.vertices[col_name]\n```\n\n## Graph Traversal Using AQL\n\nThe graph module also supports traversals using AQL, the results are converted to objects and have the\nsame structure as graph.expand method:\n\n```python\nobj = uni_graph.aql(\"FOR v, e, p IN 1..2 INBOUND 'areas/Gotham' GRAPH 'university_graph' RETURN p\")\nprint(obj._key)\n# Gotham\n\ngotham_residents = [rel._next.name for rel in obj._relations['resides_in']]\nprint(gotham_residents)\n# ['Bruce Wayne', 'John Wayne']\n```\n\n# For Developers\n\n\n## Running the Test Cases\n\nSet env variables in .env file, then load the env\n\n```shell\nset -a; source .env; set +a\n```\n\nAfterwards run tests\n\n```\npoetry run pytest\n```\n",
"bugtrack_url": null,
"license": "GNU General Public License v2 (GPLv2)",
"summary": "A SQLAlchemy like ORM implementation for arangodb",
"version": "1.1.0",
"project_urls": {
"Homepage": "https://arango-orm.compulife.com.pk",
"Repository": "https://github.com/kashifpk/arango-orm"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "f0f6968976e62a4b39883853663657b5b1ff6adf324e167d05d8153f57bbb9cf",
"md5": "222a9d649d7e565509895b6639b80352",
"sha256": "a78d25f4c8329b6af9a9428ca4648e374ef2f6fc712c92b474eb10beefb40087"
},
"downloads": -1,
"filename": "arango_orm-1.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "222a9d649d7e565509895b6639b80352",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.11",
"size": 35534,
"upload_time": "2024-10-14T01:24:55",
"upload_time_iso_8601": "2024-10-14T01:24:55.635883Z",
"url": "https://files.pythonhosted.org/packages/f0/f6/968976e62a4b39883853663657b5b1ff6adf324e167d05d8153f57bbb9cf/arango_orm-1.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "a5df4de504d0c72db24c7d94b8f595e7beabdf54058e3c8411d6504bbe1bd843",
"md5": "563534daa9c946791d563337dcf8795b",
"sha256": "7c47e29fc6314e39e37fef3ea08a9429681c5c99d20f3039bf9a6aac8a49fde2"
},
"downloads": -1,
"filename": "arango_orm-1.1.0.tar.gz",
"has_sig": false,
"md5_digest": "563534daa9c946791d563337dcf8795b",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.11",
"size": 37064,
"upload_time": "2024-10-14T01:24:57",
"upload_time_iso_8601": "2024-10-14T01:24:57.277916Z",
"url": "https://files.pythonhosted.org/packages/a5/df/4de504d0c72db24c7d94b8f595e7beabdf54058e3c8411d6504bbe1bd843/arango_orm-1.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-10-14 01:24:57",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "kashifpk",
"github_project": "arango-orm",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"tox": true,
"lcname": "arango-orm"
}