saengra 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.
- saengra-0.1.0/CMakeLists.txt +46 -0
- saengra-0.1.0/LICENSE +21 -0
- saengra-0.1.0/MANIFEST.in +8 -0
- saengra-0.1.0/PKG-INFO +195 -0
- saengra-0.1.0/pyproject.toml +96 -0
- saengra-0.1.0/saengra/README.md +160 -0
- saengra-0.1.0/saengra/__init__.py +10 -0
- saengra-0.1.0/saengra/adapter.py +37 -0
- saengra-0.1.0/saengra/api.py +50 -0
- saengra-0.1.0/saengra/c_extension.cpython-311-darwin.so +0 -0
- saengra-0.1.0/saengra/client.py +384 -0
- saengra-0.1.0/saengra/conversions.py +165 -0
- saengra-0.1.0/saengra/entity.py +401 -0
- saengra-0.1.0/saengra/environment.py +258 -0
- saengra-0.1.0/saengra/errors.py +38 -0
- saengra-0.1.0/saengra/factory.py +43 -0
- saengra-0.1.0/saengra/frozendict.py +71 -0
- saengra-0.1.0/saengra/graph.py +74 -0
- saengra-0.1.0/saengra/messages_pb2.py +106 -0
- saengra-0.1.0/saengra/observer.py +162 -0
- saengra-0.1.0/saengra/socket_adapter.py +82 -0
- saengra-0.1.0/saengra/utilities/__init__.py +0 -0
- saengra-0.1.0/saengra/utilities/annotations.py +102 -0
- saengra-0.1.0/saengra/utilities/colors.py +37 -0
- saengra-0.1.0/saengra/utilities/containers.py +177 -0
- saengra-0.1.0/saengra/utilities/itertools.py +13 -0
- saengra-0.1.0/saengra/utilities/loggers.py +48 -0
- saengra-0.1.0/saengra/utilities/properties.py +261 -0
- saengra-0.1.0/saengra/utilities/typing.py +21 -0
- saengra-0.1.0/saengra-server/CMakeLists.txt +95 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CMakeCXXCompiler.cmake +104 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CMakeSystem.cmake +15 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +949 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CompilerIdCXX/apple-sdk.cpp +1 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/CMakeDirectoryInformation.cmake +16 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/Makefile.cmake +82 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_core.dir/DependInfo.cmake +46 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_core.dir/cmake_clean.cmake +48 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_core.dir/cmake_clean_target.cmake +3 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_server.dir/DependInfo.cmake +24 -0
- saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_server.dir/cmake_clean.cmake +13 -0
- saengra-0.1.0/saengra-server/build/CTestTestfile.cmake +6 -0
- saengra-0.1.0/saengra-server/build/cmake_install.cmake +61 -0
- saengra-0.1.0/saengra-server/build/lexer.yy.cpp +1965 -0
- saengra-0.1.0/saengra-server/build/messages.pb.h +9862 -0
- saengra-0.1.0/saengra-server/build/parser.tab.cpp +1759 -0
- saengra-0.1.0/saengra-server/build/parser.tab.h +128 -0
- saengra-0.1.0/saengra-server/include/debug.h +138 -0
- saengra-0.1.0/saengra-server/include/edge.h +71 -0
- saengra-0.1.0/saengra-server/include/edges_container.h +240 -0
- saengra-0.1.0/saengra-server/include/expression.h +179 -0
- saengra-0.1.0/saengra-server/include/graph.h +120 -0
- saengra-0.1.0/saengra-server/include/incremental_matcher.h +69 -0
- saengra-0.1.0/saengra-server/include/logging.h +59 -0
- saengra-0.1.0/saengra-server/include/matcher.h +48 -0
- saengra-0.1.0/saengra-server/include/observable.h +234 -0
- saengra-0.1.0/saengra-server/include/observer_container.h +76 -0
- saengra-0.1.0/saengra-server/include/parser.h +23 -0
- saengra-0.1.0/saengra-server/include/query.h +17 -0
- saengra-0.1.0/saengra-server/include/queryset.h +94 -0
- saengra-0.1.0/saengra-server/include/refs.h +52 -0
- saengra-0.1.0/saengra-server/include/start_positions.h +25 -0
- saengra-0.1.0/saengra-server/include/subgraph.h +76 -0
- saengra-0.1.0/saengra-server/include/unix_socket_server.h +38 -0
- saengra-0.1.0/saengra-server/include/utility.h +23 -0
- saengra-0.1.0/saengra-server/include/vertex.h +91 -0
- saengra-0.1.0/saengra-server/include/vertices_container.h +71 -0
- saengra-0.1.0/saengra-server/include/worker.h +73 -0
- saengra-0.1.0/saengra-server/parser/lexer.l +73 -0
- saengra-0.1.0/saengra-server/parser/parser.y +296 -0
- saengra-0.1.0/saengra-server/proto/messages.proto +180 -0
- saengra-0.1.0/saengra-server/src/edge.cpp +41 -0
- saengra-0.1.0/saengra-server/src/edges_container.cpp +539 -0
- saengra-0.1.0/saengra-server/src/expression.cpp +274 -0
- saengra-0.1.0/saengra-server/src/graph.cpp +367 -0
- saengra-0.1.0/saengra-server/src/incremental_matcher.cpp +240 -0
- saengra-0.1.0/saengra-server/src/main.cpp +77 -0
- saengra-0.1.0/saengra-server/src/matcher.cpp +449 -0
- saengra-0.1.0/saengra-server/src/observable.cpp +73 -0
- saengra-0.1.0/saengra-server/src/observer_container.cpp +274 -0
- saengra-0.1.0/saengra-server/src/parser.cpp +83 -0
- saengra-0.1.0/saengra-server/src/refs.cpp +55 -0
- saengra-0.1.0/saengra-server/src/start_positions.cpp +151 -0
- saengra-0.1.0/saengra-server/src/unix_socket_server.cpp +119 -0
- saengra-0.1.0/saengra-server/src/vertex.cpp +27 -0
- saengra-0.1.0/saengra-server/src/vertices_container.cpp +168 -0
- saengra-0.1.0/saengra-server/src/worker.cpp +522 -0
- saengra-0.1.0/saengra-server/tests/test_edges_container.cpp +194 -0
- saengra-0.1.0/saengra-server/tests/test_graph.cpp +847 -0
- saengra-0.1.0/saengra-server/tests/test_matcher.cpp +348 -0
- saengra-0.1.0/saengra-server/tests/test_observables.cpp +29 -0
- saengra-0.1.0/saengra-server/tests/test_observers.cpp +163 -0
- saengra-0.1.0/saengra-server/tests/test_parser.cpp +263 -0
- saengra-0.1.0/saengra-server/tests/test_start_positions.cpp +299 -0
- saengra-0.1.0/saengra-server/tests/test_vertices_container.cpp +206 -0
- saengra-0.1.0/saengra.egg-info/PKG-INFO +195 -0
- saengra-0.1.0/saengra.egg-info/SOURCES.txt +127 -0
- saengra-0.1.0/saengra.egg-info/dependency_links.txt +1 -0
- saengra-0.1.0/saengra.egg-info/not-zip-safe +1 -0
- saengra-0.1.0/saengra.egg-info/requires.txt +12 -0
- saengra-0.1.0/saengra.egg-info/top_level.txt +1 -0
- saengra-0.1.0/setup.cfg +4 -0
- saengra-0.1.0/setup.py +98 -0
- saengra-0.1.0/src/adapter.cpp +959 -0
- saengra-0.1.0/tests/test_1_low_level_updates.py +222 -0
- saengra-0.1.0/tests/test_2_transactions.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.15)
|
|
2
|
+
project(saengra VERSION 0.1.0 LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
7
|
+
|
|
8
|
+
# Find Python
|
|
9
|
+
find_package(Python COMPONENTS Interpreter Development REQUIRED)
|
|
10
|
+
|
|
11
|
+
# Build saengra_core from saengra-server
|
|
12
|
+
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/saengra-server ${CMAKE_CURRENT_BINARY_DIR}/saengra-server-build)
|
|
13
|
+
|
|
14
|
+
# Create the Python extension module
|
|
15
|
+
Python_add_library(c_extension MODULE
|
|
16
|
+
src/adapter.cpp
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
target_link_libraries(c_extension PRIVATE
|
|
20
|
+
saengra_core
|
|
21
|
+
${Python_LIBRARIES}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
target_include_directories(c_extension PRIVATE
|
|
25
|
+
${Python_INCLUDE_DIRS}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Set output name and location
|
|
29
|
+
set_target_properties(c_extension PROPERTIES
|
|
30
|
+
PREFIX ""
|
|
31
|
+
OUTPUT_NAME "c_extension"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if(DEFINED CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
|
35
|
+
set_target_properties(c_extension PROPERTIES
|
|
36
|
+
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}"
|
|
37
|
+
)
|
|
38
|
+
else()
|
|
39
|
+
set_target_properties(c_extension PROPERTIES
|
|
40
|
+
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/saengra"
|
|
41
|
+
)
|
|
42
|
+
endif()
|
|
43
|
+
|
|
44
|
+
install(TARGETS c_extension
|
|
45
|
+
LIBRARY DESTINATION saengra
|
|
46
|
+
)
|
saengra-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tigran Saluev
|
|
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.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
include CMakeLists.txt
|
|
2
|
+
include src/*.cpp
|
|
3
|
+
recursive-include saengra-server *.cmake *.cpp *.h *.hpp *.proto *.l *.y
|
|
4
|
+
include saengra-server/CMakeLists.txt
|
|
5
|
+
recursive-include saengra-server/src *
|
|
6
|
+
recursive-include saengra-server/include *
|
|
7
|
+
recursive-include saengra-server/parser *
|
|
8
|
+
recursive-include saengra-server/proto *
|
saengra-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saengra
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reactive graph database with pattern matching
|
|
5
|
+
Author-email: Tigran Saluev <tigran@saluev.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Saluev/saengra
|
|
8
|
+
Project-URL: Repository, https://github.com/Saluev/saengra
|
|
9
|
+
Project-URL: Issues, https://github.com/Saluev/saengra/issues
|
|
10
|
+
Keywords: graph,database,reactive,pattern-matching,entity
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: C++
|
|
19
|
+
Classifier: Topic :: Database
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: frozendict>=2.4.6
|
|
25
|
+
Requires-Dist: protobuf>=4.0
|
|
26
|
+
Requires-Dist: termcolor>=2.4.0
|
|
27
|
+
Provides-Extra: socket
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
31
|
+
Requires-Dist: build; extra == "dev"
|
|
32
|
+
Requires-Dist: twine; extra == "dev"
|
|
33
|
+
Requires-Dist: cibuildwheel; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# Saengra
|
|
37
|
+
|
|
38
|
+
Python wrapper for Saengra graph database.
|
|
39
|
+
|
|
40
|
+
## Quickstart: primitives and edges
|
|
41
|
+
|
|
42
|
+
Saengra is a graph database. It supports hashable Python objects (**primitives**) as graph
|
|
43
|
+
vertices. Built-in types like `int` or `str` can be used directly; also Saengra provides `@primitive`
|
|
44
|
+
decorator to declare dataclass-like types to be used as graph vertices.
|
|
45
|
+
|
|
46
|
+
Directed edges between primitives are always labelled with a string (**edge label**). It can
|
|
47
|
+
be an arbitrary string, but the engine is optimized to support limited number of different labels per graph.
|
|
48
|
+
There can't be two edges between the same two primitives with the same label.
|
|
49
|
+
|
|
50
|
+
We can construct a graph directly from primitives and edges by using elementary graph operations:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from datetime import datetime
|
|
54
|
+
from saengra import primitive, Environment
|
|
55
|
+
from saengra.graph import AddVertex, AddEdge
|
|
56
|
+
|
|
57
|
+
@primitive
|
|
58
|
+
class user:
|
|
59
|
+
id: int
|
|
60
|
+
|
|
61
|
+
u1 = user(id=1)
|
|
62
|
+
u2 = user(id=2)
|
|
63
|
+
u1_registered_at = datetime(2022, 1, 1, 12, 0, 0)
|
|
64
|
+
u2_registered_at = datetime(2023, 2, 3, 15, 0, 0)
|
|
65
|
+
|
|
66
|
+
env = Environment()
|
|
67
|
+
env.update(
|
|
68
|
+
AddVertex(u1),
|
|
69
|
+
AddVertex(u2),
|
|
70
|
+
AddVertex(u1_registered_at),
|
|
71
|
+
AddVertex(u2_registered_at),
|
|
72
|
+
AddEdge(u1, "follows", u2),
|
|
73
|
+
AddEdge(u1, "registered_at", u1_registered_at),
|
|
74
|
+
AddEdge(u2, "registered_at", u2_registered_at),
|
|
75
|
+
)
|
|
76
|
+
env.commit()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quickstart: entities and environment
|
|
80
|
+
|
|
81
|
+
Operating with vertices and edges is tedious and slow. A higher-level abstraction, **entities**,
|
|
82
|
+
is provided to make working with graph more like your normal object-oriented programming.
|
|
83
|
+
|
|
84
|
+
Let's declare some entity classes and rewrite the code above:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from datetime import datetime
|
|
88
|
+
from saengra import primitive, Entity, Environment
|
|
89
|
+
|
|
90
|
+
@primitive
|
|
91
|
+
class user:
|
|
92
|
+
id: int
|
|
93
|
+
|
|
94
|
+
class User(Entity, user):
|
|
95
|
+
registered_at: datetime
|
|
96
|
+
follows: set["User"]
|
|
97
|
+
|
|
98
|
+
env = Environment(entity_types=[User])
|
|
99
|
+
|
|
100
|
+
u1 = User.create(env, id=1, registered_at=datetime(2022, 1, 1, 12, 0, 0))
|
|
101
|
+
u2 = User.create(env, id=2, registered_at=datetime(2023, 2, 3, 15, 0, 0))
|
|
102
|
+
u1.follows.add(u2)
|
|
103
|
+
|
|
104
|
+
env.commit()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Quickstart: expressions and observers
|
|
108
|
+
|
|
109
|
+
Saengra introduces a domain-specific language to describe subgraphs of the graph, i.e. subset of
|
|
110
|
+
vertices and edges. These expressions are quite similar to queries in SQL.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Find all subscriptions, i.e. pairs (u1, u2) where u1 follows u2:
|
|
114
|
+
env.match("user as u1 -follows> user as u2")
|
|
115
|
+
# -> [{"u1": User(id=1), "u2": User(id=2)}]
|
|
116
|
+
|
|
117
|
+
# Find all mutual subscriptions:
|
|
118
|
+
env.match("user as u1 <follows> user as u2")
|
|
119
|
+
# -> []
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
But the most powerful aspect of Saengra is its observation capability. Saengra can match
|
|
123
|
+
expressions incrementally after processing graph updates, and notify the program about created,
|
|
124
|
+
changed and deleted subgraphs after each commit.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from saengra import observer
|
|
128
|
+
|
|
129
|
+
mutual_follow = observer("user as u1 <follows> user as u2")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mutual_follow.on_create
|
|
133
|
+
def notify_mutuals(u1: User, u2: User):
|
|
134
|
+
print(f"{u1} is now mutuals with {u2}!")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
env.register_observers([mutual_follow])
|
|
138
|
+
|
|
139
|
+
u2.follows.add(u1)
|
|
140
|
+
env.commit()
|
|
141
|
+
# -> User(id=1) is now mutuals with User(id=2)!
|
|
142
|
+
# -> User(id=2) is now mutuals with User(id=1)!
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Generating Protobuf Code
|
|
146
|
+
|
|
147
|
+
The `messages_pb2.py` file is generated from the protobuf definitions in `saengra-server/proto/messages.proto`.
|
|
148
|
+
|
|
149
|
+
To regenerate:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
protoc --python_out=saengra --proto_path=saengra-server/proto saengra-server/proto/messages.proto
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Requirements:
|
|
156
|
+
- `protoc` (Protocol Buffers compiler) must be installed
|
|
157
|
+
- Python protobuf library: `pip install protobuf>=4.21.0`
|
|
158
|
+
|
|
159
|
+
## Usage
|
|
160
|
+
|
|
161
|
+
### Option 1: Automatically start server
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from saengra.client import SaengraClient
|
|
165
|
+
|
|
166
|
+
# Client automatically starts saengra-server in background
|
|
167
|
+
with SaengraClient() as client:
|
|
168
|
+
# Connect to a graph
|
|
169
|
+
created = client.connect("my_graph")
|
|
170
|
+
|
|
171
|
+
# Add vertices and edges
|
|
172
|
+
client.apply_updates([
|
|
173
|
+
# Your updates here
|
|
174
|
+
])
|
|
175
|
+
|
|
176
|
+
# Commit changes
|
|
177
|
+
response = client.commit()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The client expects `saengra-server` binary to be available in PATH.
|
|
181
|
+
|
|
182
|
+
### Option 2: Connect to existing server
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from saengra.client import SaengraClient
|
|
186
|
+
|
|
187
|
+
# Connect to an existing server socket
|
|
188
|
+
with SaengraClient(socket_path="/path/to/server.sock") as client:
|
|
189
|
+
# Connect to a graph
|
|
190
|
+
created = client.connect("my_graph")
|
|
191
|
+
|
|
192
|
+
# Work with the graph...
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
When using an existing socket, the client will not start or stop the server process, and will not clean up the socket file.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel", "cmake>=3.15"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "saengra"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reactive graph database with pattern matching"
|
|
9
|
+
readme = "saengra/README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Tigran Saluev", email = "tigran@saluev.com"}
|
|
13
|
+
]
|
|
14
|
+
keywords = ["graph", "database", "reactive", "pattern-matching", "entity"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: C++",
|
|
24
|
+
"Topic :: Database",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
requires-python = ">=3.10"
|
|
28
|
+
dependencies = [
|
|
29
|
+
"frozendict>=2.4.6",
|
|
30
|
+
"protobuf>=4.0",
|
|
31
|
+
"termcolor>=2.4.0"
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
socket = [] # TODO move protobuf dep here, make optional
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=7.0",
|
|
38
|
+
"pytest-cov",
|
|
39
|
+
"build",
|
|
40
|
+
"twine",
|
|
41
|
+
"cibuildwheel",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/Saluev/saengra"
|
|
46
|
+
Repository = "https://github.com/Saluev/saengra"
|
|
47
|
+
Issues = "https://github.com/Saluev/saengra/issues"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools]
|
|
50
|
+
packages = ["saengra", "saengra.utilities"]
|
|
51
|
+
package-dir = {"" = "."}
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.package-data]
|
|
54
|
+
saengra = ["*.so", "*.pyd", "*.dylib"]
|
|
55
|
+
|
|
56
|
+
# cibuildwheel configuration for multi-platform builds
|
|
57
|
+
[tool.cibuildwheel]
|
|
58
|
+
# Build for these Python versions
|
|
59
|
+
build = "cp310-* cp311-* cp312-*"
|
|
60
|
+
# Skip 32-bit builds and musllinux (alpine)
|
|
61
|
+
skip = "*-win32 *-manylinux_i686 *-musllinux_*"
|
|
62
|
+
|
|
63
|
+
[tool.cibuildwheel.linux]
|
|
64
|
+
# Install build dependencies before building
|
|
65
|
+
before-all = """
|
|
66
|
+
yum install -y build-essential make cmake git protobuf-devel boost-devel spdlog-devel bison flex || \
|
|
67
|
+
apt-get update && apt-get install -y --no-install-recommends build-essential make cmake git libprotobuf-dev protobuf-compiler libboost-dev libspdlog-dev bison flex
|
|
68
|
+
"""
|
|
69
|
+
# Build and install abseil from source
|
|
70
|
+
before-build = """
|
|
71
|
+
git clone --depth 1 --branch 20240116.2 https://github.com/abseil/abseil-cpp.git /tmp/abseil \
|
|
72
|
+
&& cd /tmp/abseil \
|
|
73
|
+
&& cmake -S . -B build \
|
|
74
|
+
-DCMAKE_BUILD_TYPE=Release \
|
|
75
|
+
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
|
76
|
+
-DABSL_ENABLE_INSTALL=ON \
|
|
77
|
+
-DABSL_USE_SYSTEM_INCLUDES=ON \
|
|
78
|
+
&& cmake --build build -j4 \
|
|
79
|
+
&& cmake --install build \
|
|
80
|
+
&& rm -rf /tmp/abseil
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
[tool.cibuildwheel.macos]
|
|
84
|
+
# Use Homebrew for dependencies
|
|
85
|
+
before-all = "brew install cmake protobuf boost spdlog abseil bison flex"
|
|
86
|
+
environment = { MACOSX_DEPLOYMENT_TARGET = "10.15" }
|
|
87
|
+
# Build for both Intel and ARM
|
|
88
|
+
archs = ["x86_64", "arm64"]
|
|
89
|
+
|
|
90
|
+
[tool.cibuildwheel.windows]
|
|
91
|
+
# Windows builds require vcpkg or pre-installed dependencies
|
|
92
|
+
before-all = """
|
|
93
|
+
choco install cmake git -y
|
|
94
|
+
"""
|
|
95
|
+
# Note: Windows builds are more complex due to dependencies
|
|
96
|
+
# Consider using vcpkg for abseil, protobuf, etc.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Saengra
|
|
2
|
+
|
|
3
|
+
Python wrapper for Saengra graph database.
|
|
4
|
+
|
|
5
|
+
## Quickstart: primitives and edges
|
|
6
|
+
|
|
7
|
+
Saengra is a graph database. It supports hashable Python objects (**primitives**) as graph
|
|
8
|
+
vertices. Built-in types like `int` or `str` can be used directly; also Saengra provides `@primitive`
|
|
9
|
+
decorator to declare dataclass-like types to be used as graph vertices.
|
|
10
|
+
|
|
11
|
+
Directed edges between primitives are always labelled with a string (**edge label**). It can
|
|
12
|
+
be an arbitrary string, but the engine is optimized to support limited number of different labels per graph.
|
|
13
|
+
There can't be two edges between the same two primitives with the same label.
|
|
14
|
+
|
|
15
|
+
We can construct a graph directly from primitives and edges by using elementary graph operations:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from saengra import primitive, Environment
|
|
20
|
+
from saengra.graph import AddVertex, AddEdge
|
|
21
|
+
|
|
22
|
+
@primitive
|
|
23
|
+
class user:
|
|
24
|
+
id: int
|
|
25
|
+
|
|
26
|
+
u1 = user(id=1)
|
|
27
|
+
u2 = user(id=2)
|
|
28
|
+
u1_registered_at = datetime(2022, 1, 1, 12, 0, 0)
|
|
29
|
+
u2_registered_at = datetime(2023, 2, 3, 15, 0, 0)
|
|
30
|
+
|
|
31
|
+
env = Environment()
|
|
32
|
+
env.update(
|
|
33
|
+
AddVertex(u1),
|
|
34
|
+
AddVertex(u2),
|
|
35
|
+
AddVertex(u1_registered_at),
|
|
36
|
+
AddVertex(u2_registered_at),
|
|
37
|
+
AddEdge(u1, "follows", u2),
|
|
38
|
+
AddEdge(u1, "registered_at", u1_registered_at),
|
|
39
|
+
AddEdge(u2, "registered_at", u2_registered_at),
|
|
40
|
+
)
|
|
41
|
+
env.commit()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart: entities and environment
|
|
45
|
+
|
|
46
|
+
Operating with vertices and edges is tedious and slow. A higher-level abstraction, **entities**,
|
|
47
|
+
is provided to make working with graph more like your normal object-oriented programming.
|
|
48
|
+
|
|
49
|
+
Let's declare some entity classes and rewrite the code above:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from datetime import datetime
|
|
53
|
+
from saengra import primitive, Entity, Environment
|
|
54
|
+
|
|
55
|
+
@primitive
|
|
56
|
+
class user:
|
|
57
|
+
id: int
|
|
58
|
+
|
|
59
|
+
class User(Entity, user):
|
|
60
|
+
registered_at: datetime
|
|
61
|
+
follows: set["User"]
|
|
62
|
+
|
|
63
|
+
env = Environment(entity_types=[User])
|
|
64
|
+
|
|
65
|
+
u1 = User.create(env, id=1, registered_at=datetime(2022, 1, 1, 12, 0, 0))
|
|
66
|
+
u2 = User.create(env, id=2, registered_at=datetime(2023, 2, 3, 15, 0, 0))
|
|
67
|
+
u1.follows.add(u2)
|
|
68
|
+
|
|
69
|
+
env.commit()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quickstart: expressions and observers
|
|
73
|
+
|
|
74
|
+
Saengra introduces a domain-specific language to describe subgraphs of the graph, i.e. subset of
|
|
75
|
+
vertices and edges. These expressions are quite similar to queries in SQL.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Find all subscriptions, i.e. pairs (u1, u2) where u1 follows u2:
|
|
79
|
+
env.match("user as u1 -follows> user as u2")
|
|
80
|
+
# -> [{"u1": User(id=1), "u2": User(id=2)}]
|
|
81
|
+
|
|
82
|
+
# Find all mutual subscriptions:
|
|
83
|
+
env.match("user as u1 <follows> user as u2")
|
|
84
|
+
# -> []
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
But the most powerful aspect of Saengra is its observation capability. Saengra can match
|
|
88
|
+
expressions incrementally after processing graph updates, and notify the program about created,
|
|
89
|
+
changed and deleted subgraphs after each commit.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from saengra import observer
|
|
93
|
+
|
|
94
|
+
mutual_follow = observer("user as u1 <follows> user as u2")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@mutual_follow.on_create
|
|
98
|
+
def notify_mutuals(u1: User, u2: User):
|
|
99
|
+
print(f"{u1} is now mutuals with {u2}!")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
env.register_observers([mutual_follow])
|
|
103
|
+
|
|
104
|
+
u2.follows.add(u1)
|
|
105
|
+
env.commit()
|
|
106
|
+
# -> User(id=1) is now mutuals with User(id=2)!
|
|
107
|
+
# -> User(id=2) is now mutuals with User(id=1)!
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Generating Protobuf Code
|
|
111
|
+
|
|
112
|
+
The `messages_pb2.py` file is generated from the protobuf definitions in `saengra-server/proto/messages.proto`.
|
|
113
|
+
|
|
114
|
+
To regenerate:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
protoc --python_out=saengra --proto_path=saengra-server/proto saengra-server/proto/messages.proto
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Requirements:
|
|
121
|
+
- `protoc` (Protocol Buffers compiler) must be installed
|
|
122
|
+
- Python protobuf library: `pip install protobuf>=4.21.0`
|
|
123
|
+
|
|
124
|
+
## Usage
|
|
125
|
+
|
|
126
|
+
### Option 1: Automatically start server
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from saengra.client import SaengraClient
|
|
130
|
+
|
|
131
|
+
# Client automatically starts saengra-server in background
|
|
132
|
+
with SaengraClient() as client:
|
|
133
|
+
# Connect to a graph
|
|
134
|
+
created = client.connect("my_graph")
|
|
135
|
+
|
|
136
|
+
# Add vertices and edges
|
|
137
|
+
client.apply_updates([
|
|
138
|
+
# Your updates here
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
# Commit changes
|
|
142
|
+
response = client.commit()
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The client expects `saengra-server` binary to be available in PATH.
|
|
146
|
+
|
|
147
|
+
### Option 2: Connect to existing server
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from saengra.client import SaengraClient
|
|
151
|
+
|
|
152
|
+
# Connect to an existing server socket
|
|
153
|
+
with SaengraClient(socket_path="/path/to/server.sock") as client:
|
|
154
|
+
# Connect to a graph
|
|
155
|
+
created = client.connect("my_graph")
|
|
156
|
+
|
|
157
|
+
# Work with the graph...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
When using an existing socket, the client will not start or stop the server process, and will not clean up the socket file.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Saengra Python client library."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from saengra.c_extension import DirectAdapter
|
|
6
|
+
from saengra.entity import primitive, Entity
|
|
7
|
+
from saengra.environment import Environment
|
|
8
|
+
from saengra.factory import EntityFactory
|
|
9
|
+
from saengra.observer import observer
|
|
10
|
+
from saengra.socket_adapter import SocketAdapter
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Iterable, Protocol, TypeAlias
|
|
2
|
+
|
|
3
|
+
from saengra.api import Observation, Observer
|
|
4
|
+
from saengra.graph import Update, Primitive, Edge, Subgraph
|
|
5
|
+
|
|
6
|
+
ShouldCommitAgain: TypeAlias = bool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Adapter(Protocol):
|
|
10
|
+
"""Protocol for adapters that communicate with the saengra graph engine."""
|
|
11
|
+
|
|
12
|
+
def update(
|
|
13
|
+
self, update_or_updates: Update | list[Update] | tuple[Update, ...]
|
|
14
|
+
) -> None: ...
|
|
15
|
+
|
|
16
|
+
def flush(self) -> None: ...
|
|
17
|
+
|
|
18
|
+
def commit(self) -> tuple[ShouldCommitAgain, list[Observation]]: ...
|
|
19
|
+
|
|
20
|
+
def rollback(self) -> None: ...
|
|
21
|
+
|
|
22
|
+
def find_vertices(self, *, with_type_name: str | None = None) -> list[Primitive]: ...
|
|
23
|
+
|
|
24
|
+
def find_edges(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
with_label: str | None = None,
|
|
28
|
+
from_vertices: list[Primitive] | tuple[Primitive, ...] | None = None,
|
|
29
|
+
to_vertices: list[Primitive] | tuple[Primitive, ...] | None = None,
|
|
30
|
+
) -> list[Edge]: ...
|
|
31
|
+
|
|
32
|
+
def find_all(self) -> tuple[list[Primitive], list[Edge]]: ...
|
|
33
|
+
|
|
34
|
+
def observe(self, observers: Iterable[Observer]) -> None: ...
|
|
35
|
+
|
|
36
|
+
def match(self, expression: str, *placeholder_values: Primitive) -> list[Subgraph]: ...
|
|
37
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from saengra import messages_pb2
|
|
5
|
+
from saengra.graph import Primitive, Edge, Subgraph
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Observer:
|
|
10
|
+
id: str
|
|
11
|
+
expression: str
|
|
12
|
+
placeholder_values: tuple[Primitive, ...] = field(default_factory=tuple)
|
|
13
|
+
on_create: bool = False
|
|
14
|
+
on_change: bool = False
|
|
15
|
+
on_delete: bool = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ObservationType(Enum):
|
|
19
|
+
ON_CREATE = messages_pb2.CommitResponse.Observation.ON_CREATE
|
|
20
|
+
ON_CHANGE = messages_pb2.CommitResponse.Observation.ON_CHANGE
|
|
21
|
+
ON_DELETE = messages_pb2.CommitResponse.Observation.ON_DELETE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class Observation:
|
|
26
|
+
observer_id: str
|
|
27
|
+
type: ObservationType
|
|
28
|
+
variables: dict[str, Primitive]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CommitStatus(Enum):
|
|
32
|
+
OK = messages_pb2.CommitResponse.OK
|
|
33
|
+
REQUIRES_REPEATED_COMMIT = messages_pb2.CommitResponse.REQUIRES_REPEATED_COMMIT
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class CommitResponse:
|
|
38
|
+
status: CommitStatus
|
|
39
|
+
observations: list[Observation]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class FindResponse:
|
|
44
|
+
vertices: list[Primitive]
|
|
45
|
+
edges: list[Edge]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class MatchResponse:
|
|
50
|
+
subgraphs: list[Subgraph]
|
|
Binary file
|