ravmem-cli 1.3.0__tar.gz → 1.3.1__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 (39) hide show
  1. {ravmem_cli-1.3.0/ravmem_cli.egg-info → ravmem_cli-1.3.1}/PKG-INFO +1 -1
  2. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/pyproject.toml +1 -1
  3. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/_common.py +2 -4
  4. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/git_local.py +6 -13
  5. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/client/api.py +38 -8
  6. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1/ravmem_cli.egg-info}/PKG-INFO +1 -1
  7. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/SOURCES.txt +1 -0
  8. ravmem_cli-1.3.1/tests/test_client_delete_timeout.py +28 -0
  9. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/LICENSE +0 -0
  10. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/README.md +0 -0
  11. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/__init__.py +0 -0
  12. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/_errors.py +0 -0
  13. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/analyze.py +0 -0
  14. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/audit.py +0 -0
  15. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/auth.py +0 -0
  16. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/browse.py +0 -0
  17. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/config_cmd.py +0 -0
  18. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/diff.py +0 -0
  19. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/doctor.py +0 -0
  20. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/impact.py +0 -0
  21. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/info.py +0 -0
  22. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/kg.py +0 -0
  23. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/license.py +0 -0
  24. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/main.py +0 -0
  25. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/remote.py +0 -0
  26. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/search.py +0 -0
  27. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/serve.py +0 -0
  28. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/client/__init__.py +0 -0
  29. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/client/config.py +0 -0
  30. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/config.py +0 -0
  31. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/config_loader.py +0 -0
  32. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/kg_config.py +0 -0
  33. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/kg_init.py +0 -0
  34. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/dependency_links.txt +0 -0
  35. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/entry_points.txt +0 -0
  36. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/requires.txt +0 -0
  37. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/top_level.txt +0 -0
  38. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/setup.cfg +0 -0
  39. {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/tests/test_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ravmem-cli
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: RavMem CLI — lightweight client for the RavMem code knowledge graph server
5
5
  Author-email: RavMem <harpreet.singh@ravmem.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ravmem-cli"
7
- version = "1.3.0"
7
+ version = "1.3.1"
8
8
  description = "RavMem CLI — lightweight client for the RavMem code knowledge graph server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -73,7 +73,7 @@ def wait_for_indexing(client, project_id: str, branch_name: str, job_id: int, wa
73
73
 
74
74
 
75
75
  def _poll_job_with_progress(
76
- client, job_id: int, label: str, interval: float = 1.0, timeout: float = 600.0,
76
+ client, job_id: int, label: str, interval: float = 1.0,
77
77
  ) -> dict:
78
78
  """Poll a job to completion, rendering a live progress bar (phase + %)
79
79
  instead of ``client.poll_job``'s silent loop — which, combined with the
@@ -92,7 +92,6 @@ def _poll_job_with_progress(
92
92
  httpx_logger = logging.getLogger("httpx")
93
93
  prev_level = httpx_logger.level
94
94
  httpx_logger.setLevel(logging.WARNING)
95
- deadline = time.monotonic() + timeout
96
95
  try:
97
96
  with Progress(
98
97
  TextColumn("[bold]{task.description}"),
@@ -112,8 +111,7 @@ def _poll_job_with_progress(
112
111
  if status in ("done", "failed", "cancelled"):
113
112
  progress.update(task, completed=100 if status == "done" else pct)
114
113
  return job
115
- if time.monotonic() >= deadline:
116
- raise TimeoutError(f"Job {job_id} did not complete within {timeout}s")
114
+ # No deadline: --wait means until the job ends, and on a large repo embedding alone takes many minutes.
117
115
  time.sleep(interval)
118
116
  finally:
119
117
  httpx_logger.setLevel(prev_level)
@@ -56,23 +56,16 @@ def diff_name_status(
56
56
 
57
57
  Paths are relative to *repo_path*.
58
58
  """
59
+ # NUL separation keeps tabs, spaces and non-ASCII names intact, and --no-renames reports a rename as a delete
60
+ # plus an add, so the old path is removed and the new one uploaded.
59
61
  out = _run(
60
- ["git", "diff", "--name-status", base_commit, target], cwd=repo_path,
62
+ ["git", "diff", "--name-status", "-z", "--no-renames", base_commit, target], cwd=repo_path,
61
63
  )
62
64
  modified: list[str] = []
63
65
  deleted: list[str] = []
64
- for line in out.splitlines():
65
- line = line.strip()
66
- if not line:
67
- continue
68
- parts = line.split("\t", 1)
69
- if len(parts) != 2:
70
- continue
71
- status, path = parts[0].strip(), parts[1].strip()
72
- if status.startswith("D"):
73
- deleted.append(path)
74
- else:
75
- modified.append(path)
66
+ fields = out.split("\0")
67
+ for status, path in zip(fields[::2], fields[1::2]):
68
+ (deleted if status.startswith("D") else modified).append(path)
76
69
  return modified, deleted
77
70
 
78
71
 
@@ -52,6 +52,21 @@ def handle_api_error(exc: Exception) -> None:
52
52
  raise typer.Exit(1)
53
53
 
54
54
 
55
+ _MAX_ATTEMPTS = 8
56
+ _MAX_RETRY_WAIT_SECS = 120
57
+ # A delete waits up to 2 minutes for running jobs to stop, then purges the graph in chunks, which takes minutes on
58
+ # a large project. The normal 30 s timeout would report a failure for a delete that goes on to succeed.
59
+ _DELETE_TIMEOUT_SECS = 1800.0
60
+
61
+
62
+ def _retry_after_secs(resp: httpx.Response, attempt: int) -> float:
63
+ try:
64
+ wait = float(resp.headers.get("Retry-After", ""))
65
+ except ValueError:
66
+ wait = 2 ** attempt
67
+ return min(max(wait, 1.0), _MAX_RETRY_WAIT_SECS)
68
+
69
+
55
70
  class RavMemClient:
56
71
  """Thin wrapper around the RavMem REST API."""
57
72
 
@@ -100,11 +115,25 @@ class RavMemClient:
100
115
  # Internal helper
101
116
  # ------------------------------------------------------------------
102
117
 
103
- def _request(self, method: str, path: str, **kwargs) -> dict | list:
104
- try:
105
- resp = self._client.request(method, path, **kwargs)
106
- except (httpx.ConnectError, httpx.TimeoutException):
107
- raise
118
+ def _request(self, method: str, path: str, *, retry_timeouts: bool = False, **kwargs) -> dict | list:
119
+ """Send a request, waiting out any 429 for as long as the server's Retry-After says.
120
+
121
+ ``retry_timeouts`` is only for calls the server treats as idempotent: a timed-out request may still have
122
+ been processed, so resending anything else could apply it twice.
123
+ """
124
+ attempt = 0
125
+ while True:
126
+ attempt += 1
127
+ try:
128
+ resp = self._client.request(method, path, **kwargs)
129
+ except httpx.TimeoutException:
130
+ if not retry_timeouts or attempt >= _MAX_ATTEMPTS:
131
+ raise
132
+ time.sleep(min(2 ** attempt, _MAX_RETRY_WAIT_SECS))
133
+ continue
134
+ if resp.status_code != 429 or attempt >= _MAX_ATTEMPTS:
135
+ break
136
+ time.sleep(_retry_after_secs(resp, attempt))
108
137
 
109
138
  try:
110
139
  resp.raise_for_status()
@@ -163,7 +192,7 @@ class RavMemClient:
163
192
  return self._request("GET", f"/api/projects/{project_id}")
164
193
 
165
194
  def delete_project(self, project_id: str) -> dict:
166
- return self._request("DELETE", f"/api/projects/{project_id}")
195
+ return self._request("DELETE", f"/api/projects/{project_id}", timeout=_DELETE_TIMEOUT_SECS)
167
196
 
168
197
  def update_project(self, project_id: str, default_branch: str) -> dict:
169
198
  return self._request(
@@ -233,7 +262,8 @@ class RavMemClient:
233
262
  body["parent_branch"] = parent_branch
234
263
  if langs is not None:
235
264
  body["langs"] = langs
236
- return self._request("POST", f"/api/projects/{project_id}/index", json=body)
265
+ # The server ignores a batch it already has, so a timed-out batch is safe to send again.
266
+ return self._request("POST", f"/api/projects/{project_id}/index", json=body, retry_timeouts=True)
237
267
 
238
268
  def index_branch(
239
269
  self,
@@ -264,7 +294,7 @@ class RavMemClient:
264
294
 
265
295
  def delete_branch_index(self, project_id: str, branch: str) -> dict:
266
296
  return self._request(
267
- "DELETE", f"/api/projects/{project_id}/branches/{branch}/index"
297
+ "DELETE", f"/api/projects/{project_id}/branches/{branch}/index", timeout=_DELETE_TIMEOUT_SECS,
268
298
  )
269
299
 
270
300
  def get_divergence(self, project_id: str, branch: str) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ravmem-cli
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: RavMem CLI — lightweight client for the RavMem code knowledge graph server
5
5
  Author-email: RavMem <harpreet.singh@ravmem.com>
6
6
  License-Expression: MIT
@@ -33,4 +33,5 @@ ravmem_cli.egg-info/dependency_links.txt
33
33
  ravmem_cli.egg-info/entry_points.txt
34
34
  ravmem_cli.egg-info/requires.txt
35
35
  ravmem_cli.egg-info/top_level.txt
36
+ tests/test_client_delete_timeout.py
36
37
  tests/test_smoke.py
@@ -0,0 +1,28 @@
1
+ import httpx
2
+
3
+ from ravmem.client import api
4
+
5
+
6
+ def _client_seeing(seen):
7
+ def handler(request: httpx.Request) -> httpx.Response:
8
+ seen.append(request.extensions["timeout"])
9
+ return httpx.Response(200, json={"deleted": True})
10
+
11
+ client = api.RavMemClient("http://server")
12
+ client._client = httpx.Client(base_url="http://server", transport=httpx.MockTransport(handler), timeout=30.0)
13
+ return client
14
+
15
+
16
+ def test_deletes_wait_longer_than_the_default_timeout():
17
+ seen = []
18
+ client = _client_seeing(seen)
19
+ client.delete_project("p")
20
+ client.delete_branch_index("p", "main")
21
+ assert [t["read"] for t in seen] == [api._DELETE_TIMEOUT_SECS] * 2
22
+
23
+
24
+ def test_other_calls_keep_the_default_timeout():
25
+ seen = []
26
+ client = _client_seeing(seen)
27
+ client._request("GET", "/api/projects")
28
+ assert seen[0]["read"] == 30.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes