Skip to content

Commit 77d0a28

Browse files
author
matmoncon
committed
docs: update documentation and split docs into separate files
1 parent 0018c57 commit 77d0a28

File tree

10 files changed

+1614
-1646
lines changed

10 files changed

+1614
-1646
lines changed

.github/workflows/ci.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
strategy:
1818
fail-fast: false
1919
matrix:
20-
python-version: [3.10.6, 3.11]
20+
python-version: ["3.10", "3.11", "3.12"]
2121

2222
steps:
2323
- uses: actions/checkout@v3
@@ -61,7 +61,7 @@ jobs:
6161
strategy:
6262
fail-fast: false
6363
matrix:
64-
python-version: [3.10.6, 3.11]
64+
python-version: ["3.10", "3.11", "3.12"]
6565

6666
steps:
6767
- uses: actions/checkout@v3
@@ -112,7 +112,7 @@ jobs:
112112
strategy:
113113
fail-fast: false
114114
matrix:
115-
python-version: [3.10.6, 3.11]
115+
python-version: ["3.10", "3.11", "3.12"]
116116
pydantic-version: ["1.10.9", "^2"]
117117

118118
steps:

.vscode/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"python.testing.pytestEnabled": true,
99
"markdownlint.config": {
1010
"MD024": false,
11-
"MD033": false,
12-
"MD039": false
11+
"MD039": false,
12+
"MD041": false
1313
},
1414
"files.exclude": {
1515
"**/.git": true,

README.md

Lines changed: 88 additions & 1641 deletions
Large diffs are not rendered by default.

docs/Concept.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## Basic concepts
2+
3+
As you might have guessed by now, `pyneo4j-ogm` is a library that allows you to interact with a Neo4j database using Python. It is designed to make your life as simple as possible, while still providing the most common operations and some more advanced features.
4+
5+
But first, how does this even work!?! Well, the basic concept boils down to the following:
6+
7+
- You define your models that represent your nodes and relationships inside the graph.
8+
- You use these models to do all sorts of things with your data.
9+
10+
Of course, there is a lot more to it than that, but this is the basic idea. So let's take a closer look at the different parts of `pyneo4j-ogm` and how to use them.
11+
12+
> **Note:** All of the examples in this documentation assume that you have already connected to a database and registered your models with the client like shown in the [`quickstart guide`](https://github.com/groc-prog/pyneo4j-ogm/blob/develop?tab=readme-ov-file#-quickstart). The models used in the following examples will build upon the ones defined there. If you are new to [`Neo4j`](https://neo4j.com/docs/) or [`Cypher`](https://neo4j.com/docs/cypher-manual/current/) in general, you should get a basic understanding of how to use them before continuing.
13+
14+
### A note on Pydantic version support
15+
16+
As of version [`v0.3.0`](https://github.com/groc-prog/pyneo4j-ogm/blob/develop/CHANGELOG.md#whats-changed-in-v030-2023-11-30), pyneo4j-ogm now supports both `Pydantic 1.10+ and 2+`. All core features of pydantic should work, meaning full support for model serialization, validation and schema generation.
17+
18+
Should you find any issues or run into any problems, feel free to open a issue!

docs/DatabaseClient.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
## Database client
2+
3+
This is where the magic happens! The `Pyneo4jClient` is the main entry point for interacting with the database. It handles all the heavy lifting for you and your models. Because of this, we have to always have at least one client initialized before doing anything else.
4+
5+
### Connecting to the database
6+
7+
Before you can run any queries, you have to connect to a database. This is done by calling the `connect()` method of the `Pyneo4jClient` instance. The `connect()` method takes a few arguments:
8+
9+
- `uri`: The connection URI to the database.
10+
- `skip_constraints`: Whether the client should skip creating any constraints defined on models when registering them. Defaults to `False`.
11+
- `skip_indexes`: Whether the client should skip creating any indexes defined on models when registering them. Defaults to `False`.
12+
- `*args`: Additional arguments that are passed directly to Neo4j's `AsyncDriver.driver()` method.
13+
- `**kwargs`: Additional keyword arguments that are passed directly to Neo4j's `AsyncDriver.driver()` method.
14+
15+
```python
16+
from pyneo4j_ogm import Pyneo4jClient
17+
18+
client = Pyneo4jClient()
19+
await client.connect(uri="<connection-uri-to-database>", auth=("<username>", "<password>"), max_connection_pool_size=10, ...)
20+
21+
## Or chained right after the instantiation of the class
22+
client = await Pyneo4jClient().connect(uri="<connection-uri-to-database>", auth=("<username>", "<password>"), max_connection_pool_size=10, ...)
23+
```
24+
25+
After connecting the client, you will be able to run any cypher queries against the database. Should you try to run a query without connecting to a database first (it happens to the best of us), you will get a `NotConnectedToDatabase` exception.
26+
27+
### Closing an existing connection
28+
29+
Connections can explicitly be closed by calling the `close()` method. This will close the connection to the database and free up any resources used by the client. Remember to always close your connections when you are done with them!
30+
31+
```python
32+
## Do some heavy-duty work...
33+
34+
## Finally done, so we close the connection to the database.
35+
await client.close()
36+
```
37+
38+
Once you closed the client, it will be seen as `disconnected` and if you try to run any further queries with it, you will get a `NotConnectedToDatabase` exception
39+
40+
### Registering models
41+
42+
Models are a core feature of pyneo4j-ogm, and therefore you probably want to use some. But to work with them, they have to be registered with the client by calling the `register_models()` method and passing in your models as a list:
43+
44+
```python
45+
## Create a new client instance and connect ...
46+
47+
await client.register_models([Developer, Coffee, Consumed])
48+
```
49+
50+
This is a crucial step, because if you don't register your models with the client, you won't be able to work with them in any way. Should you try to work with a model that has not been registered, you will get a `UnregisteredModel` exception. This exception also gets raised if a database model defines a relationship-property with other (unregistered) models as a target or relationship model and then runs a query with said relationship-property.
51+
52+
If you have defined any indexes or constraints on your models, they will be created automatically when registering them. You can prevent this behavior by passing `skip_constraints=True` or `skip_indexes=True` to the `connect()` method. If you do this, you will have to create the indexes and constraints yourself.
53+
54+
> **Note**: If you don't register your models with the client, you will still be able to run cypher queries directly with the client, but you will `lose automatic model resolution` from queries. This means that, instead of resolved models, the raw Neo4j query results are returned.
55+
56+
### Executing Cypher queries
57+
58+
Models aren't the only things capable of running queries. The client can also be used to run queries, with some additional functionality to make your life easier.
59+
60+
Node- and RelationshipModels provide many methods for commonly used cypher queries, but sometimes you might want to execute a custom cypher with more complex logic. For this purpose, the client instance provides a `cypher()` method that allows you to execute custom cypher queries. The `cypher()` method takes three arguments:
61+
62+
- `query`: The cypher query to execute.
63+
- `parameters`: A dictionary containing the parameters to pass to the query.
64+
- `resolve_models`: Whether the client should try to resolve the models from the query results. Defaults to `True`.
65+
66+
This method will always return a tuple containing a list of results and a list of variables returned by the query. Internally, the client uses the `.values()` method of the Neo4j driver to get the results of the query.
67+
68+
> **Note:** If no models have been registered with the client and resolve_models is set to True, the client will not raise any exceptions but rather return the raw query results.
69+
70+
Here is an example of how to execute a custom cypher query:
71+
72+
```python
73+
results, meta = await client.cypher(
74+
query="CREATE (d:Developer {uid: '553ac2c9-7b2d-404e-8271-40426ae80de0', name: 'John', age: 25}) RETURN d.name as developer_name, d.age",
75+
parameters={"name": "John Doe"},
76+
resolve_models=False, ## Explicitly disable model resolution
77+
)
78+
79+
print(results) ## [["John", 25]]
80+
print(meta) ## ["developer_name", "d.age"]
81+
```
82+
83+
### Batching cypher queries
84+
85+
We provide an easy way to batch multiple database queries together, regardless of whether you are using the client directly or via a model method. To do this you can use the `batch()` method, which has to be called with a asynchronous context manager like in the following example:
86+
87+
```python
88+
async with client.batch():
89+
## All queries executed inside the context manager will be batched into a single transaction
90+
## and executed once the context manager exits. If any of the queries fail, the whole transaction
91+
## will be rolled back.
92+
await client.cypher(
93+
query="CREATE (d:Developer {uid: $uid, name: $name, age: $age})",
94+
parameters={"uid": "553ac2c9-7b2d-404e-8271-40426ae80de0", "name": "John Doe", "age": 25},
95+
)
96+
await client.cypher(
97+
query="CREATE (c:Coffee {flavour: $flavour, milk: $milk, sugar: $sugar})",
98+
parameters={"flavour": "Espresso", "milk": False, "sugar": False},
99+
)
100+
101+
## Model queries also can be batched together without any extra work!
102+
coffee = await Coffee(flavour="Americano", milk=False, sugar=False).create()
103+
```
104+
105+
You can batch anything that runs a query, be that a model method, a custom query or a relationship-property method. If any of the queries fail, the whole transaction will be rolled back and an exception will be raised.
106+
107+
### Using bookmarks (Enterprise Edition only)
108+
109+
If you are using the Enterprise Edition of Neo4j, you can use bookmarks to keep track of the last transaction that has been committed. The client provides a `last_bookmarks` property that allows you to get the bookmarks from the last session. These bookmarks can be used in combination with the `use_bookmarks()` method. Like the `batch()` method, the `use_bookmarks()` method has to be called with a context manager. All queries run inside the context manager will use the bookmarks passed to the `use_bookmarks()` method. Here is an example of how to use bookmarks:
110+
111+
```python
112+
## Create a new node and get the bookmarks from the last session
113+
await client.cypher("CREATE (d:Developer {name: 'John Doe', age: 25})")
114+
bookmarks = client.last_bookmarks
115+
116+
## Create another node, but this time don't get the bookmark
117+
## When we use the bookmarks from the last session, this node will not be visible
118+
await client.cypher("CREATE (c:Coffee {flavour: 'Espresso', milk: False, sugar: False})")
119+
120+
with client.use_bookmarks(bookmarks=bookmarks):
121+
## All queries executed inside the context manager will use the bookmarks
122+
## passed to the `use_bookmarks()` method.
123+
124+
## Here we will only see the node created in the first query
125+
results, meta = await client.cypher("MATCH (n) RETURN n")
126+
127+
## Model queries also can be batched together without any extra work!
128+
## This will return no results, since the coffee node was created after
129+
## the bookmarks were taken.
130+
coffee = await Coffee.find_many()
131+
print(coffee) ## []
132+
```
133+
134+
### Manual indexing and constraints
135+
136+
Most of the time, the creation of indexes/constraints will be handled by the models themselves. But it can still be handy to have a simple way of creating new ones. This is where the `create_lookup_index()`, `create_range_index`, `create_text_index`, `create_point_index` and `create_uniqueness_constraint()` methods come in.
137+
138+
First, let's take a look at how to create a custom index in the database. The `create_range_index`, `create_text_index` and `create_point_index` methods take a few arguments:
139+
140+
- `name`: The name of the index to create (Make sure this is unique!).
141+
- `entity_type`: The entity type the index is created for. Can be either **EntityType.NODE** or **EntityType.RELATIONSHIP**.
142+
- `properties`: A list of properties to create the index for.
143+
- `labels_or_type`: The node labels or relationship type the index is created for.
144+
145+
The `create_lookup_index()` takes the same arguments, except for the `labels_or_type` and `properties` arguments.
146+
147+
The `create_uniqueness_constraint()` method also takes similar arguments.
148+
149+
- `name`: The name of the constraint to create.
150+
- `entity_type`: The entity type the constraint is created for. Can be either **EntityType.NODE** or **EntityType.RELATIONSHIP**.
151+
- `properties`: A list of properties to create the constraint for.
152+
- `labels_or_type`: The node labels or relationship type the constraint is created for.
153+
154+
Here is an example of how to use the methods:
155+
156+
```python
157+
## Creates a `RANGE` index for a `Coffee's` `sugar` and `flavour` properties
158+
await client.create_range_index("hot_beverage_index", EntityType.NODE, ["sugar", "flavour"], ["Beverage", "Hot"])
159+
160+
## Creates a UNIQUENESS constraint for a `Developer's` `uid` property
161+
await client.create_uniqueness_constraint("developer_constraint", EntityType.NODE, ["uid"], ["Developer"])
162+
```
163+
164+
### Client utilities
165+
166+
The client also provides some additional utility methods, which mostly exist for convenience when writing tests or setting up environments:
167+
168+
- `is_connected()`: Returns whether the client is currently connected to a database.
169+
- `drop_nodes()`: Drops all nodes from the database.
170+
- `drop_constraints()`: Drops all constraints from the database.
171+
- `drop_indexes()`: Drops all indexes from the database.

docs/Logging.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Logging
2+
3+
You can control the log level and whether to log to the console or not by setting the `PYNEO4J_OGM_LOG_LEVEL` and `PYNEO4J_OGM_ENABLE_LOGGING` as environment variables. The available levels are the same as provided by the build-in `logging` module. The default log level is `WARNING` and logging to the console is enabled by default.

docs/Migrations.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
## Migrations
2+
3+
As of version [`v0.5.0`](https://github.com/groc-prog/pyneo4j-ogm/blob/develop/CHANGELOG.md#whats-changed-in-v050-2024-02-06), pyneo4j-ogm supports migrations using a built-in migration tool. The migration tool is basic but flexibly, which should cover most use-cases.
4+
5+
### Initializing migrations for your project
6+
7+
To initialize migrations for your project, you can use the `poetry run pyneo4j_ogm init` command. This will create a `migrations` directory at the given path (which defaults to `./migrations`), which will contain all your migration files.
8+
9+
```bash
10+
poetry run pyneo4j_ogm init --migration-dir ./my/custom/migration/path
11+
```
12+
13+
### Creating a new migration
14+
15+
To create a new migration, you can use the `poetry run pyneo4j_ogm create` command. This will create a new migration file inside the `migrations` directory. The migration file will contain a `up` and `down` function, which you can use to define your migration.
16+
17+
```bash
18+
poetry run pyneo4j_ogm create my_first_migration
19+
```
20+
21+
Both the `up` and `down` functions will receive the client used during the migration as their only arguments. This makes the migrations pretty flexible, since you can not only use the client to execute queries, but also register models on it and use them to execute methods.
22+
23+
> **Note**: When using models inside the migration, you have to make sure that the model used implements the same data structure as the data inside the graph. Otherwise you might run into validation issues.
24+
25+
```python
26+
"""
27+
Auto-generated migration file {name}. Do not
28+
rename this file or the `up` and `down` functions.
29+
"""
30+
from pyneo4j_ogm import Pyneo4jClient
31+
32+
33+
async def up(client: Pyneo4jClient) -> None:
34+
"""
35+
Write your `UP migration` here.
36+
"""
37+
await client.cypher("CREATE (n:Node {name: 'John'})")
38+
39+
40+
async def down(client: Pyneo4jClient) -> None:
41+
"""
42+
Write your `DOWN migration` here.
43+
"""
44+
await client.cypher("MATCH (n:Node {name: 'John'}) DELETE n")
45+
```
46+
47+
### Running migrations
48+
49+
To run the migrations, you can use the `up` or `down` commands. The `up` command will run all migrations that have not been run yet, while the `down` command will run all migrations in reverse order.
50+
51+
Both commands support a `--up-count` or `--down-count` argument, which can be used to limit the number of migrations to run. By default, the `up` command will run `all pending migration` and the `down` command will roll back the `last migration`.
52+
53+
```bash
54+
poetry run pyneo4j_ogm up --up-count 3
55+
poetry run pyneo4j_ogm down --down-count 2
56+
```
57+
58+
### Listing migrations
59+
60+
The current state of all migrations can be viewed anytime using the `status` command. This will show you all migrations that have been run and all migrations that are pending.
61+
62+
```bash
63+
poetry run pyneo4j_ogm status
64+
65+
## Output
66+
┌─────────────────────────────────────────┬─────────────────────┐
67+
│ Migration │ Applied At │
68+
├─────────────────────────────────────────┼─────────────────────┤
69+
│ 20160608155948-my_awesome_migration │ 2022-03-04 15:40:22 │
70+
│ 20160608155948-my_fixed_migration │ 2022-03-04 15:41:13 │
71+
│ 20160608155948-final_fix_i_swear │ PENDING │
72+
└─────────────────────────────────────────┴─────────────────────┘
73+
```
74+
75+
### Programmatic usage
76+
77+
The migration tool can also be used programmatically. This can be useful if you want to run migrations inside your application or if you want to integrate the migration tool into your own CLI.
78+
79+
```python
80+
import asyncio
81+
from pyneo4j_ogm.migrations import create, down, init, status, up
82+
83+
## Call with same arguments as you would with cli
84+
init(migration_dir="./my/custom/migration/path")
85+
86+
create("my_first_migration")
87+
asyncio.run(up())
88+
```

0 commit comments

Comments
 (0)