ravmem-cli 0.1.0__py3-none-any.whl

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,247 @@
1
+ """Structured YAML config loader with pydantic validation and env-var override.
2
+
3
+ Resolution order (first found wins):
4
+ 1. --config CLI flag → passed as config_path argument
5
+ 2. RAVMEM_CONFIG env var → path to a YAML file
6
+ 3. $RAVMEM_DATA_DIR/ravmem.yaml
7
+ 4. ~/.ravmem/ravmem.yaml
8
+ 5. Built-in defaults (pydantic model defaults)
9
+
10
+ Env vars always win over file values — existing env var names continue to work
11
+ exactly as before; no breaking change for Docker Compose / systemd users.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import logging
17
+ import os
18
+ from pathlib import Path
19
+ from typing import Optional
20
+
21
+ import yaml
22
+ from pydantic import BaseModel, field_validator, model_validator
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _SENSITIVE_KEYS = {"admin_token", "password"}
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Pydantic config models
31
+ # ---------------------------------------------------------------------------
32
+
33
+ class TierLimits(BaseModel):
34
+ read: str = "1000/min"
35
+ mcp: str = "600/min"
36
+ write: str = "200/min"
37
+ index: str = "30/min"
38
+ auth: str = "60/min"
39
+
40
+ @field_validator("read", "mcp", "write", "index", "auth", mode="before")
41
+ @classmethod
42
+ def _validate_limit(cls, v: object) -> str:
43
+ if not isinstance(v, str):
44
+ v = str(v)
45
+ parts = v.split("/")
46
+ if len(parts) != 2 or not parts[0].strip().isdigit() or parts[1].strip() not in ("sec", "min", "hour"):
47
+ raise ValueError(f"Invalid rate limit format '{v}' — expected '<int>/sec|min|hour'")
48
+ return v
49
+
50
+
51
+ class RateLimitTiers(BaseModel):
52
+ admin: TierLimits = TierLimits(
53
+ read="5000/min", mcp="3000/min", write="1000/min", index="300/min", auth="300/min"
54
+ )
55
+ high: TierLimits = TierLimits(
56
+ read="3000/min", mcp="1800/min", write="600/min", index="120/min", auth="180/min"
57
+ )
58
+ standard: TierLimits = TierLimits(
59
+ read="1000/min", mcp="600/min", write="200/min", index="30/min", auth="60/min"
60
+ )
61
+ restricted: TierLimits = TierLimits(
62
+ read="200/min", mcp="120/min", write="40/min", index="5/min", auth="20/min"
63
+ )
64
+
65
+
66
+ class RateLimitConfig(BaseModel):
67
+ enabled: bool = True
68
+ limits: TierLimits = TierLimits()
69
+ tiers: RateLimitTiers = RateLimitTiers()
70
+ # Brute-force lockout (Phase 2)
71
+ lockout_threshold: int = 5
72
+ lockout_duration: int = 600 # seconds
73
+
74
+
75
+ class ServerConfig(BaseModel):
76
+ host: str = "0.0.0.0"
77
+ port: int = 7432
78
+ max_concurrent_jobs: int = 2
79
+ log_level: str = "INFO"
80
+ data_dir: str = "" # resolved from env/default at load time
81
+
82
+
83
+ class ArcadeDBConfig(BaseModel):
84
+ host: str = "localhost"
85
+ port: int = 2480
86
+ password: str = "arcadedb" # override via ARCADEDB_PASSWORD — never write to YAML
87
+
88
+
89
+ class AuthConfig(BaseModel):
90
+ system_db: str = "ravmem_system"
91
+ audit_log_retention_days: int = 90
92
+ admin_token: str = "" # env-only — warn if found in YAML
93
+
94
+
95
+ class ClusteringConfig(BaseModel):
96
+ seed: int = 42
97
+ resolution: float = 1.0
98
+ max_cluster_size: int = 500
99
+ subcluster_threshold: int = 200
100
+
101
+
102
+ class RavMemConfig(BaseModel):
103
+ server: ServerConfig = ServerConfig()
104
+ arcadedb: ArcadeDBConfig = ArcadeDBConfig()
105
+ auth: AuthConfig = AuthConfig()
106
+ clustering: ClusteringConfig = ClusteringConfig()
107
+ rate_limit: RateLimitConfig = RateLimitConfig()
108
+
109
+ @model_validator(mode="after")
110
+ def _warn_sensitive_in_file(self) -> "RavMemConfig":
111
+ if self.auth.admin_token:
112
+ logger.warning(
113
+ "SECURITY WARNING: 'auth.admin_token' found in config file. "
114
+ "Use the RAVMEM_ADMIN_TOKEN env var instead — config files may be world-readable."
115
+ )
116
+ if self.arcadedb.password not in ("arcadedb", ""):
117
+ pass # password in file is acceptable for dev; no warning needed
118
+ return self
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Loader
123
+ # ---------------------------------------------------------------------------
124
+
125
+ def _find_config_file(explicit_path: str | Path | None = None) -> Path | None:
126
+ """Return the first config file that exists, following the resolution order."""
127
+ candidates: list[Path] = []
128
+
129
+ if explicit_path:
130
+ candidates.append(Path(explicit_path))
131
+ elif env_path := os.environ.get("RAVMEM_CONFIG"):
132
+ candidates.append(Path(env_path))
133
+
134
+ data_dir = os.environ.get("RAVMEM_DATA_DIR", str(Path.home() / ".ravmem" / "server"))
135
+ candidates.append(Path(data_dir) / "ravmem.yaml")
136
+ candidates.append(Path.home() / ".ravmem" / "ravmem.yaml")
137
+
138
+ for path in candidates:
139
+ if path.exists():
140
+ return path
141
+ return None
142
+
143
+
144
+ def _deep_merge(base: dict, override: dict) -> dict:
145
+ """Recursively merge override into base. Override values take precedence."""
146
+ result = dict(base)
147
+ for key, val in override.items():
148
+ if key in result and isinstance(result[key], dict) and isinstance(val, dict):
149
+ result[key] = _deep_merge(result[key], val)
150
+ else:
151
+ result[key] = val
152
+ return result
153
+
154
+
155
+ def _apply_env_overrides(data: dict) -> dict:
156
+ """Apply env vars on top of parsed YAML data. Env vars always win."""
157
+ env_map: dict[str, list[tuple]] = {
158
+ "RAVMEM_LOG_LEVEL": [("server", "log_level")],
159
+ "RAVMEM_MAX_CONCURRENT_JOBS": [("server", "max_concurrent_jobs")],
160
+ "RAVMEM_DATA_DIR": [("server", "data_dir")],
161
+ "ARCADEDB_HOST": [("arcadedb", "host")],
162
+ "ARCADEDB_PORT": [("arcadedb", "port")],
163
+ "ARCADEDB_PASSWORD": [("arcadedb", "password")],
164
+ "RAVMEM_SYSTEM_DB": [("auth", "system_db")],
165
+ "RAVMEM_AUDIT_LOG_RETENTION_DAYS": [("auth", "audit_log_retention_days")],
166
+ "RAVMEM_CLUSTER_SEED": [("clustering", "seed")],
167
+ "RAVMEM_CLUSTER_RESOLUTION": [("clustering", "resolution")],
168
+ "RAVMEM_CLUSTER_MAX_SIZE": [("clustering", "max_cluster_size")],
169
+ "RAVMEM_CLUSTER_SUBCLUSTER_THRESHOLD": [("clustering", "subcluster_threshold")],
170
+ "RAVMEM_RATELIMIT_ENABLED": [("rate_limit", "enabled")],
171
+ "RAVMEM_RATELIMIT_AUTH": [
172
+ ("rate_limit", "limits", "auth"),
173
+ ("rate_limit", "tiers", "standard", "auth"),
174
+ ],
175
+ "RAVMEM_RATELIMIT_WRITE": [
176
+ ("rate_limit", "limits", "write"),
177
+ ("rate_limit", "tiers", "standard", "write"),
178
+ ],
179
+ "RAVMEM_RATELIMIT_INDEX": [
180
+ ("rate_limit", "limits", "index"),
181
+ ("rate_limit", "tiers", "standard", "index"),
182
+ ],
183
+ "RAVMEM_RATELIMIT_READ": [
184
+ ("rate_limit", "limits", "read"),
185
+ ("rate_limit", "tiers", "standard", "read"),
186
+ ],
187
+ "RAVMEM_RATELIMIT_MCP": [
188
+ ("rate_limit", "limits", "mcp"),
189
+ ("rate_limit", "tiers", "standard", "mcp"),
190
+ ],
191
+ "RAVMEM_RATELIMIT_LOCKOUT_THRESHOLD": [("rate_limit", "lockout_threshold")],
192
+ "RAVMEM_RATELIMIT_LOCKOUT_DURATION": [("rate_limit", "lockout_duration")],
193
+ }
194
+ for env_key, paths in env_map.items():
195
+ val = os.environ.get(env_key)
196
+ if val is None:
197
+ continue
198
+ # Convert boolean strings
199
+ if isinstance(val, str) and val.lower() in ("true", "1", "yes"):
200
+ val = True
201
+ elif isinstance(val, str) and val.lower() in ("false", "0", "no"):
202
+ val = False
203
+ # Apply to all target paths
204
+ for path in paths:
205
+ node = data
206
+ for part in path[:-1]:
207
+ node = node.setdefault(part, {})
208
+ node[path[-1]] = val
209
+ return data
210
+
211
+
212
+ def load_config(config_path: str | Path | None = None) -> RavMemConfig:
213
+ """Load, merge, validate and return the resolved RavMemConfig."""
214
+ raw: dict = {}
215
+
216
+ found = _find_config_file(config_path)
217
+ if found:
218
+ logger.debug("Loading config from %s", found)
219
+ with open(found) as fh:
220
+ file_data = yaml.safe_load(fh) or {}
221
+ raw = _deep_merge(raw, file_data)
222
+
223
+ raw = _apply_env_overrides(raw)
224
+
225
+ try:
226
+ return RavMemConfig.model_validate(raw)
227
+ except Exception as exc:
228
+ raise ValueError(f"Invalid ravmem config: {exc}") from exc
229
+
230
+
231
+ # Module-level singleton — loaded lazily on first access
232
+ _config: RavMemConfig | None = None
233
+
234
+
235
+ def get_config(config_path: str | Path | None = None) -> RavMemConfig:
236
+ """Return the module-level config singleton, loading it if necessary."""
237
+ global _config
238
+ if _config is None:
239
+ _config = load_config(config_path)
240
+ return _config
241
+
242
+
243
+ def reload_config(config_path: str | Path | None = None) -> RavMemConfig:
244
+ """Force-reload config (useful in tests or after serve --config flag)."""
245
+ global _config
246
+ _config = load_config(config_path)
247
+ return _config
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: ravmem-cli
3
+ Version: 0.1.0
4
+ Summary: RavMem CLI — lightweight client for the RavMem code knowledge graph server
5
+ Author-email: RavMem <harpreet.singh@ravmem.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://www.ravmem.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: typer[all]>=0.12.0
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: pydantic>=2.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # ravmem-cli
27
+
28
+ Lightweight CLI client for the [RavMem](https://github.com/ravmem/ravmem) code knowledge graph server.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install ravmem-cli
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```bash
39
+ # Point the CLI at your RavMem server
40
+ ravmem remote config --url http://localhost:7432
41
+
42
+ # Register a repository
43
+ ravmem remote add /path/to/repo --name my-project --default-branch main
44
+
45
+ # Index a repository
46
+ ravmem analyze /path/to/repo --project <ID>
47
+
48
+ # Search symbols
49
+ ravmem search "authentication handler" --project <ID>
50
+
51
+ # Blast-radius analysis
52
+ ravmem impact MyClass.my_method --project <ID>
53
+ ```
54
+
55
+ ## Requirements
56
+
57
+ - Python 3.10+
58
+ - A running [ravmem-server](https://pypi.org/project/ravmem-server/) instance
59
+
60
+ ## License
61
+
62
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,25 @@
1
+ ravmem/config.py,sha256=SkoObiRD-kyHWkI94l3wJv1bURiHTQ1a6YSlrlBCvuk,3284
2
+ ravmem/config_loader.py,sha256=vvWJh3H0sUJgNMkaPnXLZNNPR_-xUozMdYurtpy6t2E,8757
3
+ ravmem/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ ravmem/cli/analyze.py,sha256=asBWizsgTLNEc3rV8zohSJn7Yh83vTX81qMF_MO_dQU,16322
5
+ ravmem/cli/auth.py,sha256=avYt_tgM6JtgAAl5LHTCbfFoa-8_4IYu6zAeAnyn5pw,21887
6
+ ravmem/cli/browse.py,sha256=9xYb0TBjUqmTMWKkd8a-MRCVi0TlgDak1Q2UrW2mlBA,4854
7
+ ravmem/cli/config_cmd.py,sha256=Cl-dm5KdfLOuGYL12j-SZGjGdDl0fdoPMVhvepzK4nI,2210
8
+ ravmem/cli/diff.py,sha256=m2Je-j9V1Yf4TSAvLUXBMB58Ff48IjD6Vm1qbI8NhDQ,3901
9
+ ravmem/cli/git_local.py,sha256=WodcZZeEgfnQX9w8rw70o6W56pkNJUHEgkrtjHvBBzM,4117
10
+ ravmem/cli/impact.py,sha256=JjTr_VQY9jGOFA1ZUZxXmn1mzv5ALmpNkuMhLw3VE4w,4021
11
+ ravmem/cli/info.py,sha256=PY1WCEJoKYa1UQ7u0ZdUdJb_KKmgI-st46D7M0Cwmuk,2492
12
+ ravmem/cli/kg.py,sha256=FnWEjCUq0Qw9TDRNk7zhvOrdl1nANNuh5LyUBoanwSU,30978
13
+ ravmem/cli/main.py,sha256=L5WFFptXvzssuc388Az_e8HGMGIeEWlrX6lSxH01_X0,2952
14
+ ravmem/cli/remote.py,sha256=l616Cs_txtGAP5udFR6JtZGo4tPpIf8ttNsrCh612ms,19622
15
+ ravmem/cli/search.py,sha256=ht56_YvblfXBibUYyRIJKynQnJQVJI28HEx6AdqbtbQ,3081
16
+ ravmem/cli/serve.py,sha256=HAsy3N4EVi7BPntg9220PowMLke65zgm7s31SjtT8bs,1503
17
+ ravmem/client/__init__.py,sha256=kqQNEjY_MMB4U7V7jeQVvwpyAH8My_indn47BERBgxk,71
18
+ ravmem/client/api.py,sha256=evBGJQOrmb6iBAHBrqYQVU1N637ltUPRj-aooXN-cAg,9417
19
+ ravmem/client/config.py,sha256=fvpB9CYxjsI-lM5gmuLtat73xqQFUEWzJsXRUt_fJ90,1405
20
+ ravmem_cli-0.1.0.dist-info/licenses/LICENSE,sha256=wI3Yj7pnbSHXvrmP3869k5Ft3ln5C83l-1w-RN0R14I,1063
21
+ ravmem_cli-0.1.0.dist-info/METADATA,sha256=5RUE_oivL9LW-P14cr4X82CjIhs9t0HSY0kFOfEmpWg,1696
22
+ ravmem_cli-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
+ ravmem_cli-0.1.0.dist-info/entry_points.txt,sha256=ldUYV83DRLplX6etlK-gvrs2MpBpWgk8KIGVMpNL354,47
24
+ ravmem_cli-0.1.0.dist-info/top_level.txt,sha256=nSWRo9Gm3vH4guHC29V3rbB9smoQm3zYxFhbrqLH8jo,7
25
+ ravmem_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ravmem = ravmem.cli.main:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RavMem
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 @@
1
+ ravmem