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.
- {ravmem_cli-1.3.0/ravmem_cli.egg-info → ravmem_cli-1.3.1}/PKG-INFO +1 -1
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/pyproject.toml +1 -1
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/_common.py +2 -4
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/git_local.py +6 -13
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/client/api.py +38 -8
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1/ravmem_cli.egg-info}/PKG-INFO +1 -1
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/SOURCES.txt +1 -0
- ravmem_cli-1.3.1/tests/test_client_delete_timeout.py +28 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/LICENSE +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/README.md +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/__init__.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/_errors.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/analyze.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/audit.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/auth.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/browse.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/config_cmd.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/diff.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/doctor.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/impact.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/info.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/kg.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/license.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/main.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/remote.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/search.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/cli/serve.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/client/__init__.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/client/config.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/config.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/config_loader.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/kg_config.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem/kg_init.py +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/dependency_links.txt +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/entry_points.txt +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/requires.txt +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/ravmem_cli.egg-info/top_level.txt +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/setup.cfg +0 -0
- {ravmem_cli-1.3.0 → ravmem_cli-1.3.1}/tests/test_smoke.py +0 -0
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
if
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
|
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
|
|
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
|