cyphersmith 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aswin K V
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,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: cyphersmith
3
+ Version: 0.1.0
4
+ Summary: Turn natural language into read-only Cypher, validate it with CyVer, and execute it against Neo4j — powered by LiteLLM.
5
+ Author: Aswin K V
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Aswin-K-V/Neo4j-Cypher-generator
8
+ Project-URL: Repository, https://github.com/Aswin-K-V/Neo4j-Cypher-generator
9
+ Project-URL: Issues, https://github.com/Aswin-K-V/Neo4j-Cypher-generator/issues
10
+ Keywords: neo4j,cypher,llm,natural-language,graph-database,litellm,text-to-cypher
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: neo4j>=5.20
25
+ Requires-Dist: litellm>=1.0
26
+ Requires-Dist: CyVer>=2.0.0
27
+ Requires-Dist: pydantic>=2.0
28
+ Requires-Dist: PyYAML>=6.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Requires-Dist: pytest>=8.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # cyphersmith
35
+
36
+ `cyphersmith` turns natural language into read-only Cypher, validates it with CyVer, executes it against Neo4j, and returns the query plus records.
37
+
38
+ It uses LiteLLM for provider-neutral model access, so the same package works with OpenAI, Azure OpenAI, Anthropic, Gemini, and all other LiteLLM-supported providers.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install cyphersmith
44
+ ```
45
+
46
+ ## Python API
47
+
48
+ ```python
49
+ from cyphersmith import CypherGenerator, LLMConfig, Neo4jCredentials
50
+
51
+ generator = CypherGenerator(
52
+ neo4j=Neo4jCredentials(
53
+ uri="bolt://localhost:7687",
54
+ username="neo4j",
55
+ password="password",
56
+ database="neo4j",
57
+ ),
58
+ llm=LLMConfig(
59
+ model="openai/gpt-4o-mini",
60
+ temperature=0,
61
+ ),
62
+ business_context_path="business_context.yaml",
63
+ )
64
+
65
+ result = generator.ask("show top 10 items by a chosen metric")
66
+ print(result.cypher)
67
+ print(result.records)
68
+ ```
69
+
70
+ ## CLI
71
+
72
+ Interactive setup and question loop:
73
+
74
+ ```bash
75
+ cyphersmith chat --pretty
76
+ ```
77
+
78
+ The interactive command prompts for:
79
+
80
+ - LLM provider and model
81
+ - API key and any provider-specific endpoint values
82
+ - Neo4j URI, username, password, and database
83
+ - optional business context file path
84
+
85
+ After setup, keep asking questions at `Question>`. Type `/exit` to stop. Each answer prints the generated Cypher query, records, validation details, attempts, and any error.
86
+
87
+ One-shot command:
88
+
89
+ ```bash
90
+ cyphersmith ask \
91
+ --neo4j-uri bolt://localhost:7687 \
92
+ --neo4j-user neo4j \
93
+ --neo4j-password password \
94
+ --neo4j-database neo4j \
95
+ --model openai/gpt-4o-mini \
96
+ --business-context business_context.yaml \
97
+ "show top 10 items by a chosen metric"
98
+ ```
99
+
100
+ The CLI prints live intermediate progress steps to `stderr` (context load, schema fetch, generation attempts, safety checks, validation, and execution) with terminal colors for readability. Final structured output stays on `stdout`.
101
+
102
+ Use these flags if needed:
103
+
104
+ - `--no-progress`: disable intermediate step logs
105
+ - `--no-color`: keep progress logs but disable ANSI colors
106
+
107
+ The final CLI payload includes `cypher`, `records`, `validation`, `attempts`, and `error`.
108
+
109
+ ## Environment Variables
110
+
111
+ Neo4j values can be passed explicitly or read from:
112
+
113
+ - `NEO4J_URI`
114
+ - `NEO4J_USER`
115
+ - `NEO4J_PASSWORD`
116
+ - `NEO4J_DATABASE`
117
+
118
+ LiteLLM credentials should use the environment variables expected by the provider, such as `OPENAI_API_KEY`, `AZURE_API_KEY`, `AZURE_API_BASE`, or provider-specific equivalents.
119
+
120
+ ## Business Context
121
+
122
+ Pass a `.txt`, `.md`, `.yaml`, `.yml`, or `.json` file through `business_context_path` or `--business-context`. The content is added to the Cypher generation prompt as business context only; it is not treated as a schema source.
123
+
124
+ ## Safety
125
+
126
+ Generated Cypher is blocked unless it passes both:
127
+
128
+ - a local read-only keyword/procedure check
129
+ - CyVer syntax, schema, and property validation
130
+
131
+ The package will not execute generated queries containing obvious write operations such as `CREATE`, `MERGE`, `DELETE`, `SET`, `REMOVE`, or procedures like `CALL db` and `CALL apoc`.
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,102 @@
1
+ # cyphersmith
2
+
3
+ `cyphersmith` turns natural language into read-only Cypher, validates it with CyVer, executes it against Neo4j, and returns the query plus records.
4
+
5
+ It uses LiteLLM for provider-neutral model access, so the same package works with OpenAI, Azure OpenAI, Anthropic, Gemini, and all other LiteLLM-supported providers.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install cyphersmith
11
+ ```
12
+
13
+ ## Python API
14
+
15
+ ```python
16
+ from cyphersmith import CypherGenerator, LLMConfig, Neo4jCredentials
17
+
18
+ generator = CypherGenerator(
19
+ neo4j=Neo4jCredentials(
20
+ uri="bolt://localhost:7687",
21
+ username="neo4j",
22
+ password="password",
23
+ database="neo4j",
24
+ ),
25
+ llm=LLMConfig(
26
+ model="openai/gpt-4o-mini",
27
+ temperature=0,
28
+ ),
29
+ business_context_path="business_context.yaml",
30
+ )
31
+
32
+ result = generator.ask("show top 10 items by a chosen metric")
33
+ print(result.cypher)
34
+ print(result.records)
35
+ ```
36
+
37
+ ## CLI
38
+
39
+ Interactive setup and question loop:
40
+
41
+ ```bash
42
+ cyphersmith chat --pretty
43
+ ```
44
+
45
+ The interactive command prompts for:
46
+
47
+ - LLM provider and model
48
+ - API key and any provider-specific endpoint values
49
+ - Neo4j URI, username, password, and database
50
+ - optional business context file path
51
+
52
+ After setup, keep asking questions at `Question>`. Type `/exit` to stop. Each answer prints the generated Cypher query, records, validation details, attempts, and any error.
53
+
54
+ One-shot command:
55
+
56
+ ```bash
57
+ cyphersmith ask \
58
+ --neo4j-uri bolt://localhost:7687 \
59
+ --neo4j-user neo4j \
60
+ --neo4j-password password \
61
+ --neo4j-database neo4j \
62
+ --model openai/gpt-4o-mini \
63
+ --business-context business_context.yaml \
64
+ "show top 10 items by a chosen metric"
65
+ ```
66
+
67
+ The CLI prints live intermediate progress steps to `stderr` (context load, schema fetch, generation attempts, safety checks, validation, and execution) with terminal colors for readability. Final structured output stays on `stdout`.
68
+
69
+ Use these flags if needed:
70
+
71
+ - `--no-progress`: disable intermediate step logs
72
+ - `--no-color`: keep progress logs but disable ANSI colors
73
+
74
+ The final CLI payload includes `cypher`, `records`, `validation`, `attempts`, and `error`.
75
+
76
+ ## Environment Variables
77
+
78
+ Neo4j values can be passed explicitly or read from:
79
+
80
+ - `NEO4J_URI`
81
+ - `NEO4J_USER`
82
+ - `NEO4J_PASSWORD`
83
+ - `NEO4J_DATABASE`
84
+
85
+ LiteLLM credentials should use the environment variables expected by the provider, such as `OPENAI_API_KEY`, `AZURE_API_KEY`, `AZURE_API_BASE`, or provider-specific equivalents.
86
+
87
+ ## Business Context
88
+
89
+ Pass a `.txt`, `.md`, `.yaml`, `.yml`, or `.json` file through `business_context_path` or `--business-context`. The content is added to the Cypher generation prompt as business context only; it is not treated as a schema source.
90
+
91
+ ## Safety
92
+
93
+ Generated Cypher is blocked unless it passes both:
94
+
95
+ - a local read-only keyword/procedure check
96
+ - CyVer syntax, schema, and property validation
97
+
98
+ The package will not execute generated queries containing obvious write operations such as `CREATE`, `MERGE`, `DELETE`, `SET`, `REMOVE`, or procedures like `CALL db` and `CALL apoc`.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cyphersmith"
7
+ version = "0.1.0"
8
+ description = "Turn natural language into read-only Cypher, validate it with CyVer, and execute it against Neo4j — powered by LiteLLM."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Aswin K V" }
14
+ ]
15
+ keywords = ["neo4j", "cypher", "llm", "natural-language", "graph-database", "litellm", "text-to-cypher"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Database",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "neo4j>=5.20",
30
+ "litellm>=1.0",
31
+ "CyVer>=2.0.0",
32
+ "pydantic>=2.0",
33
+ "PyYAML>=6.0",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/Aswin-K-V/Neo4j-Cypher-generator"
38
+ Repository = "https://github.com/Aswin-K-V/Neo4j-Cypher-generator"
39
+ Issues = "https://github.com/Aswin-K-V/Neo4j-Cypher-generator/issues"
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "build>=1.2",
44
+ "pytest>=8.0",
45
+ ]
46
+
47
+ [project.scripts]
48
+ cyphersmith = "cyphersmith.cli:main"
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+
53
+ [tool.pytest.ini_options]
54
+ testpaths = ["tests"]
55
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ """Public API for the cyphersmith package."""
2
+
3
+ from .generator import CypherGenerator
4
+ from .models import CypherGenerationResult, LLMConfig, Neo4jCredentials
5
+ from .neo4j_client import Neo4jClient, get_neo4j_client
6
+ from .progress import TerminalProgressReporter
7
+
8
+ __all__ = [
9
+ "CypherGenerationResult",
10
+ "CypherGenerator",
11
+ "LLMConfig",
12
+ "Neo4jClient",
13
+ "Neo4jCredentials",
14
+ "TerminalProgressReporter",
15
+ "get_neo4j_client",
16
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,303 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import getpass
5
+ import json
6
+ import sys
7
+ from typing import Sequence
8
+
9
+ from .generator import CypherGenerator
10
+ from .models import LLMConfig, Neo4jCredentials
11
+ from .progress import TerminalProgressReporter
12
+
13
+
14
+ def main(argv: Sequence[str] | None = None) -> int:
15
+ parser = _build_parser()
16
+ args = parser.parse_args(argv)
17
+
18
+ if args.command == "ask":
19
+ return _run_ask(args)
20
+ if args.command in {"chat", "setup", "interactive"}:
21
+ return _run_chat(args)
22
+
23
+ parser.print_help()
24
+ return 2
25
+
26
+
27
+ def _build_parser() -> argparse.ArgumentParser:
28
+ parser = argparse.ArgumentParser(
29
+ prog="cypher-generator",
30
+ description="Generate, validate, and execute read-only Cypher against Neo4j.",
31
+ )
32
+ subparsers = parser.add_subparsers(dest="command")
33
+
34
+ ask = subparsers.add_parser("ask", help="Ask a natural-language graph question.")
35
+ ask.add_argument("query", help="Natural-language graph question.")
36
+ ask.add_argument("--neo4j-uri", dest="neo4j_uri")
37
+ ask.add_argument("--neo4j-user", dest="neo4j_user")
38
+ ask.add_argument("--neo4j-password", dest="neo4j_password")
39
+ ask.add_argument("--neo4j-database", dest="neo4j_database", default=None)
40
+ ask.add_argument("--model", required=True, help="LiteLLM model name.")
41
+ ask.add_argument("--temperature", type=float, default=0)
42
+ ask.add_argument("--timeout", type=float, default=None)
43
+ ask.add_argument("--api-key", dest="api_key", default=None)
44
+ ask.add_argument("--api-base", dest="api_base", default=None)
45
+ ask.add_argument("--api-version", dest="api_version", default=None)
46
+ ask.add_argument("--business-context", dest="business_context", default=None)
47
+ ask.add_argument("--max-validation-retries", type=int, default=2)
48
+ ask.add_argument(
49
+ "--no-progress",
50
+ action="store_true",
51
+ help="Disable intermediate progress output.",
52
+ )
53
+ ask.add_argument(
54
+ "--no-color",
55
+ action="store_true",
56
+ help="Disable ANSI colors in progress output.",
57
+ )
58
+ ask.add_argument("--pretty", action="store_true", help="Pretty-print JSON output.")
59
+ ask.set_defaults(command="ask")
60
+
61
+ chat = subparsers.add_parser(
62
+ "chat",
63
+ aliases=["setup", "interactive"],
64
+ help="Prompt for setup once, then ask questions in a loop.",
65
+ )
66
+ chat.add_argument("--pretty", action="store_true", help="Pretty-print result JSON.")
67
+ chat.add_argument("--max-validation-retries", type=int, default=2)
68
+ chat.add_argument(
69
+ "--no-progress",
70
+ action="store_true",
71
+ help="Disable intermediate progress output.",
72
+ )
73
+ chat.add_argument(
74
+ "--no-color",
75
+ action="store_true",
76
+ help="Disable ANSI colors in progress output.",
77
+ )
78
+ chat.set_defaults(command="chat")
79
+ return parser
80
+
81
+
82
+ def _run_ask(args: argparse.Namespace) -> int:
83
+ progress_reporter = _build_progress_reporter(args)
84
+ generator = CypherGenerator(
85
+ neo4j=Neo4jCredentials(
86
+ uri=args.neo4j_uri,
87
+ username=args.neo4j_user,
88
+ password=args.neo4j_password,
89
+ database=args.neo4j_database,
90
+ ),
91
+ llm=LLMConfig(
92
+ model=args.model,
93
+ temperature=args.temperature,
94
+ timeout=args.timeout,
95
+ api_key=args.api_key,
96
+ api_base=args.api_base,
97
+ api_version=args.api_version,
98
+ ),
99
+ business_context_path=args.business_context,
100
+ max_validation_retries=args.max_validation_retries,
101
+ progress_reporter=progress_reporter,
102
+ )
103
+ result = generator.ask(args.query)
104
+ payload = result.model_dump(mode="json")
105
+ print(
106
+ json.dumps(
107
+ payload,
108
+ indent=2 if args.pretty else None,
109
+ ensure_ascii=False,
110
+ )
111
+ )
112
+ return 1 if result.error else 0
113
+
114
+
115
+ def _run_chat(args: argparse.Namespace) -> int:
116
+ print("Cypher Generator interactive setup")
117
+ print("Press Enter to accept defaults. Type /exit in the question loop to stop.")
118
+
119
+ llm_config = _prompt_llm_config()
120
+ neo4j_credentials = _prompt_neo4j_credentials()
121
+ business_context = _prompt_optional(
122
+ "Business context file path (.txt/.md/.yaml/.json), blank for none",
123
+ default="",
124
+ )
125
+
126
+ generator = CypherGenerator(
127
+ neo4j=neo4j_credentials,
128
+ llm=llm_config,
129
+ business_context_path=business_context or None,
130
+ max_validation_retries=args.max_validation_retries,
131
+ progress_reporter=_build_progress_reporter(args),
132
+ )
133
+
134
+ print("\nSetup complete. Ask a question, or type /exit to quit.")
135
+ while True:
136
+ try:
137
+ question = input("\nQuestion> ").strip()
138
+ except (EOFError, KeyboardInterrupt):
139
+ print()
140
+ return 0
141
+
142
+ if question.lower() in {"/exit", "/quit", "exit", "quit", "q"}:
143
+ return 0
144
+ if not question:
145
+ continue
146
+
147
+ result = generator.ask(question)
148
+ _print_interactive_result(result, pretty=args.pretty)
149
+
150
+
151
+ def _prompt_llm_config() -> LLMConfig:
152
+ providers = [
153
+ ("OpenAI", "openai"),
154
+ ("Azure OpenAI", "azure"),
155
+ ("Anthropic", "anthropic"),
156
+ ("Google Gemini", "gemini"),
157
+ ("Groq", "groq"),
158
+ ("Custom LiteLLM model string", "custom"),
159
+ ]
160
+
161
+ print("\nLLM provider")
162
+ for index, (label, _) in enumerate(providers, start=1):
163
+ print(f" {index}. {label}")
164
+
165
+ provider_index = _prompt_choice("Select provider", default=1, maximum=len(providers))
166
+ provider = providers[provider_index - 1][1]
167
+
168
+ if provider == "openai":
169
+ model = _prompt_optional("Model", default="openai/gpt-5")
170
+ api_key = _prompt_secret("OpenAI API key", required=True)
171
+ return LLMConfig(model=model, api_key=api_key, temperature=0)
172
+
173
+ if provider == "azure":
174
+ deployment = _prompt_required(
175
+ "Azure deployment name, without azure/ prefix"
176
+ )
177
+ api_key = _prompt_secret("Azure OpenAI API key", required=True)
178
+ api_base = _prompt_required(
179
+ "Azure API base, e.g. https://your-resource.openai.azure.com"
180
+ )
181
+ api_version = _prompt_optional("Azure API version", default="2024-10-21")
182
+ return LLMConfig(
183
+ model=f"azure/{deployment}",
184
+ api_key=api_key,
185
+ api_base=api_base,
186
+ api_version=api_version,
187
+ temperature=0,
188
+ )
189
+
190
+ if provider == "anthropic":
191
+ model = _prompt_optional(
192
+ "Model",
193
+ default="anthropic/claude-3-5-sonnet-latest",
194
+ )
195
+ api_key = _prompt_secret("Anthropic API key", required=True)
196
+ return LLMConfig(model=model, api_key=api_key, temperature=0)
197
+
198
+ if provider == "gemini":
199
+ model = _prompt_optional("Model", default="gemini/gemini-2.5-pro")
200
+ api_key = _prompt_secret("Gemini API key", required=True)
201
+ return LLMConfig(model=model, api_key=api_key, temperature=0)
202
+
203
+ if provider == "groq":
204
+ model = _prompt_optional("Model", default="groq/llama-3.3-70b-versatile")
205
+ api_key = _prompt_secret("Groq API key", required=True)
206
+ return LLMConfig(model=model, api_key=api_key, temperature=0)
207
+
208
+ model = _prompt_required(
209
+ "LiteLLM model string, e.g. openai/gpt-5 or azure/my-deployment"
210
+ )
211
+ api_key = _prompt_secret("API key, blank to use provider environment variables")
212
+ api_base = _prompt_optional("API base, blank unless provider needs it", default="")
213
+ api_version = _prompt_optional(
214
+ "API version, blank unless provider needs it",
215
+ default="",
216
+ )
217
+ return LLMConfig(
218
+ model=model,
219
+ api_key=api_key or None,
220
+ api_base=api_base or None,
221
+ api_version=api_version or None,
222
+ temperature=0,
223
+ )
224
+
225
+
226
+ def _prompt_neo4j_credentials() -> Neo4jCredentials:
227
+ print("\nNeo4j connection")
228
+ uri = _prompt_optional("Neo4j URI", default="bolt://localhost:7687")
229
+ username = _prompt_optional("Neo4j username", default="neo4j")
230
+ password = _prompt_secret("Neo4j password", required=True)
231
+ database = _prompt_optional("Neo4j database", default="neo4j")
232
+ return Neo4jCredentials(
233
+ uri=uri,
234
+ username=username,
235
+ password=password,
236
+ database=database,
237
+ )
238
+
239
+
240
+ def _prompt_choice(prompt: str, *, default: int, maximum: int) -> int:
241
+ while True:
242
+ raw = input(f"{prompt} [{default}]: ").strip()
243
+ if not raw:
244
+ return default
245
+ try:
246
+ value = int(raw)
247
+ except ValueError:
248
+ print(f"Enter a number from 1 to {maximum}.", file=sys.stderr)
249
+ continue
250
+ if 1 <= value <= maximum:
251
+ return value
252
+ print(f"Enter a number from 1 to {maximum}.", file=sys.stderr)
253
+
254
+
255
+ def _prompt_optional(prompt: str, *, default: str = "") -> str:
256
+ suffix = f" [{default}]" if default else ""
257
+ value = input(f"{prompt}{suffix}: ").strip()
258
+ return value or default
259
+
260
+
261
+ def _prompt_required(prompt: str) -> str:
262
+ while True:
263
+ value = input(f"{prompt}: ").strip()
264
+ if value:
265
+ return value
266
+ print("This value is required.", file=sys.stderr)
267
+
268
+
269
+ def _prompt_secret(prompt: str, *, required: bool = False) -> str:
270
+ while True:
271
+ value = getpass.getpass(f"{prompt}: ").strip()
272
+ if value or not required:
273
+ return value
274
+ print("This value is required.", file=sys.stderr)
275
+
276
+
277
+ def _print_interactive_result(result: object, *, pretty: bool) -> None:
278
+ cypher = getattr(result, "cypher", "") or ""
279
+ records = getattr(result, "records", []) or []
280
+ validation = getattr(result, "validation", {}) or {}
281
+ error = getattr(result, "error", None)
282
+ attempts = getattr(result, "attempts", 0)
283
+
284
+ print("\nCypher:")
285
+ print(cypher or "(none)")
286
+
287
+ print("\nResults:")
288
+ print(json.dumps(records, indent=2 if pretty else None, ensure_ascii=False))
289
+
290
+ print("\nValidation:")
291
+ print(json.dumps(validation, indent=2 if pretty else None, ensure_ascii=False))
292
+
293
+ print(f"\nAttempts: {attempts}")
294
+ if error:
295
+ print(f"Error: {error}", file=sys.stderr)
296
+
297
+
298
+ def _build_progress_reporter(
299
+ args: argparse.Namespace,
300
+ ) -> TerminalProgressReporter | None:
301
+ if getattr(args, "no_progress", False):
302
+ return None
303
+ return TerminalProgressReporter(use_color=not getattr(args, "no_color", False))