sonnet-graph 0.3.2__tar.gz → 0.4.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.
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/PKG-INFO +2 -2
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/pyproject.toml +5 -2
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/__init__.py +1 -7
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/context.py +7 -1
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/metadata/__init__.py +12 -34
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/metadata/model_store.py +48 -8
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/metadata/models.py +1 -25
- sonnet_graph-0.4.0/src/sonnet_graph/metadata/seeding.py +64 -0
- sonnet_graph-0.4.0/src/sonnet_graph/py.typed +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph.egg-info/PKG-INFO +2 -2
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph.egg-info/SOURCES.txt +1 -1
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph.egg-info/requires.txt +1 -1
- sonnet_graph-0.3.2/src/sonnet_graph/metadata/query_store.py +0 -264
- sonnet_graph-0.3.2/src/sonnet_graph/metadata/seeding.py +0 -170
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/README.md +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/setup.cfg +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/checks/__init__.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/checks/graphdb_health_check.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/checks/graphdb_initialization.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/checks/stages.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/dependencies.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/extension.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/metadata/graph_info.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/metadata/template_resolver.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph/status.py +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph.egg-info/dependency_links.txt +0 -0
- {sonnet_graph-0.3.2 → sonnet_graph-0.4.0}/src/sonnet_graph.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sonnet-graph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Graph database integration (cypher-graphdb) for sonnet-server applications
|
|
5
5
|
Author-email: Wolfgang Miller <wolfgang.miller@petrarca-labs.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -12,7 +12,7 @@ Requires-Python: <4.0,>=3.14
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: sonnet-core>=0.1.0
|
|
14
14
|
Requires-Dist: sonnet-server>=0.4.0
|
|
15
|
-
Requires-Dist: cypher-graphdb>=0.
|
|
15
|
+
Requires-Dist: cypher-graphdb>=0.7.1
|
|
16
16
|
Requires-Dist: loguru>=0.7.3
|
|
17
17
|
Requires-Dist: jinja2>=3.1.6
|
|
18
18
|
Provides-Extra: dev
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sonnet-graph"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Wolfgang Miller", email = "wolfgang.miller@petrarca-labs.com" },
|
|
10
10
|
]
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
dependencies = [
|
|
22
22
|
"sonnet-core>=0.1.0",
|
|
23
23
|
"sonnet-server>=0.4.0",
|
|
24
|
-
"cypher-graphdb>=0.
|
|
24
|
+
"cypher-graphdb>=0.7.1",
|
|
25
25
|
"loguru>=0.7.3",
|
|
26
26
|
"jinja2>=3.1.6",
|
|
27
27
|
]
|
|
@@ -37,6 +37,9 @@ dev = [
|
|
|
37
37
|
[tool.setuptools.packages.find]
|
|
38
38
|
where = ["src"]
|
|
39
39
|
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
sonnet_graph = ["py.typed"]
|
|
42
|
+
|
|
40
43
|
[tool.pytest.ini_options]
|
|
41
44
|
markers = [
|
|
42
45
|
"unit: marks tests as unit tests (default)",
|
|
@@ -33,11 +33,9 @@ Public API:
|
|
|
33
33
|
|
|
34
34
|
Graph metadata:
|
|
35
35
|
GraphModelStore -- load/save/remove :_GraphModel node (schema + description)
|
|
36
|
-
GraphQueryStore -- load/save/remove :_NamedQuery nodes (queries)
|
|
37
36
|
init_graph_metadata -- initialize model + description (one-time setup)
|
|
38
|
-
seed_named_queries -- seed :_NamedQuery nodes from file pairs
|
|
39
37
|
get_graph_info -- LLM-friendly schema + description in one call
|
|
40
|
-
resolve_template -- Jinja2 template resolution
|
|
38
|
+
resolve_template -- stateless Jinja2 template resolution
|
|
41
39
|
|
|
42
40
|
FastAPI dependencies:
|
|
43
41
|
get_graphdb_session -- factory for router-level ambient session dependency
|
|
@@ -74,11 +72,9 @@ from sonnet_graph.dependencies import get_graphdb, get_graphdb_session
|
|
|
74
72
|
from sonnet_graph.extension import GraphDBExtension
|
|
75
73
|
from sonnet_graph.metadata import (
|
|
76
74
|
GraphModelStore,
|
|
77
|
-
GraphQueryStore,
|
|
78
75
|
get_graph_info,
|
|
79
76
|
init_graph_metadata,
|
|
80
77
|
resolve_template,
|
|
81
|
-
seed_named_queries,
|
|
82
78
|
)
|
|
83
79
|
from sonnet_graph.status import GraphPoolStatus, PoolInfo, snapshot_pools
|
|
84
80
|
|
|
@@ -118,9 +114,7 @@ __all__ = [
|
|
|
118
114
|
"GraphdbStage",
|
|
119
115
|
# Graph metadata
|
|
120
116
|
"GraphModelStore",
|
|
121
|
-
"GraphQueryStore",
|
|
122
117
|
"get_graph_info",
|
|
123
118
|
"init_graph_metadata",
|
|
124
119
|
"resolve_template",
|
|
125
|
-
"seed_named_queries",
|
|
126
120
|
]
|
|
@@ -201,7 +201,13 @@ def register_graph(name: str, *, env_prefix: str | None = None, settings: Cypher
|
|
|
201
201
|
raise ValueError("register_graph requires one of 'env_prefix' or 'settings' -- neither was given.")
|
|
202
202
|
if env_prefix is not None and settings is not None:
|
|
203
203
|
raise ValueError("register_graph requires exactly one of 'env_prefix' or 'settings' -- both were given.")
|
|
204
|
-
|
|
204
|
+
# Exactly one is non-None here; assign in explicit branches so the narrowed
|
|
205
|
+
# (non-None) type is what lands in _graph_specs[str, CypherSettings | str].
|
|
206
|
+
if settings is not None:
|
|
207
|
+
_graph_specs[name] = settings
|
|
208
|
+
else:
|
|
209
|
+
assert env_prefix is not None # guaranteed by the checks above; narrows for the type checker
|
|
210
|
+
_graph_specs[name] = env_prefix
|
|
205
211
|
logger.debug("register_graph: '{}' registered ({})", name, "settings" if settings is not None else f"env_prefix={env_prefix}")
|
|
206
212
|
|
|
207
213
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
"""Graph metadata -- self-describing graph nodes for model
|
|
1
|
+
"""Graph metadata -- self-describing graph nodes for model and description.
|
|
2
2
|
|
|
3
|
-
The graph stores its own metadata as dedicated
|
|
3
|
+
The graph stores its own metadata as a dedicated node prefixed with
|
|
4
4
|
underscore to indicate internal infrastructure:
|
|
5
5
|
|
|
6
6
|
- :_GraphModel -- graph schema (node/edge types, properties) and
|
|
7
7
|
human-readable description (markdown)
|
|
8
|
-
- :_NamedQuery -- pre-defined queries with metadata (cypher, jinja2, datalog)
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
The metadata type has a facade store and a typed model.
|
|
11
10
|
|
|
12
11
|
Two paths to seed a graph model:
|
|
13
12
|
|
|
@@ -40,12 +39,11 @@ Graph description (stored on the :_GraphModel node)::
|
|
|
40
39
|
content, fmt = store.load_description()
|
|
41
40
|
# -> ("# Tech Stack Graph\\n...", "text/markdown")
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
Initialize model + description in one call::
|
|
44
43
|
|
|
45
|
-
from sonnet_graph import borrow_graphdb,
|
|
44
|
+
from sonnet_graph import borrow_graphdb, init_graph_metadata
|
|
46
45
|
|
|
47
46
|
with borrow_graphdb("techstack") as cdb:
|
|
48
|
-
# Initialize model + description
|
|
49
47
|
init_graph_metadata(
|
|
50
48
|
cdb,
|
|
51
49
|
models_path="plugins/tech_stack/graph_models.py",
|
|
@@ -53,58 +51,38 @@ Named queries::
|
|
|
53
51
|
)
|
|
54
52
|
cdb.commit()
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
title="Hub Technologies",
|
|
61
|
-
query_content="MATCH (t:Technology)<-[:USES]-(c) RETURN t, count(c) ORDER BY count(c) DESC",
|
|
62
|
-
language="cypher",
|
|
63
|
-
tags=["analysis"],
|
|
64
|
-
)
|
|
65
|
-
cdb.commit()
|
|
66
|
-
|
|
67
|
-
queries = store.list() # metadata only
|
|
68
|
-
query = store.load("hub-technologies") # full detail
|
|
69
|
-
cypher = resolve_template(query["query_content"], language=query["language"])
|
|
54
|
+
Named queries are a product-owned feature: their storage and lifecycle belong
|
|
55
|
+
to the consuming application, not to graph metadata. sonnet-graph provides only
|
|
56
|
+
``resolve_template`` (stateless Jinja2 rendering) for callers that resolve
|
|
57
|
+
parameterized query text themselves.
|
|
70
58
|
|
|
71
59
|
Public API:
|
|
72
60
|
|
|
73
61
|
Stores:
|
|
74
62
|
GraphModelStore -- load/save/remove :_GraphModel node (schema + description)
|
|
75
|
-
GraphQueryStore -- load/save/remove :_NamedQuery nodes
|
|
76
63
|
|
|
77
64
|
Initialization:
|
|
78
65
|
init_graph_metadata -- initialize model + description (lauro graph init)
|
|
79
66
|
|
|
80
|
-
Query seeding:
|
|
81
|
-
seed_named_queries -- seed :_NamedQuery nodes from file pairs
|
|
82
|
-
|
|
83
67
|
Helpers:
|
|
84
68
|
get_graph_info -- LLM-friendly schema + description in one call
|
|
85
|
-
resolve_template -- Jinja2 template resolution
|
|
69
|
+
resolve_template -- stateless Jinja2 template resolution
|
|
86
70
|
|
|
87
71
|
Node models:
|
|
88
72
|
GraphModelNode -- model for the _GraphModel label
|
|
89
|
-
NamedQueryNode -- model for the _NamedQuery label
|
|
90
73
|
"""
|
|
91
74
|
|
|
92
75
|
from sonnet_graph.metadata.graph_info import get_graph_info
|
|
93
76
|
from sonnet_graph.metadata.model_store import GraphModelStore
|
|
94
|
-
from sonnet_graph.metadata.models import GraphModelNode
|
|
95
|
-
from sonnet_graph.metadata.
|
|
96
|
-
from sonnet_graph.metadata.seeding import QuerySeedResult, init_graph_metadata, seed_named_queries
|
|
77
|
+
from sonnet_graph.metadata.models import GraphModelNode
|
|
78
|
+
from sonnet_graph.metadata.seeding import init_graph_metadata
|
|
97
79
|
from sonnet_graph.metadata.template_resolver import TemplateResolutionError, resolve_template
|
|
98
80
|
|
|
99
81
|
__all__ = [
|
|
100
82
|
"GraphModelNode",
|
|
101
83
|
"GraphModelStore",
|
|
102
|
-
"GraphQueryStore",
|
|
103
|
-
"NamedQueryNode",
|
|
104
|
-
"QuerySeedResult",
|
|
105
84
|
"TemplateResolutionError",
|
|
106
85
|
"get_graph_info",
|
|
107
86
|
"init_graph_metadata",
|
|
108
87
|
"resolve_template",
|
|
109
|
-
"seed_named_queries",
|
|
110
88
|
]
|
|
@@ -101,6 +101,8 @@ class GraphModelStore:
|
|
|
101
101
|
|
|
102
102
|
- ``save_from_provider(provider)`` -- models already registered
|
|
103
103
|
in a ModelProvider (e.g. a graph_models module's ``mp``)
|
|
104
|
+
- ``sync_from_provider(provider)`` -- the same, written only when the
|
|
105
|
+
stored schema, name or version differ
|
|
104
106
|
- ``save_from_path(path)`` -- models in a Python file/directory
|
|
105
107
|
- ``save_from_file(path)`` -- pre-generated graph.schema.json
|
|
106
108
|
- ``save(schema_dict)`` -- raw schema dict
|
|
@@ -276,6 +278,20 @@ class GraphModelStore:
|
|
|
276
278
|
|
|
277
279
|
self._cdb.create_or_merge(node)
|
|
278
280
|
|
|
281
|
+
@staticmethod
|
|
282
|
+
def schema_from_provider(provider: ModelProvider) -> dict[str, Any]:
|
|
283
|
+
"""Generate the combined JSON Schema of a ModelProvider's registered models.
|
|
284
|
+
|
|
285
|
+
Raises:
|
|
286
|
+
ValueError: If the provider has no models with schemas.
|
|
287
|
+
"""
|
|
288
|
+
from cypher_graphdb.utils.schema_utils import combine_schemas, extract_schemas_from_model_infos
|
|
289
|
+
|
|
290
|
+
model_infos = [info for info in provider.values() if info.graph_schema]
|
|
291
|
+
if not model_infos:
|
|
292
|
+
raise ValueError("No models with schemas found in the provider")
|
|
293
|
+
return combine_schemas(extract_schemas_from_model_infos(model_infos))
|
|
294
|
+
|
|
279
295
|
def save_from_provider(
|
|
280
296
|
self, provider: ModelProvider, *, name: str | None = None, version: str | None = None
|
|
281
297
|
) -> dict[str, Any]:
|
|
@@ -292,17 +308,41 @@ class GraphModelStore:
|
|
|
292
308
|
Raises:
|
|
293
309
|
ValueError: If the provider has no models with schemas.
|
|
294
310
|
"""
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
model_infos = [info for info in provider.values() if info.graph_schema]
|
|
298
|
-
if not model_infos:
|
|
299
|
-
raise ValueError("No models with schemas found in the provider")
|
|
300
|
-
|
|
301
|
-
schemas = extract_schemas_from_model_infos(model_infos)
|
|
302
|
-
combined = combine_schemas(schemas)
|
|
311
|
+
combined = self.schema_from_provider(provider)
|
|
303
312
|
self.save(combined, name=name, version=version)
|
|
304
313
|
return combined
|
|
305
314
|
|
|
315
|
+
def sync_from_provider(self, provider: ModelProvider, *, name: str | None = None, version: str | None = None) -> bool:
|
|
316
|
+
"""Save the provider's schema unless the stored one is already the same; return whether it was written.
|
|
317
|
+
|
|
318
|
+
Compares in the stored form (the schema as JSON), with the name and
|
|
319
|
+
version ``save`` would write: a changed model, name or version
|
|
320
|
+
rewrites the node, an unchanged one leaves it -- and its
|
|
321
|
+
``updated_at`` -- as it is. Either way the schema's types are
|
|
322
|
+
registered into the connection's ModelProvider, as after a save.
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
ValueError: If the provider has no models with schemas.
|
|
326
|
+
"""
|
|
327
|
+
schema = self.schema_from_provider(provider)
|
|
328
|
+
if self._is_stored(schema, name=name, version=version):
|
|
329
|
+
self._register_schema(schema)
|
|
330
|
+
return False
|
|
331
|
+
self.save(schema, name=name, version=version)
|
|
332
|
+
return True
|
|
333
|
+
|
|
334
|
+
def _is_stored(self, schema: dict[str, Any], *, name: str | None, version: str | None) -> bool:
|
|
335
|
+
"""Whether the stored node holds exactly what ``save(schema, name=, version=)`` would write."""
|
|
336
|
+
node = self._load_node()
|
|
337
|
+
if node is None or not node.data:
|
|
338
|
+
return False
|
|
339
|
+
try:
|
|
340
|
+
stored = json.loads(node.data)
|
|
341
|
+
except json.JSONDecodeError, TypeError:
|
|
342
|
+
return False
|
|
343
|
+
as_stored = json.loads(json.dumps(schema, ensure_ascii=False))
|
|
344
|
+
return stored == as_stored and node.name == (name or schema.get("title", "")) and (node.version or "") == (version or "")
|
|
345
|
+
|
|
306
346
|
def save_from_path(
|
|
307
347
|
self,
|
|
308
348
|
models_path: str | Path,
|
|
@@ -18,7 +18,7 @@ metadata_provider = ModelProvider()
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def ensure_metadata_types(provider: ModelProvider | None) -> None:
|
|
21
|
-
"""Seed the metadata node types (_GraphModel
|
|
21
|
+
"""Seed the metadata node types (_GraphModel) into ``provider``.
|
|
22
22
|
|
|
23
23
|
Idempotent and non-destructive: a graph that legitimately declares its own
|
|
24
24
|
type under one of these labels keeps it (``overwrite=False``).
|
|
@@ -57,27 +57,3 @@ class GraphModelNode(GraphNode):
|
|
|
57
57
|
description: str | None = None # human-readable documentation (markdown)
|
|
58
58
|
description_format: str | None = None # MIME type: text/markdown, text/plain, text/html
|
|
59
59
|
updated_at: str | None = None
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@metadata_provider.node(label="_NamedQuery", merge_keys=["query_key"])
|
|
63
|
-
class NamedQueryNode(GraphNode):
|
|
64
|
-
"""A pre-defined query with metadata.
|
|
65
|
-
|
|
66
|
-
Multiple per graph, keyed by ``query_key`` (``merge_keys=["query_key"]``).
|
|
67
|
-
The ``query_content`` holds the query text -- plain Cypher, a Jinja2
|
|
68
|
-
template, or Datalog rules. The ``language`` field discriminates.
|
|
69
|
-
|
|
70
|
-
The ``tags`` and ``params_schema`` and ``ui_schema`` fields are
|
|
71
|
-
JSON-serialized strings for cross-backend compatibility (AGE
|
|
72
|
-
stores arrays as JSON strings).
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
query_key: str | None = None
|
|
76
|
-
title: str | None = None
|
|
77
|
-
description: str | None = None
|
|
78
|
-
tags: str | None = None # JSON-serialized list[str]
|
|
79
|
-
query_content: str | None = None
|
|
80
|
-
language: str | None = None # cypher | jinja2 | datalog
|
|
81
|
-
params_schema: str | None = None # JSON-serialized JSON Schema
|
|
82
|
-
ui_schema: str | None = None # JSON-serialized UI hints
|
|
83
|
-
updated_at: str | None = None
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Graph metadata initialization.
|
|
2
|
+
|
|
3
|
+
``init_graph_metadata(cdb, ...)`` -- initialize model and description
|
|
4
|
+
on the :_GraphModel node. One-time setup, called by
|
|
5
|
+
``lauro graph init``. Raises on failure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from cypher_graphdb import CypherGraphDB
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from sonnet_graph.metadata.model_store import GraphModelStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def init_graph_metadata(
|
|
19
|
+
cdb: CypherGraphDB,
|
|
20
|
+
*,
|
|
21
|
+
model: dict | None = None,
|
|
22
|
+
models_path: str | Path | None = None,
|
|
23
|
+
model_name: str | None = None,
|
|
24
|
+
model_version: str | None = None,
|
|
25
|
+
description: str | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize graph metadata -- model schema and description.
|
|
28
|
+
|
|
29
|
+
Writes the :_GraphModel node with schema and optional description.
|
|
30
|
+
Intended for one-time graph initialization (``lauro graph init``).
|
|
31
|
+
Does NOT call ``commit()`` -- the caller is responsible.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
cdb: Active CypherGraphDB connection.
|
|
35
|
+
model: JSON Schema dict to write as :_GraphModel node.
|
|
36
|
+
models_path: Path to a Python file or directory declaring graph
|
|
37
|
+
models on a ModelProvider. Generates and saves the schema.
|
|
38
|
+
Mutually exclusive with ``model``.
|
|
39
|
+
model_name: Graph model name.
|
|
40
|
+
model_version: Schema version string.
|
|
41
|
+
description: Markdown string stored on the :_GraphModel node.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
ValueError: If both ``model`` and ``models_path`` are provided.
|
|
45
|
+
ValueError: If ``description`` is provided without a model/models_path
|
|
46
|
+
and no :_GraphModel node already exists in the graph. Use
|
|
47
|
+
``GraphModelStore.update_description()`` directly to update
|
|
48
|
+
an existing node's description independently.
|
|
49
|
+
"""
|
|
50
|
+
if model is not None and models_path is not None:
|
|
51
|
+
raise ValueError("Provide either 'model' or 'models_path', not both")
|
|
52
|
+
|
|
53
|
+
store = GraphModelStore(cdb)
|
|
54
|
+
|
|
55
|
+
if model is not None:
|
|
56
|
+
store.save(model, name=model_name, version=model_version)
|
|
57
|
+
logger.debug("Initialized graph model '{}' from dict", model_name or "")
|
|
58
|
+
elif models_path is not None:
|
|
59
|
+
store.save_from_path(models_path, name=model_name, version=model_version)
|
|
60
|
+
logger.debug("Initialized graph model '{}' from path '{}'", model_name or "", models_path)
|
|
61
|
+
|
|
62
|
+
if description is not None:
|
|
63
|
+
store.update_description(description)
|
|
64
|
+
logger.debug("Updated graph description")
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sonnet-graph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Graph database integration (cypher-graphdb) for sonnet-server applications
|
|
5
5
|
Author-email: Wolfgang Miller <wolfgang.miller@petrarca-labs.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -12,7 +12,7 @@ Requires-Python: <4.0,>=3.14
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
Requires-Dist: sonnet-core>=0.1.0
|
|
14
14
|
Requires-Dist: sonnet-server>=0.4.0
|
|
15
|
-
Requires-Dist: cypher-graphdb>=0.
|
|
15
|
+
Requires-Dist: cypher-graphdb>=0.7.1
|
|
16
16
|
Requires-Dist: loguru>=0.7.3
|
|
17
17
|
Requires-Dist: jinja2>=3.1.6
|
|
18
18
|
Provides-Extra: dev
|
|
@@ -4,6 +4,7 @@ src/sonnet_graph/__init__.py
|
|
|
4
4
|
src/sonnet_graph/context.py
|
|
5
5
|
src/sonnet_graph/dependencies.py
|
|
6
6
|
src/sonnet_graph/extension.py
|
|
7
|
+
src/sonnet_graph/py.typed
|
|
7
8
|
src/sonnet_graph/status.py
|
|
8
9
|
src/sonnet_graph.egg-info/PKG-INFO
|
|
9
10
|
src/sonnet_graph.egg-info/SOURCES.txt
|
|
@@ -18,6 +19,5 @@ src/sonnet_graph/metadata/__init__.py
|
|
|
18
19
|
src/sonnet_graph/metadata/graph_info.py
|
|
19
20
|
src/sonnet_graph/metadata/model_store.py
|
|
20
21
|
src/sonnet_graph/metadata/models.py
|
|
21
|
-
src/sonnet_graph/metadata/query_store.py
|
|
22
22
|
src/sonnet_graph/metadata/seeding.py
|
|
23
23
|
src/sonnet_graph/metadata/template_resolver.py
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
"""GraphQueryStore -- facade for :_NamedQuery nodes in a graph.
|
|
2
|
-
|
|
3
|
-
Stores pre-defined queries with metadata. Multiple per graph, keyed
|
|
4
|
-
by ``query_key`` (kebab-case). Supports three query languages:
|
|
5
|
-
|
|
6
|
-
- ``cypher`` -- plain Cypher, executed directly
|
|
7
|
-
- ``jinja2`` -- Jinja2 Cypher template, resolved via ``resolve_template()``
|
|
8
|
-
before execution
|
|
9
|
-
- ``datalog`` -- Souffle-compatible Datalog rules, resolved by the
|
|
10
|
-
consumer's Datalog engine (e.g. lauro)
|
|
11
|
-
|
|
12
|
-
The store is pure CRUD -- it never executes or resolves queries.
|
|
13
|
-
Resolution is the consumer's responsibility.
|
|
14
|
-
|
|
15
|
-
Usage::
|
|
16
|
-
|
|
17
|
-
from sonnet_graph import borrow_graphdb, GraphQueryStore
|
|
18
|
-
|
|
19
|
-
with borrow_graphdb("techstack") as cdb:
|
|
20
|
-
store = GraphQueryStore(cdb)
|
|
21
|
-
|
|
22
|
-
# Save a query
|
|
23
|
-
store.save(
|
|
24
|
-
query_key="hub-technologies",
|
|
25
|
-
title="Hub Technologies",
|
|
26
|
-
description="Technologies used by the most components",
|
|
27
|
-
tags=["analysis", "technology"],
|
|
28
|
-
query_content="MATCH (c:Component)-[:USES_TECHNOLOGY]->(t) ...",
|
|
29
|
-
language="cypher",
|
|
30
|
-
)
|
|
31
|
-
cdb.commit()
|
|
32
|
-
|
|
33
|
-
# List all (metadata only, no query_content)
|
|
34
|
-
queries = store.list()
|
|
35
|
-
|
|
36
|
-
# List filtered by tag
|
|
37
|
-
queries = store.list(tags=["analysis"])
|
|
38
|
-
|
|
39
|
-
# Load full query
|
|
40
|
-
query = store.load("hub-technologies")
|
|
41
|
-
|
|
42
|
-
# Remove one
|
|
43
|
-
store.remove("hub-technologies")
|
|
44
|
-
|
|
45
|
-
# Remove all (clean re-seed)
|
|
46
|
-
store.remove_all()
|
|
47
|
-
cdb.commit()
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
from __future__ import annotations
|
|
51
|
-
|
|
52
|
-
import builtins
|
|
53
|
-
import json
|
|
54
|
-
from typing import Any
|
|
55
|
-
|
|
56
|
-
import arrow
|
|
57
|
-
from cypher_graphdb import CypherGraphDB
|
|
58
|
-
|
|
59
|
-
from sonnet_graph.metadata.models import NamedQueryNode, ensure_metadata_types
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class GraphQueryStore:
|
|
63
|
-
"""Load/save/remove :_NamedQuery nodes in a graph.
|
|
64
|
-
|
|
65
|
-
Each query is identified by ``query_key`` (unique within the graph).
|
|
66
|
-
Uses ``create_or_merge`` with ``merge_keys=["query_key"]`` so each
|
|
67
|
-
node gets a proper ``gid_`` for cypher-graphdb identity.
|
|
68
|
-
|
|
69
|
-
The ``tags`` field is stored as a JSON-serialized string for
|
|
70
|
-
cross-backend compatibility (AGE stores arrays as JSON strings).
|
|
71
|
-
The ``list()`` and ``load()`` methods deserialize it back to
|
|
72
|
-
``list[str]`` in the returned dicts.
|
|
73
|
-
"""
|
|
74
|
-
|
|
75
|
-
def __init__(self, cdb: CypherGraphDB) -> None:
|
|
76
|
-
self._cdb = cdb
|
|
77
|
-
# The metadata types must be known to the connection's provider or
|
|
78
|
-
# these nodes come back as generic GraphNode. Seed them here so the
|
|
79
|
-
# store works on any connection, pooled or built directly.
|
|
80
|
-
ensure_metadata_types(cdb.model_provider)
|
|
81
|
-
|
|
82
|
-
# -- list ----------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
def list(self, *, tags: list[str] | None = None) -> list[dict[str, Any]]:
|
|
85
|
-
"""List all named queries, optionally filtered by tags.
|
|
86
|
-
|
|
87
|
-
Returns metadata only (no ``query_content``) for lightweight
|
|
88
|
-
listing. Each entry is a dict with: ``query_key``, ``title``,
|
|
89
|
-
``description``, ``tags``, ``language``, ``updated_at``.
|
|
90
|
-
|
|
91
|
-
Args:
|
|
92
|
-
tags: If provided, only return queries whose tags overlap
|
|
93
|
-
with at least one of the given tags.
|
|
94
|
-
"""
|
|
95
|
-
result = self._cdb.execute(
|
|
96
|
-
"MATCH (q:_NamedQuery) RETURN q ORDER BY q.query_key",
|
|
97
|
-
unnest_result=True,
|
|
98
|
-
)
|
|
99
|
-
if not result:
|
|
100
|
-
return []
|
|
101
|
-
|
|
102
|
-
nodes = result if isinstance(result, list) else [result]
|
|
103
|
-
entries = []
|
|
104
|
-
for node in nodes:
|
|
105
|
-
if not isinstance(node, NamedQueryNode):
|
|
106
|
-
continue
|
|
107
|
-
node_tags = _deserialize_tags(node.tags)
|
|
108
|
-
if tags and not set(tags) & set(node_tags):
|
|
109
|
-
continue
|
|
110
|
-
entries.append(
|
|
111
|
-
{
|
|
112
|
-
"query_key": node.query_key,
|
|
113
|
-
"title": node.title,
|
|
114
|
-
"description": node.description,
|
|
115
|
-
"tags": node_tags,
|
|
116
|
-
"language": node.language,
|
|
117
|
-
"updated_at": node.updated_at,
|
|
118
|
-
}
|
|
119
|
-
)
|
|
120
|
-
return entries
|
|
121
|
-
|
|
122
|
-
# -- load ----------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
def load(self, query_key: str) -> dict[str, Any] | None:
|
|
125
|
-
"""Load full query detail including query_content.
|
|
126
|
-
|
|
127
|
-
Returns a dict with all properties, or None if not found.
|
|
128
|
-
The ``tags``, ``params_schema``, and ``ui_schema`` fields
|
|
129
|
-
are deserialized from JSON strings.
|
|
130
|
-
"""
|
|
131
|
-
node = self._load_node(query_key)
|
|
132
|
-
if node is None:
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
"query_key": node.query_key,
|
|
137
|
-
"title": node.title,
|
|
138
|
-
"description": node.description,
|
|
139
|
-
"tags": _deserialize_tags(node.tags),
|
|
140
|
-
"query_content": node.query_content,
|
|
141
|
-
"language": node.language,
|
|
142
|
-
"params_schema": _deserialize_json(node.params_schema),
|
|
143
|
-
"ui_schema": _deserialize_json(node.ui_schema),
|
|
144
|
-
"updated_at": node.updated_at,
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
def _load_node(self, query_key: str) -> NamedQueryNode | None:
|
|
148
|
-
"""Load the raw NamedQueryNode by query_key, or None."""
|
|
149
|
-
result = self._cdb.execute(
|
|
150
|
-
"MATCH (q:_NamedQuery {query_key: $key}) RETURN q",
|
|
151
|
-
params={"key": query_key},
|
|
152
|
-
unnest_result=True,
|
|
153
|
-
fetch_one=True,
|
|
154
|
-
)
|
|
155
|
-
if not result or not isinstance(result, NamedQueryNode):
|
|
156
|
-
return None
|
|
157
|
-
return result
|
|
158
|
-
|
|
159
|
-
# -- save ----------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
def save(
|
|
162
|
-
self,
|
|
163
|
-
query_key: str,
|
|
164
|
-
*,
|
|
165
|
-
title: str | None = None,
|
|
166
|
-
description: str | None = None,
|
|
167
|
-
tags: builtins.list[str] | None = None,
|
|
168
|
-
query_content: str | None = None,
|
|
169
|
-
language: str | None = None,
|
|
170
|
-
params_schema: dict | None = None,
|
|
171
|
-
ui_schema: dict | None = None,
|
|
172
|
-
) -> None:
|
|
173
|
-
"""Create or update a named query.
|
|
174
|
-
|
|
175
|
-
Fetches the existing node (if any), updates only the provided
|
|
176
|
-
(non-None) fields, and persists via ``create_or_merge``. New
|
|
177
|
-
nodes get a ``gid_`` automatically.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
query_key: Unique identifier (kebab-case).
|
|
181
|
-
title: Display name.
|
|
182
|
-
description: What the query does.
|
|
183
|
-
tags: Categorization tags (serialized to JSON).
|
|
184
|
-
query_content: The query text (Cypher, Jinja2, or Datalog).
|
|
185
|
-
language: ``"cypher"``, ``"jinja2"``, or ``"datalog"``.
|
|
186
|
-
params_schema: JSON Schema dict for parameter validation.
|
|
187
|
-
ui_schema: UI rendering hints dict for sonnet-ui.
|
|
188
|
-
"""
|
|
189
|
-
existing = self._load_node(query_key)
|
|
190
|
-
node = existing if existing is not None else NamedQueryNode(query_key=query_key)
|
|
191
|
-
|
|
192
|
-
# Update only non-None fields (partial update)
|
|
193
|
-
if title is not None:
|
|
194
|
-
node.title = title
|
|
195
|
-
if description is not None:
|
|
196
|
-
node.description = description
|
|
197
|
-
if tags is not None:
|
|
198
|
-
node.tags = json.dumps(tags, ensure_ascii=False)
|
|
199
|
-
if query_content is not None:
|
|
200
|
-
node.query_content = query_content
|
|
201
|
-
if language is not None:
|
|
202
|
-
node.language = language
|
|
203
|
-
if params_schema is not None:
|
|
204
|
-
node.params_schema = json.dumps(params_schema, ensure_ascii=False)
|
|
205
|
-
if ui_schema is not None:
|
|
206
|
-
node.ui_schema = json.dumps(ui_schema, ensure_ascii=False)
|
|
207
|
-
|
|
208
|
-
node.updated_at = arrow.utcnow().isoformat()
|
|
209
|
-
|
|
210
|
-
self._cdb.create_or_merge(node)
|
|
211
|
-
|
|
212
|
-
# -- remove --------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
def remove(self, query_key: str) -> bool:
|
|
215
|
-
"""Remove a named query. Returns True if it existed."""
|
|
216
|
-
existing = self._load_node(query_key)
|
|
217
|
-
if existing is None:
|
|
218
|
-
return False
|
|
219
|
-
self._cdb.delete(existing)
|
|
220
|
-
return True
|
|
221
|
-
|
|
222
|
-
def remove_all(self) -> int:
|
|
223
|
-
"""Remove all named queries. Returns count removed.
|
|
224
|
-
|
|
225
|
-
Used for clean re-seeding.
|
|
226
|
-
"""
|
|
227
|
-
result = self._cdb.execute(
|
|
228
|
-
"MATCH (q:_NamedQuery) RETURN count(q)",
|
|
229
|
-
unnest_result=True,
|
|
230
|
-
fetch_one=True,
|
|
231
|
-
)
|
|
232
|
-
count = int(result) if result else 0
|
|
233
|
-
if count > 0:
|
|
234
|
-
self._cdb.execute("MATCH (q:_NamedQuery) DELETE q")
|
|
235
|
-
return count
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
# -- helpers -----------------------------------------------------------------
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def _deserialize_tags(tags_str: str | None) -> list[str]:
|
|
242
|
-
"""Deserialize tags from JSON string to list."""
|
|
243
|
-
if not tags_str:
|
|
244
|
-
return []
|
|
245
|
-
try:
|
|
246
|
-
parsed = json.loads(tags_str)
|
|
247
|
-
if isinstance(parsed, list):
|
|
248
|
-
return parsed
|
|
249
|
-
except json.JSONDecodeError, TypeError:
|
|
250
|
-
pass
|
|
251
|
-
return []
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def _deserialize_json(value: str | None) -> dict | None:
|
|
255
|
-
"""Deserialize a JSON string to dict, or None."""
|
|
256
|
-
if not value:
|
|
257
|
-
return None
|
|
258
|
-
try:
|
|
259
|
-
parsed = json.loads(value)
|
|
260
|
-
if isinstance(parsed, dict):
|
|
261
|
-
return parsed
|
|
262
|
-
except json.JSONDecodeError, TypeError:
|
|
263
|
-
pass
|
|
264
|
-
return None
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
"""Graph metadata initialization and query seeding utilities.
|
|
2
|
-
|
|
3
|
-
Two separate concerns:
|
|
4
|
-
|
|
5
|
-
``init_graph_metadata(cdb, ...)`` -- initialize model and description
|
|
6
|
-
on the :_GraphModel node. One-time setup, called by
|
|
7
|
-
``lauro graph init``. Raises on failure.
|
|
8
|
-
|
|
9
|
-
``seed_named_queries(cdb, queries_dir)`` -- seed :_NamedQuery nodes from
|
|
10
|
-
file pairs (.json + .cypher/.jinja/.dl). Data seeding, idempotent
|
|
11
|
-
via MERGE. Returns ``QuerySeedResult`` with saved/skipped counts.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import json
|
|
17
|
-
from dataclasses import dataclass, field
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
from cypher_graphdb import CypherGraphDB
|
|
21
|
-
from loguru import logger
|
|
22
|
-
|
|
23
|
-
from sonnet_graph.metadata.model_store import GraphModelStore
|
|
24
|
-
from sonnet_graph.metadata.query_store import GraphQueryStore
|
|
25
|
-
|
|
26
|
-
# File extensions mapped to query language.
|
|
27
|
-
_QUERY_EXTENSIONS: dict[str, str] = {
|
|
28
|
-
".cypher": "cypher",
|
|
29
|
-
".jinja": "jinja2",
|
|
30
|
-
".jinja2": "jinja2",
|
|
31
|
-
".dl": "datalog",
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@dataclass
|
|
36
|
-
class QuerySeedResult:
|
|
37
|
-
"""Result of a ``seed_named_queries`` call.
|
|
38
|
-
|
|
39
|
-
Carries the partial-failure outcome: which query files were saved
|
|
40
|
-
and which were skipped (e.g. missing matching query file, parse
|
|
41
|
-
error). Callers use this to log, warn, or fail.
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
queries_saved: list[str] = field(default_factory=list)
|
|
45
|
-
queries_skipped: list[str] = field(default_factory=list)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def init_graph_metadata(
|
|
49
|
-
cdb: CypherGraphDB,
|
|
50
|
-
*,
|
|
51
|
-
model: dict | None = None,
|
|
52
|
-
models_path: str | Path | None = None,
|
|
53
|
-
model_name: str | None = None,
|
|
54
|
-
model_version: str | None = None,
|
|
55
|
-
description: str | None = None,
|
|
56
|
-
) -> None:
|
|
57
|
-
"""Initialize graph metadata -- model schema and description.
|
|
58
|
-
|
|
59
|
-
Writes the :_GraphModel node with schema and optional description.
|
|
60
|
-
Intended for one-time graph initialization (``lauro graph init``).
|
|
61
|
-
Does NOT call ``commit()`` -- the caller is responsible.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
cdb: Active CypherGraphDB connection.
|
|
65
|
-
model: JSON Schema dict to write as :_GraphModel node.
|
|
66
|
-
models_path: Path to a Python file or directory declaring graph
|
|
67
|
-
models on a ModelProvider. Generates and saves the schema.
|
|
68
|
-
Mutually exclusive with ``model``.
|
|
69
|
-
model_name: Graph model name.
|
|
70
|
-
model_version: Schema version string.
|
|
71
|
-
description: Markdown string stored on the :_GraphModel node.
|
|
72
|
-
|
|
73
|
-
Raises:
|
|
74
|
-
ValueError: If both ``model`` and ``models_path`` are provided.
|
|
75
|
-
ValueError: If ``description`` is provided without a model/models_path
|
|
76
|
-
and no :_GraphModel node already exists in the graph. Use
|
|
77
|
-
``GraphModelStore.update_description()`` directly to update
|
|
78
|
-
an existing node's description independently.
|
|
79
|
-
"""
|
|
80
|
-
if model is not None and models_path is not None:
|
|
81
|
-
raise ValueError("Provide either 'model' or 'models_path', not both")
|
|
82
|
-
|
|
83
|
-
store = GraphModelStore(cdb)
|
|
84
|
-
|
|
85
|
-
if model is not None:
|
|
86
|
-
store.save(model, name=model_name, version=model_version)
|
|
87
|
-
logger.debug("Initialized graph model '{}' from dict", model_name or "")
|
|
88
|
-
elif models_path is not None:
|
|
89
|
-
store.save_from_path(models_path, name=model_name, version=model_version)
|
|
90
|
-
logger.debug("Initialized graph model '{}' from path '{}'", model_name or "", models_path)
|
|
91
|
-
|
|
92
|
-
if description is not None:
|
|
93
|
-
store.update_description(description)
|
|
94
|
-
logger.debug("Updated graph description")
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def seed_named_queries(
|
|
98
|
-
cdb: CypherGraphDB,
|
|
99
|
-
queries_dir: str | Path,
|
|
100
|
-
) -> QuerySeedResult:
|
|
101
|
-
"""Seed :_NamedQuery nodes from file pairs in a directory.
|
|
102
|
-
|
|
103
|
-
Each pair is a ``.json`` metadata file + a query file (``.cypher``,
|
|
104
|
-
``.jinja``, ``.jinja2``, or ``.dl``). The file stem is used as
|
|
105
|
-
``query_key`` if not specified in the JSON.
|
|
106
|
-
|
|
107
|
-
Does NOT call ``commit()`` -- the caller is responsible.
|
|
108
|
-
|
|
109
|
-
Args:
|
|
110
|
-
cdb: Active CypherGraphDB connection.
|
|
111
|
-
queries_dir: Directory with query file pairs.
|
|
112
|
-
|
|
113
|
-
Returns:
|
|
114
|
-
QuerySeedResult with saved and skipped query keys.
|
|
115
|
-
"""
|
|
116
|
-
result = QuerySeedResult()
|
|
117
|
-
_seed_queries(GraphQueryStore(cdb), Path(queries_dir), result)
|
|
118
|
-
return result
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _seed_queries(store: GraphQueryStore, queries_dir: Path, result: QuerySeedResult) -> None:
|
|
122
|
-
"""Seed named queries from file pairs in a directory."""
|
|
123
|
-
if not queries_dir.is_dir():
|
|
124
|
-
logger.warning("Queries directory does not exist: {}", queries_dir)
|
|
125
|
-
return
|
|
126
|
-
|
|
127
|
-
for json_path in sorted(queries_dir.glob("*.json")):
|
|
128
|
-
stem = json_path.stem
|
|
129
|
-
query_path = _find_query_file(queries_dir, stem)
|
|
130
|
-
if query_path is None:
|
|
131
|
-
logger.warning("No query file found for {}.json (expected .cypher, .jinja, .jinja2, or .dl)", stem)
|
|
132
|
-
result.queries_skipped.append(stem)
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
|
-
try:
|
|
136
|
-
_seed_one_query(store, json_path, query_path, stem)
|
|
137
|
-
result.queries_saved.append(stem)
|
|
138
|
-
logger.debug("Seeded query '{}'", stem)
|
|
139
|
-
except Exception:
|
|
140
|
-
logger.exception("Failed to seed query '{}' from {}", stem, json_path)
|
|
141
|
-
result.queries_skipped.append(stem)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _find_query_file(directory: Path, stem: str) -> Path | None:
|
|
145
|
-
"""Find the query file matching a JSON metadata file."""
|
|
146
|
-
for ext in _QUERY_EXTENSIONS:
|
|
147
|
-
candidate = directory / f"{stem}{ext}"
|
|
148
|
-
if candidate.is_file():
|
|
149
|
-
return candidate
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def _seed_one_query(store: GraphQueryStore, json_path: Path, query_path: Path, stem: str) -> None:
|
|
154
|
-
"""Read a file pair and save as a named query."""
|
|
155
|
-
metadata = json.loads(json_path.read_text(encoding="utf-8"))
|
|
156
|
-
query_content = query_path.read_text(encoding="utf-8")
|
|
157
|
-
language = _QUERY_EXTENSIONS.get(query_path.suffix, "cypher")
|
|
158
|
-
|
|
159
|
-
query_key = metadata.get("template_key") or metadata.get("query_key") or stem
|
|
160
|
-
|
|
161
|
-
store.save(
|
|
162
|
-
query_key,
|
|
163
|
-
title=metadata.get("title"),
|
|
164
|
-
description=metadata.get("description"),
|
|
165
|
-
tags=metadata.get("tags"),
|
|
166
|
-
query_content=query_content,
|
|
167
|
-
language=language,
|
|
168
|
-
params_schema=metadata.get("json_schema") or metadata.get("params_schema"),
|
|
169
|
-
ui_schema=metadata.get("ui_schema"),
|
|
170
|
-
)
|
|
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
|
|
File without changes
|
|
File without changes
|