coffy 0.1.2__tar.gz → 0.1.5__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 (40) hide show
  1. {coffy-0.1.2/coffy.egg-info → coffy-0.1.5}/PKG-INFO +30 -25
  2. {coffy-0.1.2 → coffy-0.1.5}/README.md +29 -24
  3. coffy-0.1.5/coffy/graph/__pycache__/graphdb_nx.cpython-312.pyc +0 -0
  4. coffy-0.1.5/coffy/graph/graph_tests.py +137 -0
  5. coffy-0.1.5/coffy/graph/graphdb_nx.py +379 -0
  6. coffy-0.1.5/coffy/nosql/__pycache__/engine.cpython-312.pyc +0 -0
  7. {coffy-0.1.2 → coffy-0.1.5}/coffy/nosql/engine.py +37 -15
  8. coffy-0.1.5/coffy/nosql/nosql_tests.py +104 -0
  9. {coffy-0.1.2 → coffy-0.1.5/coffy.egg-info}/PKG-INFO +30 -25
  10. {coffy-0.1.2 → coffy-0.1.5}/coffy.egg-info/SOURCES.txt +2 -0
  11. {coffy-0.1.2 → coffy-0.1.5}/setup.py +1 -1
  12. coffy-0.1.2/coffy/graph/__pycache__/graphdb_nx.cpython-312.pyc +0 -0
  13. coffy-0.1.2/coffy/graph/graphdb_nx.py +0 -125
  14. coffy-0.1.2/coffy/nosql/__pycache__/engine.cpython-312.pyc +0 -0
  15. {coffy-0.1.2 → coffy-0.1.5}/LICENSE +0 -0
  16. {coffy-0.1.2 → coffy-0.1.5}/MANIFEST.in +0 -0
  17. {coffy-0.1.2 → coffy-0.1.5}/coffy/__init__.py +0 -0
  18. {coffy-0.1.2 → coffy-0.1.5}/coffy/__pycache__/__init__.cpython-311.pyc +0 -0
  19. {coffy-0.1.2 → coffy-0.1.5}/coffy/__pycache__/__init__.cpython-312.pyc +0 -0
  20. {coffy-0.1.2 → coffy-0.1.5}/coffy/graph/__init__.py +0 -0
  21. {coffy-0.1.2 → coffy-0.1.5}/coffy/graph/__pycache__/__init__.cpython-312.pyc +0 -0
  22. {coffy-0.1.2 → coffy-0.1.5}/coffy/nosql/__init__.py +0 -0
  23. {coffy-0.1.2 → coffy-0.1.5}/coffy/nosql/__pycache__/__init__.cpython-311.pyc +0 -0
  24. {coffy-0.1.2 → coffy-0.1.5}/coffy/nosql/__pycache__/__init__.cpython-312.pyc +0 -0
  25. {coffy-0.1.2 → coffy-0.1.5}/coffy/nosql/__pycache__/engine.cpython-311.pyc +0 -0
  26. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__init__.py +0 -0
  27. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__pycache__/__init__.cpython-311.pyc +0 -0
  28. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__pycache__/__init__.cpython-312.pyc +0 -0
  29. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__pycache__/engine.cpython-311.pyc +0 -0
  30. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__pycache__/engine.cpython-312.pyc +0 -0
  31. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__pycache__/io.cpython-312.pyc +0 -0
  32. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__pycache__/sqldict.cpython-311.pyc +0 -0
  33. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/__pycache__/sqldict.cpython-312.pyc +0 -0
  34. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/engine.py +0 -0
  35. {coffy-0.1.2 → coffy-0.1.5}/coffy/sql/sqldict.py +0 -0
  36. {coffy-0.1.2 → coffy-0.1.5}/coffy.egg-info/dependency_links.txt +0 -0
  37. {coffy-0.1.2 → coffy-0.1.5}/coffy.egg-info/requires.txt +0 -0
  38. {coffy-0.1.2 → coffy-0.1.5}/coffy.egg-info/top_level.txt +0 -0
  39. {coffy-0.1.2 → coffy-0.1.5}/pyproject.toml +0 -0
  40. {coffy-0.1.2 → coffy-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coffy
3
- Version: 0.1.2
3
+ Version: 0.1.5
4
4
  Summary: Lightweight local NoSQL, SQL, and Graph embedded database engine
5
5
  Author: nsarathy
6
6
  Classifier: Programming Language :: Python :: 3
@@ -24,10 +24,15 @@ Dynamic: summary
24
24
  **Coffy** is a lightweight embedded database engine for Python, designed for local-first apps, scripts, and tools. It includes:
25
25
 
26
26
  - `coffy.nosql`: A simple JSON-backed NoSQL engine with a fluent, chainable query interface
27
- - `coffy.sql`: A wrapper over SQLite for executing raw SQL with clean tabular results
28
- - `coffy.graph`: An graph engine built on `networkx` with advanced filtering and logic-based querying
27
+ - `coffy.graph`: An graph engine built on `networkx` with advanced filtering and logic-based querying
28
+ - `coffy.sql`: A wrapper over `SQLite` for executing raw SQL
29
29
 
30
- No dependencies (except `networkx`). No boilerplate. Just data.
30
+ ---
31
+ ## Latest Updates
32
+ - Added projection and dot-notation support for nested fields in NoSQL queries. Expanded logic chaining (`_and`, `_or`, `_not`) with improved test and better query semantics.
33
+ - GraphDB now supports saving query results, fixed relationship type serialization, and added more robust unit coverage.
34
+ - Unit tests added to test NoSQL and Graph database features.
35
+ - Documentation Updated.
31
36
 
32
37
  ---
33
38
 
@@ -43,31 +48,31 @@ pip install coffy
43
48
 
44
49
  ### `coffy.nosql`
45
50
 
46
- - JSON-based collections with fluent `.where().eq().gt()...` query chaining
47
- - Joins, updates, filters, aggregation, export/import
48
- - All data saved to human-readable `.json` files
51
+ - Embedded NoSQL document store with a fluent, chainable query API
52
+ - Supports nested fields, logical filters, aggregations, projections, and joins
53
+ - Built for local usage with optional persistence; minimal setup, fast iteration
49
54
 
50
55
  📄 [NoSQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/NOSQL_DOCS.md)
51
56
 
52
57
  ---
53
58
 
54
- ### `coffy.sql`
59
+ ### `coffy.graph`
55
60
 
56
- - SQLite-backed engine with raw SQL query support
57
- - Outputs as readable tables or exportable lists
58
- - Uses in-memory DB by default, or json-based if initialized with a path
61
+ - Lightweight, file-backed graph database using `networkx` under the hood
62
+ - Supports pattern matching, label/type filtering, logical conditions, and projections
63
+ - Query results can be saved, updated, or transformed; ideal for local, schema-flexible graph data
59
64
 
60
- 📄 [SQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/SQL_DOCS.md)
65
+ 📄 [Graph Documentation →](https://github.com/nsarathy/Coffy/blob/main/GRAPH_DOCS.md)
61
66
 
62
67
  ---
63
68
 
64
- ### `coffy.graph`
69
+ ### `coffy.sql`
65
70
 
66
- - Wrapper around `networkx` with simplified node/relationship API
67
- - Query nodes and relationships using filters like `gt`, `lt`, `eq`, `or`, `not`
71
+ - SQLite-backed engine with raw SQL query support
72
+ - Outputs as readable tables or exportable lists
68
73
  - Uses in-memory DB by default, or json-based if initialized with a path
69
74
 
70
- 📄 [Graph Documentation →](https://github.com/nsarathy/Coffy/blob/main/GRAPH_DOCS.md)
75
+ 📄 [SQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/SQL_DOCS.md)
71
76
 
72
77
  ---
73
78
 
@@ -81,15 +86,6 @@ users.add({"id": 1, "name": "Neel"})
81
86
  print(users.where("name").eq("Neel").first())
82
87
  ```
83
88
 
84
- ```python
85
- from coffy.sql import init, query
86
-
87
- init("app.db")
88
- query("CREATE TABLE test (id INT, name TEXT)")
89
- query("INSERT INTO test VALUES (1, 'Neel')")
90
- print(query("SELECT * FROM test"))
91
- ```
92
-
93
89
  ```python
94
90
  from coffy.graph import GraphDB
95
91
 
@@ -99,6 +95,15 @@ g.add_relationships([{"source": 1, "target": 2, "type": "friend"}])
99
95
  print(g.find_relationships(type="friend"))
100
96
  ```
101
97
 
98
+ ```python
99
+ from coffy.sql import init, query
100
+
101
+ init("app.db")
102
+ query("CREATE TABLE test (id INT, name TEXT)")
103
+ query("INSERT INTO test VALUES (1, 'Neel')")
104
+ print(query("SELECT * FROM test"))
105
+ ```
106
+
102
107
  ---
103
108
 
104
109
  ## 📄 License
@@ -3,10 +3,15 @@
3
3
  **Coffy** is a lightweight embedded database engine for Python, designed for local-first apps, scripts, and tools. It includes:
4
4
 
5
5
  - `coffy.nosql`: A simple JSON-backed NoSQL engine with a fluent, chainable query interface
6
- - `coffy.sql`: A wrapper over SQLite for executing raw SQL with clean tabular results
7
- - `coffy.graph`: An graph engine built on `networkx` with advanced filtering and logic-based querying
6
+ - `coffy.graph`: An graph engine built on `networkx` with advanced filtering and logic-based querying
7
+ - `coffy.sql`: A wrapper over `SQLite` for executing raw SQL
8
8
 
9
- No dependencies (except `networkx`). No boilerplate. Just data.
9
+ ---
10
+ ## Latest Updates
11
+ - Added projection and dot-notation support for nested fields in NoSQL queries. Expanded logic chaining (`_and`, `_or`, `_not`) with improved test and better query semantics.
12
+ - GraphDB now supports saving query results, fixed relationship type serialization, and added more robust unit coverage.
13
+ - Unit tests added to test NoSQL and Graph database features.
14
+ - Documentation Updated.
10
15
 
11
16
  ---
12
17
 
@@ -22,31 +27,31 @@ pip install coffy
22
27
 
23
28
  ### `coffy.nosql`
24
29
 
25
- - JSON-based collections with fluent `.where().eq().gt()...` query chaining
26
- - Joins, updates, filters, aggregation, export/import
27
- - All data saved to human-readable `.json` files
30
+ - Embedded NoSQL document store with a fluent, chainable query API
31
+ - Supports nested fields, logical filters, aggregations, projections, and joins
32
+ - Built for local usage with optional persistence; minimal setup, fast iteration
28
33
 
29
34
  📄 [NoSQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/NOSQL_DOCS.md)
30
35
 
31
36
  ---
32
37
 
33
- ### `coffy.sql`
38
+ ### `coffy.graph`
34
39
 
35
- - SQLite-backed engine with raw SQL query support
36
- - Outputs as readable tables or exportable lists
37
- - Uses in-memory DB by default, or json-based if initialized with a path
40
+ - Lightweight, file-backed graph database using `networkx` under the hood
41
+ - Supports pattern matching, label/type filtering, logical conditions, and projections
42
+ - Query results can be saved, updated, or transformed; ideal for local, schema-flexible graph data
38
43
 
39
- 📄 [SQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/SQL_DOCS.md)
44
+ 📄 [Graph Documentation →](https://github.com/nsarathy/Coffy/blob/main/GRAPH_DOCS.md)
40
45
 
41
46
  ---
42
47
 
43
- ### `coffy.graph`
48
+ ### `coffy.sql`
44
49
 
45
- - Wrapper around `networkx` with simplified node/relationship API
46
- - Query nodes and relationships using filters like `gt`, `lt`, `eq`, `or`, `not`
50
+ - SQLite-backed engine with raw SQL query support
51
+ - Outputs as readable tables or exportable lists
47
52
  - Uses in-memory DB by default, or json-based if initialized with a path
48
53
 
49
- 📄 [Graph Documentation →](https://github.com/nsarathy/Coffy/blob/main/GRAPH_DOCS.md)
54
+ 📄 [SQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/SQL_DOCS.md)
50
55
 
51
56
  ---
52
57
 
@@ -60,15 +65,6 @@ users.add({"id": 1, "name": "Neel"})
60
65
  print(users.where("name").eq("Neel").first())
61
66
  ```
62
67
 
63
- ```python
64
- from coffy.sql import init, query
65
-
66
- init("app.db")
67
- query("CREATE TABLE test (id INT, name TEXT)")
68
- query("INSERT INTO test VALUES (1, 'Neel')")
69
- print(query("SELECT * FROM test"))
70
- ```
71
-
72
68
  ```python
73
69
  from coffy.graph import GraphDB
74
70
 
@@ -78,6 +74,15 @@ g.add_relationships([{"source": 1, "target": 2, "type": "friend"}])
78
74
  print(g.find_relationships(type="friend"))
79
75
  ```
80
76
 
77
+ ```python
78
+ from coffy.sql import init, query
79
+
80
+ init("app.db")
81
+ query("CREATE TABLE test (id INT, name TEXT)")
82
+ query("INSERT INTO test VALUES (1, 'Neel')")
83
+ print(query("SELECT * FROM test"))
84
+ ```
85
+
81
86
  ---
82
87
 
83
88
  ## 📄 License
@@ -0,0 +1,137 @@
1
+ import json
2
+ import unittest
3
+ import tempfile
4
+ import os
5
+ from graphdb_nx import GraphDB
6
+
7
+
8
+ class TestGraphDB(unittest.TestCase):
9
+
10
+ def setUp(self):
11
+ self.temp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".json").name
12
+ self.db = GraphDB(path=self.temp_path)
13
+ self.db.add_node("A", labels="Person", name="Alice", age=30)
14
+ self.db.add_node("B", labels="Person", name="Bob", age=25)
15
+ self.db.add_node("C", labels="Person", name="Carol", age=40)
16
+ self.db.add_relationship("A", "B", rel_type="KNOWS", since=2010)
17
+ self.db.add_relationship("B", "C", rel_type="KNOWS", since=2015)
18
+
19
+ def tearDown(self):
20
+ os.remove(self.temp_path)
21
+
22
+ def test_add_and_get_node(self):
23
+ self.db.add_node("D", labels="Person", name="Dan")
24
+ node = self.db.get_node("D")
25
+ self.assertEqual(node["name"], "Dan")
26
+
27
+ def test_remove_node(self):
28
+ self.db.remove_node("C")
29
+ self.assertFalse(self.db.has_node("C"))
30
+
31
+ def test_add_and_get_relationship(self):
32
+ rel = self.db.get_relationship("A", "B")
33
+ self.assertEqual(rel["_type"], "KNOWS")
34
+
35
+ def test_update_node(self):
36
+ self.db.update_node("A", age=35, city="Wonderland")
37
+ node = self.db.get_node("A")
38
+ self.assertEqual(node["age"], 35)
39
+ self.assertEqual(node["city"], "Wonderland")
40
+
41
+ def test_update_relationship(self):
42
+ self.db.update_relationship("A", "B", since=2020, weight=0.8)
43
+ rel = self.db.get_relationship("A", "B")
44
+ self.assertEqual(rel["since"], 2020)
45
+ self.assertEqual(rel["weight"], 0.8)
46
+
47
+ def test_set_node_update(self):
48
+ self.db.set_node("A", name="Alicia", mood="happy")
49
+ node = self.db.get_node("A")
50
+ self.assertEqual(node["name"], "Alicia")
51
+ self.assertEqual(node["mood"], "happy")
52
+
53
+ def test_set_node_add(self):
54
+ self.db.set_node("Z", labels="Robot", name="Zeta", age=5)
55
+ self.assertTrue(self.db.has_node("Z"))
56
+ node = self.db.get_node("Z")
57
+ self.assertEqual(node["name"], "Zeta")
58
+ self.assertEqual(node["age"], 5)
59
+ self.assertIn("Robot", node["_labels"])
60
+
61
+ def test_remove_relationship(self):
62
+ self.db.remove_relationship("A", "B")
63
+ self.assertFalse(self.db.has_relationship("A", "B"))
64
+
65
+ def test_find_nodes_basic(self):
66
+ results = self.db.find_nodes(name="Alice")
67
+ self.assertEqual(len(results), 1)
68
+ self.assertEqual(results[0]["name"], "Alice")
69
+
70
+ def test_find_nodes_logic_or(self):
71
+ results = self.db.find_nodes(_logic="or", name="Alice", age={"gt": 35})
72
+ names = {r["name"] for r in results}
73
+ self.assertIn("Alice", names)
74
+ self.assertIn("Carol", names)
75
+
76
+ def test_find_nodes_logic_not(self):
77
+ results = self.db.find_nodes(_logic="not", age={"lt": 35})
78
+ self.assertEqual(len(results), 1)
79
+ self.assertEqual(results[0]["name"], "Carol")
80
+
81
+ def test_find_by_label(self):
82
+ results = self.db.find_by_label("Person")
83
+ self.assertEqual(len(results), 3)
84
+
85
+ def test_find_relationships_type_and_filter(self):
86
+ results = self.db.find_relationships(rel_type="KNOWS", since={"gte": 2011})
87
+ self.assertEqual(len(results), 1)
88
+ self.assertEqual(results[0]["target"], "C")
89
+
90
+ def test_project_node_fields(self):
91
+ result = self.db.project_node("A", fields=["name"])
92
+ self.assertEqual(result, {"name": "Alice"})
93
+
94
+ def test_project_relationship_fields(self):
95
+ result = self.db.project_relationship("A", "B", fields=["since"])
96
+ self.assertEqual(result, {"since": 2010})
97
+
98
+ def test_match_node_path(self):
99
+ pattern = [{"rel_type": "KNOWS", "node": {"name": "Bob"}}]
100
+ paths = self.db.match_node_path(start={"name": "Alice"}, pattern=pattern)
101
+ self.assertEqual(len(paths), 1)
102
+ self.assertEqual(paths[0][0]["name"], "Alice")
103
+ self.assertEqual(paths[0][1]["name"], "Bob")
104
+
105
+ def test_match_full_path(self):
106
+ pattern = [{"rel_type": "KNOWS", "node": {"name": "Bob"}}, {"rel_type": "KNOWS", "node": {"name": "Carol"}}]
107
+ results = self.db.match_full_path(start={"name": "Alice"}, pattern=pattern)
108
+ self.assertEqual(len(results), 1)
109
+ self.assertEqual(results[0]["nodes"][2]["name"], "Carol")
110
+ self.assertEqual(results[0]["relationships"][0]["type"], "KNOWS")
111
+
112
+ def test_match_path_structured(self):
113
+ pattern = [{"rel_type": "KNOWS", "node": {"name": "Bob"}}]
114
+ result = self.db.match_path_structured(start={"name": "Alice"}, pattern=pattern)
115
+ self.assertEqual(len(result), 1)
116
+ path = result[0]["path"]
117
+ self.assertEqual(path[0]["node"]["name"], "Alice")
118
+ self.assertEqual(path[1]["relationship"]["type"], "KNOWS")
119
+ self.assertEqual(path[2]["node"]["name"], "Bob")
120
+
121
+ def test_save_and_load(self):
122
+ self.db.save()
123
+ new_db = GraphDB(path=self.temp_path)
124
+ self.assertTrue(new_db.has_node("A"))
125
+ self.assertTrue(new_db.has_relationship("A", "B"))
126
+
127
+ def test_save_query_result(self):
128
+ result = self.db.find_nodes(name="Alice")
129
+ temp_result_path = self.temp_path.replace(".json", "_result.json")
130
+ self.db.save_query_result(result, path=temp_result_path)
131
+ with open(temp_result_path) as f:
132
+ loaded = json.load(f)
133
+ self.assertEqual(loaded[0]["name"], "Alice")
134
+ os.remove(temp_result_path)
135
+
136
+
137
+ unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromTestCase(TestGraphDB))
@@ -0,0 +1,379 @@
1
+ # coffy/graph/graphdb_nx.py
2
+ # author: nsarathy
3
+
4
+ import networkx as nx
5
+ import json
6
+ import os
7
+
8
+ class GraphDB:
9
+
10
+ def __init__(self, directed=False, path=None):
11
+ self.g = nx.DiGraph() if directed else nx.Graph()
12
+ self.directed = directed
13
+ self.in_memory = path == ":memory:"
14
+
15
+ if path:
16
+ if not path.endswith(".json"):
17
+ raise ValueError("Path must be to a .json file")
18
+ self.path = path
19
+ else:
20
+ self.in_memory = True
21
+ if not self.in_memory and os.path.exists(self.path):
22
+ self.load(self.path)
23
+ elif not self.in_memory:
24
+ os.makedirs(os.path.dirname(self.path), exist_ok=True)
25
+ self.save(self.path)
26
+
27
+
28
+ # Node operations
29
+ def add_node(self, node_id, labels=None, **attrs):
30
+ if labels is not None:
31
+ attrs["_labels"] = labels if isinstance(labels, list) else [labels]
32
+ self.g.add_node(node_id, **attrs)
33
+ self._persist()
34
+
35
+ def add_nodes(self, nodes):
36
+ for node in nodes:
37
+ node_id = node["id"]
38
+ labels = node.get("labels") or node.get("_labels") # Accept either form
39
+ attrs = {k: v for k, v in node.items() if k not in ["id", "labels", "_labels"]}
40
+ self.add_node(node_id, labels=labels, **attrs)
41
+
42
+ def get_node(self, node_id):
43
+ return self.g.nodes[node_id]
44
+
45
+ def _get_neighbors(self, node_id, direction):
46
+ if self.directed:
47
+ if direction == "out":
48
+ return self.g.successors(node_id)
49
+ elif direction == "in":
50
+ return self.g.predecessors(node_id)
51
+ elif direction == "any":
52
+ return set(self.g.successors(node_id)).union(self.g.predecessors(node_id))
53
+ else:
54
+ raise ValueError("Direction must be 'in', 'out', or 'any'")
55
+ else:
56
+ return self.g.neighbors(node_id)
57
+
58
+ def remove_node(self, node_id):
59
+ self.g.remove_node(node_id)
60
+ self._persist()
61
+
62
+ # Relationship (edge) operations
63
+ def add_relationship(self, source, target, rel_type=None, **attrs):
64
+ if rel_type:
65
+ attrs["_type"] = rel_type
66
+ self.g.add_edge(source, target, **attrs)
67
+ self._persist()
68
+
69
+ def add_relationships(self, relationships):
70
+ for rel in relationships:
71
+ source = rel["source"]
72
+ target = rel["target"]
73
+ rel_type = rel.get("type") or rel.get("_type")
74
+ attrs = {k: v for k, v in rel.items() if k not in ["source", "target", "type", "_type"]}
75
+ self.add_relationship(source, target, rel_type=rel_type, **attrs)
76
+
77
+ def get_relationship(self, source, target):
78
+ return self.g.get_edge_data(source, target)
79
+
80
+ def remove_relationship(self, source, target):
81
+ self.g.remove_edge(source, target)
82
+ self._persist()
83
+
84
+ # Basic queries
85
+ def neighbors(self, node_id):
86
+ return list(self.g.neighbors(node_id))
87
+
88
+ def degree(self, node_id):
89
+ return self.g.degree[node_id]
90
+
91
+ def has_node(self, node_id):
92
+ return self.g.has_node(node_id)
93
+
94
+ def has_relationship(self, u, v):
95
+ return self.g.has_edge(u, v)
96
+
97
+ def update_node(self, node_id, **attrs):
98
+ if not self.has_node(node_id):
99
+ raise KeyError(f"Node '{node_id}' does not exist.")
100
+ self.g.nodes[node_id].update(attrs)
101
+ self._persist()
102
+
103
+ def update_relationship(self, source, target, **attrs):
104
+ if not self.has_relationship(source, target):
105
+ raise KeyError(f"Relationship '{source}->{target}' does not exist.")
106
+ self.g.edges[source, target].update(attrs)
107
+ self._persist()
108
+
109
+ def set_node(self, node_id, labels=None, **attrs):
110
+ if self.has_node(node_id):
111
+ self.update_node(node_id, **attrs)
112
+ else:
113
+ self.add_node(node_id, labels=labels, **attrs)
114
+ self._persist()
115
+
116
+ # Advanced node search
117
+ def project_node(self, node_id, fields=None):
118
+ if not self.has_node(node_id):
119
+ return None
120
+ node = self.get_node(node_id).copy()
121
+ node["id"] = node_id
122
+ if fields is None:
123
+ return node
124
+ return {k: node[k] for k in fields if k in node}
125
+
126
+ def project_relationship(self, source, target, fields=None):
127
+ if not self.has_relationship(source, target):
128
+ return None
129
+ rel = self.get_relationship(source, target).copy()
130
+ rel.update({"source": source, "target": target, "type": rel.get("_type")})
131
+ if fields is None:
132
+ return rel
133
+ return {k: rel[k] for k in fields if k in rel}
134
+
135
+ def find_nodes(self, label=None, fields=None, **conditions):
136
+ return [
137
+ self.project_node(n, fields) for n, a in self.g.nodes(data=True)
138
+ if (label is None or label in a.get("_labels", [])) and self._match_conditions(a, conditions)
139
+ ]
140
+
141
+ def find_by_label(self, label, fields=None):
142
+ return [
143
+ self.project_node(n, fields) for n, a in self.g.nodes(data=True)
144
+ if label in a.get("_labels", [])
145
+ ]
146
+
147
+ def find_relationships(self, rel_type=None, fields=None, **conditions):
148
+ return [
149
+ self.project_relationship(u, v, fields) for u, v, a in self.g.edges(data=True)
150
+ if (rel_type is None or a.get("_type") == rel_type) and self._match_conditions(a, conditions)
151
+ ]
152
+
153
+ def find_by_relationship_type(self, rel_type, fields=None):
154
+ return [
155
+ self.project_relationship(u, v, fields) for u, v, a in self.g.edges(data=True)
156
+ if a.get("_type") == rel_type
157
+ ]
158
+
159
+ def _match_conditions(self, attrs, conditions):
160
+ if not conditions:
161
+ return True
162
+ logic = conditions.get("_logic", "and")
163
+ conditions = {k: v for k, v in conditions.items() if k != "_logic"}
164
+ results = []
165
+
166
+ for key, expected in conditions.items():
167
+ actual = attrs.get(key)
168
+ if isinstance(expected, dict):
169
+ for op, val in expected.items():
170
+ if op == "gt": results.append(actual > val)
171
+ elif op == "lt": results.append(actual < val)
172
+ elif op == "gte": results.append(actual >= val)
173
+ elif op == "lte": results.append(actual <= val)
174
+ elif op == "ne": results.append(actual != val)
175
+ elif op == "eq": results.append(actual == val)
176
+ else: results.append(False)
177
+ else:
178
+ results.append(actual == expected)
179
+
180
+ if logic == "or":
181
+ return any(results)
182
+ elif logic == "not":
183
+ return not all(results)
184
+ return all(results)
185
+
186
+ def match_node_path(self, start, pattern, return_nodes=True, node_fields=None, direction="out"):
187
+ start_nodes = self.find_nodes(**start)
188
+ node_paths = []
189
+
190
+ for s in start_nodes:
191
+ self._match_node_path(
192
+ current_id=s["id"],
193
+ pattern=pattern,
194
+ node_path=[s["id"]],
195
+ node_paths=node_paths,
196
+ direction=direction
197
+ )
198
+
199
+ unique_paths = list({tuple(p) for p in node_paths})
200
+
201
+ if return_nodes:
202
+ return [
203
+ [self.project_node(n, node_fields) for n in path]
204
+ for path in unique_paths
205
+ ]
206
+ return unique_paths
207
+
208
+
209
+ def _match_node_path(self, current_id, pattern, node_path, node_paths, direction):
210
+ if not pattern:
211
+ node_paths.append(node_path)
212
+ return
213
+
214
+ step = pattern[0]
215
+ rel_type = step.get("rel_type")
216
+ next_node_cond = step.get("node", {})
217
+
218
+ for neighbor in self._get_neighbors(current_id, direction):
219
+ rel = self.get_relationship(current_id, neighbor)
220
+ if rel_type and rel.get("_type") != rel_type:
221
+ continue
222
+ node_attrs = self.get_node(neighbor)
223
+ if not self._match_conditions(node_attrs, next_node_cond):
224
+ continue
225
+ if neighbor in node_path: # avoid cycles
226
+ continue
227
+ self._match_node_path(neighbor, pattern[1:], node_path + [neighbor], node_paths, direction)
228
+
229
+ def match_full_path(self, start, pattern, node_fields=None, rel_fields=None, direction="out"):
230
+ start_nodes = self.find_nodes(**start)
231
+ matched_paths = []
232
+
233
+ for s in start_nodes:
234
+ self._match_full_path(
235
+ current_id=s["id"],
236
+ pattern=pattern,
237
+ relationship_path=[],
238
+ node_path=[s["id"]],
239
+ matched_paths=matched_paths,
240
+ direction=direction
241
+ )
242
+
243
+ return [
244
+ {
245
+ "nodes": [self.project_node(n, node_fields) for n in nodes],
246
+ "relationships": [self.project_relationship(u, v, rel_fields) for u, v in path]
247
+ }
248
+ for path, nodes in matched_paths
249
+ ]
250
+
251
+ def _match_full_path(self, current_id, pattern, relationship_path, node_path, matched_paths, direction):
252
+ if not pattern:
253
+ matched_paths.append((relationship_path, node_path))
254
+ return
255
+
256
+ step = pattern[0]
257
+ rel_type = step.get("rel_type")
258
+ next_node_cond = step.get("node", {})
259
+
260
+ for neighbor in self._get_neighbors(current_id, direction):
261
+ if neighbor in node_path:
262
+ continue
263
+ rel = self.get_relationship(current_id, neighbor)
264
+ if rel_type and rel.get("_type") != rel_type:
265
+ continue
266
+ if not self._match_conditions(self.get_node(neighbor), next_node_cond):
267
+ continue
268
+
269
+ self._match_full_path(
270
+ neighbor,
271
+ pattern[1:],
272
+ relationship_path + [(current_id, neighbor)],
273
+ node_path + [neighbor],
274
+ matched_paths,
275
+ direction
276
+ )
277
+
278
+ def match_path_structured(self, start, pattern, node_fields=None, rel_fields=None, direction="out"):
279
+ start_nodes = self.find_nodes(**start)
280
+ structured_paths = []
281
+
282
+ for s in start_nodes:
283
+ self._match_structured_path(
284
+ current_id=s["id"],
285
+ pattern=pattern,
286
+ path=[{"node": self.project_node(s["id"], node_fields)}],
287
+ structured_paths=structured_paths,
288
+ direction=direction
289
+ )
290
+
291
+ return structured_paths
292
+
293
+ def _match_structured_path(self, current_id, pattern, path, structured_paths, direction):
294
+ if not pattern:
295
+ structured_paths.append({"path": path})
296
+ return
297
+
298
+ step = pattern[0]
299
+ rel_type = step.get("rel_type")
300
+ next_node_cond = step.get("node", {})
301
+
302
+ for neighbor in self._get_neighbors(current_id, direction):
303
+ if any(e.get("node", {}).get("id") == neighbor for e in path):
304
+ continue # Avoid cycles
305
+
306
+ rel = self.get_relationship(current_id, neighbor)
307
+ if rel_type and rel.get("_type") != rel_type:
308
+ continue
309
+ if not self._match_conditions(self.get_node(neighbor), next_node_cond):
310
+ continue
311
+
312
+ extended_path = path + [
313
+ {"relationship": self.project_relationship(current_id, neighbor)},
314
+ {"node": self.project_node(neighbor)}
315
+ ]
316
+
317
+ self._match_structured_path(
318
+ neighbor,
319
+ pattern[1:],
320
+ extended_path,
321
+ structured_paths,
322
+ direction
323
+ )
324
+
325
+ # Export
326
+ def nodes(self):
327
+ return [{"id": n, "labels": a.get("_labels", []), **{k: v for k, v in a.items() if k != "_labels"}} for n, a in self.g.nodes(data=True)]
328
+
329
+ def relationships(self):
330
+ return [
331
+ {
332
+ "source": u,
333
+ "target": v,
334
+ "type": a.get("_type"),
335
+ **{k: v for k, v in a.items() if k != "_type"}
336
+ }
337
+ for u, v, a in self.g.edges(data=True)
338
+ ]
339
+
340
+ def to_dict(self):
341
+ return {
342
+ "nodes": self.nodes(),
343
+ "relationships": self.relationships()
344
+ }
345
+
346
+ def save(self, path=None):
347
+ path = path or self.path
348
+ if not path:
349
+ raise ValueError("No path specified to save the graph.")
350
+ with open(path, "w", encoding="utf-8") as f:
351
+ json.dump(self.to_dict(), f, indent=4)
352
+
353
+ def load(self, path=None):
354
+ path = path or self.path
355
+ if not path:
356
+ raise ValueError("No path specified to load the graph.")
357
+ if os.path.getsize(path) == 0:
358
+ return
359
+ with open(path, "r", encoding="utf-8") as f:
360
+ data = json.load(f)
361
+ self.g.clear()
362
+ for node in data.get("nodes", []):
363
+ self.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
364
+ for rel in data.get("relationships", []):
365
+ self.add_relationship(
366
+ rel["source"], rel["target"],
367
+ rel_type=rel.get("type") or rel.get("_type"),
368
+ **{k: v for k, v in rel.items() if k not in ["source", "target", "type", "_type"]}
369
+ )
370
+
371
+ def save_query_result(self, result, path=None):
372
+ if path is None:
373
+ raise ValueError("No path specified to save the query result.")
374
+ with open(path, "w", encoding="utf-8") as f:
375
+ json.dump(result, f, indent=4)
376
+
377
+ def _persist(self):
378
+ if not self.in_memory:
379
+ self.save(self.path)
@@ -13,47 +13,57 @@ class QueryBuilder:
13
13
  self.all_collections = all_collections or {}
14
14
  self._lookup_done = False
15
15
  self._lookup_results = None
16
+
17
+ @staticmethod
18
+ def _get_nested(doc, dotted_key):
19
+ keys = dotted_key.split('.')
20
+ for k in keys:
21
+ if not isinstance(doc, dict) or k not in doc:
22
+ return None
23
+ doc = doc[k]
24
+ return doc
16
25
 
17
26
  def where(self, field):
18
27
  self.current_field = field
19
28
  return self
20
29
 
21
30
  # Comparison
22
- def eq(self, value): return self._add_filter(lambda d: d.get(self.current_field) == value)
23
- def ne(self, value): return self._add_filter(lambda d: d.get(self.current_field) != value)
31
+ def eq(self, value): return self._add_filter(lambda d: QueryBuilder._get_nested(d, self.current_field) == value)
32
+ def ne(self, value): return self._add_filter(lambda d: QueryBuilder._get_nested(d, self.current_field) != value)
24
33
  def gt(self, value):
25
34
  return self._add_filter(
26
- lambda d: isinstance(d.get(self.current_field), (int, float)) and d.get(self.current_field) > value
35
+ lambda d: isinstance(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) > value
27
36
  )
28
37
 
29
38
  def gte(self, value):
30
39
  return self._add_filter(
31
- lambda d: isinstance(d.get(self.current_field), (int, float)) and d.get(self.current_field) >= value
40
+ lambda d: isinstance(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) >= value
32
41
  )
33
42
 
34
43
  def lt(self, value):
35
44
  return self._add_filter(
36
- lambda d: isinstance(d.get(self.current_field), (int, float)) and d.get(self.current_field) < value
45
+ lambda d: isinstance(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) < value
37
46
  )
38
47
 
39
48
  def lte(self, value):
40
49
  return self._add_filter(
41
- lambda d: isinstance(d.get(self.current_field), (int, float)) and d.get(self.current_field) <= value
50
+ lambda d: isinstance(QueryBuilder._get_nested(d, self.current_field), (int, float)) and QueryBuilder._get_nested(d, self.current_field) <= value
42
51
  )
43
52
 
44
53
  def in_(self, values):
45
54
  return self._add_filter(
46
- lambda d: d.get(self.current_field) in values
55
+ lambda d: QueryBuilder._get_nested(d, self.current_field) in values
47
56
  )
48
57
 
49
58
  def nin(self, values):
50
59
  return self._add_filter(
51
- lambda d: d.get(self.current_field) not in values
60
+ lambda d: QueryBuilder._get_nested(d, self.current_field) not in values
52
61
  )
53
62
 
54
- def matches(self, regex): return self._add_filter(lambda d: re.search(regex, str(d.get(self.current_field))))
63
+ def matches(self, regex): return self._add_filter(lambda d: re.search(regex, str(QueryBuilder._get_nested(d, self.current_field))))
55
64
 
56
- def exists(self): return self._add_filter(lambda d: self.current_field in d)
65
+ def exists(self):
66
+ return self._add_filter(lambda d: QueryBuilder._get_nested(d, self.current_field) is not None)
57
67
 
58
68
  # Logic grouping
59
69
  def _and(self, *fns):
@@ -87,12 +97,24 @@ class QueryBuilder:
87
97
  return self
88
98
 
89
99
  # Core execution
90
- def run(self):
100
+ def run(self, fields=None):
91
101
  results = [doc for doc in self.documents if all(f(doc) for f in self.filters)]
92
102
  if self._lookup_done:
93
103
  results = self._lookup_results
104
+
105
+ if fields is not None:
106
+ projected = []
107
+ for doc in results:
108
+ proj = {}
109
+ for f in fields:
110
+ value = QueryBuilder._get_nested(doc, f)
111
+ proj[f] = value
112
+ projected.append(proj)
113
+ return DocList(projected)
114
+
94
115
  return DocList(results)
95
116
 
117
+
96
118
  def update(self, changes):
97
119
  count = 0
98
120
  for doc in self.documents:
@@ -165,18 +187,17 @@ class QueryBuilder:
165
187
  _collection_registry = {}
166
188
 
167
189
  class CollectionManager:
168
- DEFAULT_DIR = os.path.join(os.getcwd(), "nosql_data")
169
190
 
170
191
  def __init__(self, name: str, path: str = None):
171
192
  self.name = name
172
193
  self.in_memory = False
173
194
 
174
195
  if path:
196
+ if not path.endswith(".json"):
197
+ raise ValueError("Path must be to a .json file")
175
198
  self.path = path
176
199
  else:
177
- os.makedirs(self.DEFAULT_DIR, exist_ok=True)
178
- self.path = os.path.join(self.DEFAULT_DIR, f"{name}.json")
179
- self.in_memory = True if name == ":memory:" else False
200
+ self.in_memory = True
180
201
 
181
202
  self.documents = []
182
203
  self._load()
@@ -190,6 +211,7 @@ class CollectionManager:
190
211
  with open(self.path, 'r', encoding='utf-8') as f:
191
212
  self.documents = json.load(f)
192
213
  except FileNotFoundError:
214
+ os.makedirs(os.path.dirname(self.path), exist_ok=True)
193
215
  self.documents = []
194
216
 
195
217
  def _save(self):
@@ -0,0 +1,104 @@
1
+ # coffy/nosql/nosql_tests.py
2
+ # author: nsarathy
3
+
4
+ import unittest
5
+ from engine import CollectionManager
6
+
7
+ class TestCollectionManager(unittest.TestCase):
8
+
9
+ def setUp(self):
10
+ self.col = CollectionManager(name="test_collection")
11
+ self.col.clear()
12
+ self.col.add_many([
13
+ {"name": "Alice", "age": 30, "tags": ["x", "y"]},
14
+ {"name": "Bob", "age": 25, "tags": ["y", "z"]},
15
+ {"name": "Carol", "age": 40, "nested": {"score": 100}},
16
+ ])
17
+
18
+ def test_add_and_all_docs(self):
19
+ result = self.col.all_docs()
20
+ self.assertEqual(len(result), 3)
21
+
22
+ def test_where_eq(self):
23
+ q = self.col.where("name").eq("Alice")
24
+ self.assertEqual(q.count(), 1)
25
+ self.assertEqual(q.first()["age"], 30)
26
+
27
+ def test_where_gt_and_lt(self):
28
+ gt_q = self.col.where("age").gt(26)
29
+ lt_q = self.col.where("age").lt(40)
30
+ self.assertEqual(gt_q.count(), 2)
31
+ self.assertEqual(lt_q.count(), 2)
32
+
33
+ def test_exists(self):
34
+ q = self.col.where("nested").exists()
35
+ self.assertEqual(q.count(), 1)
36
+ self.assertEqual(q.first()["name"], "Carol")
37
+
38
+ def test_in_and_nin(self):
39
+ q1 = self.col.where("name").in_(["Alice", "Bob"])
40
+ q2 = self.col.where("name").nin(["Carol"])
41
+ self.assertEqual(q1.count(), 2)
42
+ self.assertEqual(q2.count(), 2)
43
+
44
+ def test_matches(self):
45
+ q = self.col.where("name").matches("^A")
46
+ self.assertEqual(q.count(), 1)
47
+ self.assertEqual(q.first()["name"], "Alice")
48
+
49
+ def test_nested_field_access(self):
50
+ q = self.col.where("nested.score").eq(100)
51
+ self.assertEqual(q.count(), 1)
52
+ self.assertEqual(q.first()["name"], "Carol")
53
+
54
+ def test_logic_and_or_not(self):
55
+ q = self.col.match_all(
56
+ lambda q: q.where("age").gte(25),
57
+ lambda q: q.where("age").lt(40)
58
+ )
59
+ self.assertEqual(q.count(), 2)
60
+
61
+ q = self.col.match_any(
62
+ lambda q: q.where("name").eq("Alice"),
63
+ lambda q: q.where("name").eq("Bob")
64
+ )
65
+ self.assertEqual(q.count(), 2)
66
+
67
+ q = self.col.not_any(
68
+ lambda q: q.where("name").eq("Bob"),
69
+ lambda q: q.where("age").eq(40)
70
+ )
71
+ self.assertEqual(q.count(), 1)
72
+ self.assertEqual(q.first()["name"], "Alice")
73
+
74
+ def test_run_with_projection(self):
75
+ q = self.col.where("age").gte(25)
76
+ result = q.run(fields=["name"])
77
+ self.assertEqual(len(result), 3)
78
+ for doc in result:
79
+ self.assertEqual(list(doc.keys()), ["name"])
80
+
81
+ def test_update_and_delete_and_replace(self):
82
+ self.col.where("name").eq("Alice").update({"updated": True})
83
+ updated = self.col.where("updated").eq(True).first()
84
+ self.assertEqual(updated["name"], "Alice")
85
+
86
+ self.col.where("name").eq("Bob").delete()
87
+ self.assertEqual(self.col.where("name").eq("Bob").count(), 0)
88
+
89
+ self.col.where("name").eq("Carol").replace({"name": "New", "age": 99})
90
+ new_doc = self.col.where("name").eq("New").first()
91
+ self.assertEqual(new_doc["age"], 99)
92
+
93
+ def test_aggregates(self):
94
+ self.assertEqual(self.col.sum("age"), 95)
95
+ self.assertEqual(self.col.avg("age"), 95 / 3)
96
+ self.assertEqual(self.col.min("age"), 25)
97
+ self.assertEqual(self.col.max("age"), 40)
98
+
99
+ def test_merge(self):
100
+ q = self.col.where("name").eq("Alice")
101
+ merged = q.merge(lambda d: {"new": d["age"] + 10}).run()
102
+ self.assertEqual(merged[0]["new"], 40)
103
+
104
+ unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromTestCase(TestCollectionManager))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coffy
3
- Version: 0.1.2
3
+ Version: 0.1.5
4
4
  Summary: Lightweight local NoSQL, SQL, and Graph embedded database engine
5
5
  Author: nsarathy
6
6
  Classifier: Programming Language :: Python :: 3
@@ -24,10 +24,15 @@ Dynamic: summary
24
24
  **Coffy** is a lightweight embedded database engine for Python, designed for local-first apps, scripts, and tools. It includes:
25
25
 
26
26
  - `coffy.nosql`: A simple JSON-backed NoSQL engine with a fluent, chainable query interface
27
- - `coffy.sql`: A wrapper over SQLite for executing raw SQL with clean tabular results
28
- - `coffy.graph`: An graph engine built on `networkx` with advanced filtering and logic-based querying
27
+ - `coffy.graph`: An graph engine built on `networkx` with advanced filtering and logic-based querying
28
+ - `coffy.sql`: A wrapper over `SQLite` for executing raw SQL
29
29
 
30
- No dependencies (except `networkx`). No boilerplate. Just data.
30
+ ---
31
+ ## Latest Updates
32
+ - Added projection and dot-notation support for nested fields in NoSQL queries. Expanded logic chaining (`_and`, `_or`, `_not`) with improved test and better query semantics.
33
+ - GraphDB now supports saving query results, fixed relationship type serialization, and added more robust unit coverage.
34
+ - Unit tests added to test NoSQL and Graph database features.
35
+ - Documentation Updated.
31
36
 
32
37
  ---
33
38
 
@@ -43,31 +48,31 @@ pip install coffy
43
48
 
44
49
  ### `coffy.nosql`
45
50
 
46
- - JSON-based collections with fluent `.where().eq().gt()...` query chaining
47
- - Joins, updates, filters, aggregation, export/import
48
- - All data saved to human-readable `.json` files
51
+ - Embedded NoSQL document store with a fluent, chainable query API
52
+ - Supports nested fields, logical filters, aggregations, projections, and joins
53
+ - Built for local usage with optional persistence; minimal setup, fast iteration
49
54
 
50
55
  📄 [NoSQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/NOSQL_DOCS.md)
51
56
 
52
57
  ---
53
58
 
54
- ### `coffy.sql`
59
+ ### `coffy.graph`
55
60
 
56
- - SQLite-backed engine with raw SQL query support
57
- - Outputs as readable tables or exportable lists
58
- - Uses in-memory DB by default, or json-based if initialized with a path
61
+ - Lightweight, file-backed graph database using `networkx` under the hood
62
+ - Supports pattern matching, label/type filtering, logical conditions, and projections
63
+ - Query results can be saved, updated, or transformed; ideal for local, schema-flexible graph data
59
64
 
60
- 📄 [SQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/SQL_DOCS.md)
65
+ 📄 [Graph Documentation →](https://github.com/nsarathy/Coffy/blob/main/GRAPH_DOCS.md)
61
66
 
62
67
  ---
63
68
 
64
- ### `coffy.graph`
69
+ ### `coffy.sql`
65
70
 
66
- - Wrapper around `networkx` with simplified node/relationship API
67
- - Query nodes and relationships using filters like `gt`, `lt`, `eq`, `or`, `not`
71
+ - SQLite-backed engine with raw SQL query support
72
+ - Outputs as readable tables or exportable lists
68
73
  - Uses in-memory DB by default, or json-based if initialized with a path
69
74
 
70
- 📄 [Graph Documentation →](https://github.com/nsarathy/Coffy/blob/main/GRAPH_DOCS.md)
75
+ 📄 [SQL Documentation →](https://github.com/nsarathy/Coffy/blob/main/SQL_DOCS.md)
71
76
 
72
77
  ---
73
78
 
@@ -81,15 +86,6 @@ users.add({"id": 1, "name": "Neel"})
81
86
  print(users.where("name").eq("Neel").first())
82
87
  ```
83
88
 
84
- ```python
85
- from coffy.sql import init, query
86
-
87
- init("app.db")
88
- query("CREATE TABLE test (id INT, name TEXT)")
89
- query("INSERT INTO test VALUES (1, 'Neel')")
90
- print(query("SELECT * FROM test"))
91
- ```
92
-
93
89
  ```python
94
90
  from coffy.graph import GraphDB
95
91
 
@@ -99,6 +95,15 @@ g.add_relationships([{"source": 1, "target": 2, "type": "friend"}])
99
95
  print(g.find_relationships(type="friend"))
100
96
  ```
101
97
 
98
+ ```python
99
+ from coffy.sql import init, query
100
+
101
+ init("app.db")
102
+ query("CREATE TABLE test (id INT, name TEXT)")
103
+ query("INSERT INTO test VALUES (1, 'Neel')")
104
+ print(query("SELECT * FROM test"))
105
+ ```
106
+
102
107
  ---
103
108
 
104
109
  ## 📄 License
@@ -12,11 +12,13 @@ coffy.egg-info/top_level.txt
12
12
  coffy/__pycache__/__init__.cpython-311.pyc
13
13
  coffy/__pycache__/__init__.cpython-312.pyc
14
14
  coffy/graph/__init__.py
15
+ coffy/graph/graph_tests.py
15
16
  coffy/graph/graphdb_nx.py
16
17
  coffy/graph/__pycache__/__init__.cpython-312.pyc
17
18
  coffy/graph/__pycache__/graphdb_nx.cpython-312.pyc
18
19
  coffy/nosql/__init__.py
19
20
  coffy/nosql/engine.py
21
+ coffy/nosql/nosql_tests.py
20
22
  coffy/nosql/__pycache__/__init__.cpython-311.pyc
21
23
  coffy/nosql/__pycache__/__init__.cpython-312.pyc
22
24
  coffy/nosql/__pycache__/engine.cpython-311.pyc
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="coffy",
5
- version="0.1.2",
5
+ version="0.1.5",
6
6
  author="nsarathy",
7
7
  description="Lightweight local NoSQL, SQL, and Graph embedded database engine",
8
8
  long_description=open("README.md", encoding="utf-8").read(),
@@ -1,125 +0,0 @@
1
- import networkx as nx
2
- import json
3
- import os
4
-
5
- class GraphDB:
6
- def __init__(self, directed=False, path=None):
7
- self.g = nx.DiGraph() if directed else nx.Graph()
8
- self.directed = directed
9
- self.path = path
10
- if path and os.path.exists(path):
11
- self.load(path)
12
-
13
- # Node operations
14
- def add_node(self, node_id, **attrs):
15
- self.g.add_node(node_id, **attrs)
16
-
17
- def add_nodes(self, nodes):
18
- for node in nodes:
19
- self.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
20
-
21
- def get_node(self, node_id):
22
- return self.g.nodes[node_id]
23
-
24
- def remove_node(self, node_id):
25
- self.g.remove_node(node_id)
26
-
27
- # Relationship (edge) operations
28
- def add_relationship(self, source, target, **attrs):
29
- self.g.add_edge(source, target, **attrs)
30
-
31
- def add_relationships(self, relationships):
32
- for rel in relationships:
33
- self.add_relationship(rel["source"], rel["target"],
34
- **{k: v for k, v in rel.items() if k not in ["source", "target"]})
35
-
36
- def get_relationship(self, source, target):
37
- return self.g.get_edge_data(source, target)
38
-
39
- def remove_relationship(self, source, target):
40
- self.g.remove_edge(source, target)
41
-
42
- # Basic queries
43
- def neighbors(self, node_id):
44
- return list(self.g.neighbors(node_id))
45
-
46
- def degree(self, node_id):
47
- return self.g.degree[node_id]
48
-
49
- def has_node(self, node_id):
50
- return self.g.has_node(node_id)
51
-
52
- def has_relationship(self, u, v):
53
- return self.g.has_edge(u, v)
54
-
55
- # Advanced node search
56
- def find_nodes(self, **conditions):
57
- return [
58
- {"id": n, **a} for n, a in self.g.nodes(data=True)
59
- if self._match_conditions(a, conditions)
60
- ]
61
-
62
- def find_relationships(self, **conditions):
63
- return [
64
- {"source": u, "target": v, **a} for u, v, a in self.g.edges(data=True)
65
- if self._match_conditions(a, conditions)
66
- ]
67
-
68
- def _match_conditions(self, attrs, conditions):
69
- if not conditions:
70
- return True
71
- logic = conditions.pop("_logic", "and")
72
- results = []
73
-
74
- for key, expected in conditions.items():
75
- actual = attrs.get(key)
76
- if isinstance(expected, dict):
77
- for op, val in expected.items():
78
- if op == "gt": results.append(actual > val)
79
- elif op == "lt": results.append(actual < val)
80
- elif op == "gte": results.append(actual >= val)
81
- elif op == "lte": results.append(actual <= val)
82
- elif op == "ne": results.append(actual != val)
83
- elif op == "eq": results.append(actual == val)
84
- else: results.append(False)
85
- else:
86
- results.append(actual == expected)
87
-
88
- if logic == "or":
89
- return any(results)
90
- elif logic == "not":
91
- return not all(results)
92
- return all(results)
93
-
94
- # Export
95
- def nodes(self):
96
- return [{"id": n, **a} for n, a in self.g.nodes(data=True)]
97
-
98
- def relationships(self):
99
- return [{"source": u, "target": v, **a} for u, v, a in self.g.edges(data=True)]
100
-
101
- def to_dict(self):
102
- return {
103
- "nodes": self.nodes(),
104
- "relationships": self.relationships()
105
- }
106
-
107
- def save(self, path=None):
108
- path = path or self.path
109
- if not path:
110
- raise ValueError("No path specified to save the graph.")
111
- with open(path, "w", encoding="utf-8") as f:
112
- json.dump(self.to_dict(), f, indent=4)
113
-
114
- def load(self, path=None):
115
- path = path or self.path
116
- if not path:
117
- raise ValueError("No path specified to load the graph.")
118
- with open(path, "r", encoding="utf-8") as f:
119
- data = json.load(f)
120
- self.g.clear()
121
- for node in data.get("nodes", []):
122
- self.add_node(node["id"], **{k: v for k, v in node.items() if k != "id"})
123
- for rel in data.get("relationships", []):
124
- self.add_relationship(rel["source"], rel["target"],
125
- **{k: v for k, v in rel.items() if k not in ["source", "target"]})
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes