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.
Files changed (106) hide show
  1. saengra-0.1.0/CMakeLists.txt +46 -0
  2. saengra-0.1.0/LICENSE +21 -0
  3. saengra-0.1.0/MANIFEST.in +8 -0
  4. saengra-0.1.0/PKG-INFO +195 -0
  5. saengra-0.1.0/pyproject.toml +96 -0
  6. saengra-0.1.0/saengra/README.md +160 -0
  7. saengra-0.1.0/saengra/__init__.py +10 -0
  8. saengra-0.1.0/saengra/adapter.py +37 -0
  9. saengra-0.1.0/saengra/api.py +50 -0
  10. saengra-0.1.0/saengra/c_extension.cpython-311-darwin.so +0 -0
  11. saengra-0.1.0/saengra/client.py +384 -0
  12. saengra-0.1.0/saengra/conversions.py +165 -0
  13. saengra-0.1.0/saengra/entity.py +401 -0
  14. saengra-0.1.0/saengra/environment.py +258 -0
  15. saengra-0.1.0/saengra/errors.py +38 -0
  16. saengra-0.1.0/saengra/factory.py +43 -0
  17. saengra-0.1.0/saengra/frozendict.py +71 -0
  18. saengra-0.1.0/saengra/graph.py +74 -0
  19. saengra-0.1.0/saengra/messages_pb2.py +106 -0
  20. saengra-0.1.0/saengra/observer.py +162 -0
  21. saengra-0.1.0/saengra/socket_adapter.py +82 -0
  22. saengra-0.1.0/saengra/utilities/__init__.py +0 -0
  23. saengra-0.1.0/saengra/utilities/annotations.py +102 -0
  24. saengra-0.1.0/saengra/utilities/colors.py +37 -0
  25. saengra-0.1.0/saengra/utilities/containers.py +177 -0
  26. saengra-0.1.0/saengra/utilities/itertools.py +13 -0
  27. saengra-0.1.0/saengra/utilities/loggers.py +48 -0
  28. saengra-0.1.0/saengra/utilities/properties.py +261 -0
  29. saengra-0.1.0/saengra/utilities/typing.py +21 -0
  30. saengra-0.1.0/saengra-server/CMakeLists.txt +95 -0
  31. saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CMakeCXXCompiler.cmake +104 -0
  32. saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CMakeSystem.cmake +15 -0
  33. saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +949 -0
  34. saengra-0.1.0/saengra-server/build/CMakeFiles/4.1.2/CompilerIdCXX/apple-sdk.cpp +1 -0
  35. saengra-0.1.0/saengra-server/build/CMakeFiles/CMakeDirectoryInformation.cmake +16 -0
  36. saengra-0.1.0/saengra-server/build/CMakeFiles/Makefile.cmake +82 -0
  37. saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_core.dir/DependInfo.cmake +46 -0
  38. saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_core.dir/cmake_clean.cmake +48 -0
  39. saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_core.dir/cmake_clean_target.cmake +3 -0
  40. saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_server.dir/DependInfo.cmake +24 -0
  41. saengra-0.1.0/saengra-server/build/CMakeFiles/saengra_server.dir/cmake_clean.cmake +13 -0
  42. saengra-0.1.0/saengra-server/build/CTestTestfile.cmake +6 -0
  43. saengra-0.1.0/saengra-server/build/cmake_install.cmake +61 -0
  44. saengra-0.1.0/saengra-server/build/lexer.yy.cpp +1965 -0
  45. saengra-0.1.0/saengra-server/build/messages.pb.h +9862 -0
  46. saengra-0.1.0/saengra-server/build/parser.tab.cpp +1759 -0
  47. saengra-0.1.0/saengra-server/build/parser.tab.h +128 -0
  48. saengra-0.1.0/saengra-server/include/debug.h +138 -0
  49. saengra-0.1.0/saengra-server/include/edge.h +71 -0
  50. saengra-0.1.0/saengra-server/include/edges_container.h +240 -0
  51. saengra-0.1.0/saengra-server/include/expression.h +179 -0
  52. saengra-0.1.0/saengra-server/include/graph.h +120 -0
  53. saengra-0.1.0/saengra-server/include/incremental_matcher.h +69 -0
  54. saengra-0.1.0/saengra-server/include/logging.h +59 -0
  55. saengra-0.1.0/saengra-server/include/matcher.h +48 -0
  56. saengra-0.1.0/saengra-server/include/observable.h +234 -0
  57. saengra-0.1.0/saengra-server/include/observer_container.h +76 -0
  58. saengra-0.1.0/saengra-server/include/parser.h +23 -0
  59. saengra-0.1.0/saengra-server/include/query.h +17 -0
  60. saengra-0.1.0/saengra-server/include/queryset.h +94 -0
  61. saengra-0.1.0/saengra-server/include/refs.h +52 -0
  62. saengra-0.1.0/saengra-server/include/start_positions.h +25 -0
  63. saengra-0.1.0/saengra-server/include/subgraph.h +76 -0
  64. saengra-0.1.0/saengra-server/include/unix_socket_server.h +38 -0
  65. saengra-0.1.0/saengra-server/include/utility.h +23 -0
  66. saengra-0.1.0/saengra-server/include/vertex.h +91 -0
  67. saengra-0.1.0/saengra-server/include/vertices_container.h +71 -0
  68. saengra-0.1.0/saengra-server/include/worker.h +73 -0
  69. saengra-0.1.0/saengra-server/parser/lexer.l +73 -0
  70. saengra-0.1.0/saengra-server/parser/parser.y +296 -0
  71. saengra-0.1.0/saengra-server/proto/messages.proto +180 -0
  72. saengra-0.1.0/saengra-server/src/edge.cpp +41 -0
  73. saengra-0.1.0/saengra-server/src/edges_container.cpp +539 -0
  74. saengra-0.1.0/saengra-server/src/expression.cpp +274 -0
  75. saengra-0.1.0/saengra-server/src/graph.cpp +367 -0
  76. saengra-0.1.0/saengra-server/src/incremental_matcher.cpp +240 -0
  77. saengra-0.1.0/saengra-server/src/main.cpp +77 -0
  78. saengra-0.1.0/saengra-server/src/matcher.cpp +449 -0
  79. saengra-0.1.0/saengra-server/src/observable.cpp +73 -0
  80. saengra-0.1.0/saengra-server/src/observer_container.cpp +274 -0
  81. saengra-0.1.0/saengra-server/src/parser.cpp +83 -0
  82. saengra-0.1.0/saengra-server/src/refs.cpp +55 -0
  83. saengra-0.1.0/saengra-server/src/start_positions.cpp +151 -0
  84. saengra-0.1.0/saengra-server/src/unix_socket_server.cpp +119 -0
  85. saengra-0.1.0/saengra-server/src/vertex.cpp +27 -0
  86. saengra-0.1.0/saengra-server/src/vertices_container.cpp +168 -0
  87. saengra-0.1.0/saengra-server/src/worker.cpp +522 -0
  88. saengra-0.1.0/saengra-server/tests/test_edges_container.cpp +194 -0
  89. saengra-0.1.0/saengra-server/tests/test_graph.cpp +847 -0
  90. saengra-0.1.0/saengra-server/tests/test_matcher.cpp +348 -0
  91. saengra-0.1.0/saengra-server/tests/test_observables.cpp +29 -0
  92. saengra-0.1.0/saengra-server/tests/test_observers.cpp +163 -0
  93. saengra-0.1.0/saengra-server/tests/test_parser.cpp +263 -0
  94. saengra-0.1.0/saengra-server/tests/test_start_positions.cpp +299 -0
  95. saengra-0.1.0/saengra-server/tests/test_vertices_container.cpp +206 -0
  96. saengra-0.1.0/saengra.egg-info/PKG-INFO +195 -0
  97. saengra-0.1.0/saengra.egg-info/SOURCES.txt +127 -0
  98. saengra-0.1.0/saengra.egg-info/dependency_links.txt +1 -0
  99. saengra-0.1.0/saengra.egg-info/not-zip-safe +1 -0
  100. saengra-0.1.0/saengra.egg-info/requires.txt +12 -0
  101. saengra-0.1.0/saengra.egg-info/top_level.txt +1 -0
  102. saengra-0.1.0/setup.cfg +4 -0
  103. saengra-0.1.0/setup.py +98 -0
  104. saengra-0.1.0/src/adapter.cpp +959 -0
  105. saengra-0.1.0/tests/test_1_low_level_updates.py +222 -0
  106. 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]