memorysync-cli 1.1.2__tar.gz → 1.1.3__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.
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/PKG-INFO +1 -1
- memorysync_cli-1.1.3/src/memorysync_cli/_version.py +1 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/http.py +149 -16
- memorysync_cli-1.1.2/src/memorysync_cli/_version.py +0 -1
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/.gitignore +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/LICENSE +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/README.md +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/pyproject.toml +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/__init__.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/__main__.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/args.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/commands/__init__.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/commands/admin.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/commands/init.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/commands/memory.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/commands/source.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/commands/tooling.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/completions.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/config.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/credentials.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/errors.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/evaluation.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/main.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/output.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/registry.json +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.3}/src/memorysync_cli/registry.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.3"
|
|
@@ -121,13 +121,25 @@ class ApiClient:
|
|
|
121
121
|
started = time.monotonic()
|
|
122
122
|
request = urllib.request.Request(url, data=payload, headers=headers, method=method)
|
|
123
123
|
|
|
124
|
+
# Headers are kept, not discarded: the API returns the request id in
|
|
125
|
+
# `x-request-id`, and it is the only handle support has on a failure. The
|
|
126
|
+
# Node CLI printed it and this one did not.
|
|
127
|
+
response_headers: Any = None
|
|
128
|
+
status_reason = ""
|
|
129
|
+
|
|
124
130
|
try:
|
|
125
131
|
with urllib.request.urlopen(request, timeout=limit_ms / 1000) as response:
|
|
126
132
|
text = response.read().decode("utf-8", errors="replace")
|
|
127
133
|
status = response.status
|
|
134
|
+
response_headers = response.headers
|
|
128
135
|
except urllib.error.HTTPError as error:
|
|
129
136
|
text = error.read().decode("utf-8", errors="replace")
|
|
130
137
|
status = error.code
|
|
138
|
+
response_headers = error.headers
|
|
139
|
+
# The status phrase, so a body with no usable message reads
|
|
140
|
+
# "400 Bad Request" as it does under the Node CLI, rather than this
|
|
141
|
+
# CLI's own wording.
|
|
142
|
+
status_reason = str(getattr(error, "reason", "") or "")
|
|
131
143
|
except TimeoutError:
|
|
132
144
|
raise network_error(
|
|
133
145
|
f"Request to {path} timed out after {limit_ms}ms.",
|
|
@@ -156,25 +168,129 @@ class ApiClient:
|
|
|
156
168
|
if 200 <= status < 300:
|
|
157
169
|
return parsed
|
|
158
170
|
|
|
159
|
-
raise self._to_error(status, parsed)
|
|
171
|
+
raise self._to_error(status, parsed, response_headers, status_reason)
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _request_id(payload: Any, headers: Any) -> str | None:
|
|
175
|
+
from_header = headers.get("x-request-id") if headers is not None else None
|
|
176
|
+
if from_header:
|
|
177
|
+
return from_header
|
|
178
|
+
if isinstance(payload, dict) and isinstance(payload.get("request_id"), str):
|
|
179
|
+
return payload["request_id"]
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def _from_oauth_error(
|
|
183
|
+
self, status: int, body: dict, request_id: str | None, headers: Any
|
|
184
|
+
) -> CliError:
|
|
185
|
+
"""An ``{error, error_description}`` body, as ``/evaluation/*`` returns.
|
|
186
|
+
|
|
187
|
+
The exit code comes from the code rather than the HTTP status, because the
|
|
188
|
+
status cannot separate these: a refused claim and an already-claimed
|
|
189
|
+
account are both 409, and only one of them is worth retrying with
|
|
190
|
+
different input. Mirrors ``#fromOAuthError`` in the Node CLI.
|
|
191
|
+
"""
|
|
192
|
+
code = body["error"]
|
|
193
|
+
message = body["error_description"]
|
|
194
|
+
|
|
195
|
+
if code == "rate_limited":
|
|
196
|
+
retry_after = headers.get("retry-after") if headers is not None else None
|
|
197
|
+
return CliError(
|
|
198
|
+
message,
|
|
199
|
+
exit_code=Exit.NETWORK,
|
|
200
|
+
code=code,
|
|
201
|
+
hint=(
|
|
202
|
+
f"Wait {retry_after} seconds and try again."
|
|
203
|
+
if retry_after
|
|
204
|
+
else "Wait and try again."
|
|
205
|
+
),
|
|
206
|
+
request_id=request_id,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Wrong input the caller can correct, or a state it cannot argue with.
|
|
210
|
+
# Both are exit 2: the fix is a different command, not a retry.
|
|
211
|
+
correctable = {
|
|
212
|
+
"invalid_email",
|
|
213
|
+
"email_already_registered",
|
|
214
|
+
"invalid_code",
|
|
215
|
+
"code_expired",
|
|
216
|
+
"too_many_attempts",
|
|
217
|
+
"not_an_evaluation_key",
|
|
218
|
+
"already_claimed",
|
|
219
|
+
"unclaimable",
|
|
220
|
+
}
|
|
221
|
+
if code in correctable:
|
|
222
|
+
return CliError(
|
|
223
|
+
message, exit_code=Exit.USAGE, code=code, request_id=request_id
|
|
224
|
+
)
|
|
160
225
|
|
|
161
|
-
|
|
226
|
+
if status in (401, 403):
|
|
227
|
+
return auth_error(message)
|
|
228
|
+
if status == 404:
|
|
229
|
+
return not_found_error(message)
|
|
230
|
+
return CliError(
|
|
231
|
+
message, exit_code=Exit.FAILURE, code=code, request_id=request_id
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def _to_error(
|
|
235
|
+
self,
|
|
236
|
+
status: int,
|
|
237
|
+
payload: Any,
|
|
238
|
+
headers: Any = None,
|
|
239
|
+
reason: str = "",
|
|
240
|
+
) -> CliError:
|
|
162
241
|
"""Map a failed response to the taxonomy.
|
|
163
242
|
|
|
243
|
+
Mirrors ``#toError`` in ``sdk/cli/src/http.mjs`` step for step. The
|
|
244
|
+
unwrapping below is the part that had drifted: this CLI treated ``detail``
|
|
245
|
+
itself as the carrier and never looked inside ``detail.error``, so the
|
|
246
|
+
nested shape the API uses on several routes lost both its sentence and its
|
|
247
|
+
code and fell through to a generic "Request failed with status 400."
|
|
248
|
+
|
|
164
249
|
The status alone is not enough for 429: a plan limit and a rate limit both
|
|
165
250
|
arrive as 429 but mean different things to a script. Only the first is
|
|
166
251
|
about the plan, and only the first is pointless to retry.
|
|
167
252
|
"""
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
253
|
+
request_id = self._request_id(payload, headers)
|
|
254
|
+
|
|
255
|
+
# Node: `payload?.detail ?? payload`
|
|
256
|
+
detail: Any = payload
|
|
257
|
+
if isinstance(payload, dict) and payload.get("detail") is not None:
|
|
258
|
+
detail = payload["detail"]
|
|
259
|
+
|
|
260
|
+
# `/evaluation/*` answers in the OAuth error shape. Without this branch the
|
|
261
|
+
# sentence in `error_description` was dropped entirely and this CLI
|
|
262
|
+
# reported "Request failed with status 409." for a refused claim, while the
|
|
263
|
+
# Node CLI reported the bare code `email_already_registered`. Neither told
|
|
264
|
+
# the user that the address already has an account.
|
|
265
|
+
if (
|
|
266
|
+
isinstance(detail, dict)
|
|
267
|
+
and isinstance(detail.get("error"), str)
|
|
268
|
+
and isinstance(detail.get("error_description"), str)
|
|
269
|
+
):
|
|
270
|
+
return self._from_oauth_error(status, detail, request_id, headers)
|
|
271
|
+
|
|
272
|
+
# Node: `detail?.error ?? detail`
|
|
273
|
+
inner: Any = detail
|
|
274
|
+
if isinstance(detail, dict) and detail.get("error") is not None:
|
|
275
|
+
inner = detail["error"]
|
|
276
|
+
|
|
277
|
+
message = inner if isinstance(inner, str) else None
|
|
278
|
+
if message is None and isinstance(inner, dict):
|
|
279
|
+
message = inner.get("message")
|
|
280
|
+
if not message and isinstance(payload, dict):
|
|
281
|
+
message = payload.get("message")
|
|
282
|
+
if not message:
|
|
283
|
+
message = f"{status} {reason}".strip() if reason else (
|
|
284
|
+
f"Request failed with status {status}."
|
|
285
|
+
)
|
|
177
286
|
|
|
287
|
+
code = inner.get("code") if isinstance(inner, dict) else None
|
|
288
|
+
|
|
289
|
+
# Every branch below matches the Node CLI's `#toError`, including exit
|
|
290
|
+
# codes. They had drifted: a 500 exited 5 here and 1 there, so a script
|
|
291
|
+
# retrying on "network" retried a server error under one CLI and gave up
|
|
292
|
+
# under the other. The fallback code was `api_error` here and
|
|
293
|
+
# `http_<status>` there, and the request id was dropped here entirely.
|
|
178
294
|
if status in (401, 403):
|
|
179
295
|
return auth_error(message)
|
|
180
296
|
if status == 404:
|
|
@@ -188,17 +304,34 @@ class ApiClient:
|
|
|
188
304
|
return CliError(
|
|
189
305
|
message,
|
|
190
306
|
exit_code=Exit.NETWORK,
|
|
191
|
-
code="rate_limited",
|
|
307
|
+
code=code or "rate_limited",
|
|
192
308
|
hint="Too many requests. Wait and retry.",
|
|
309
|
+
request_id=request_id,
|
|
310
|
+
)
|
|
311
|
+
if code == "MISSING_END_USER_ID":
|
|
312
|
+
return CliError(
|
|
313
|
+
"This command needs an end user.",
|
|
314
|
+
exit_code=Exit.USAGE,
|
|
315
|
+
code=code,
|
|
316
|
+
hint="Pass --user <id>, or set a default with `memorysync init --user <id>`.",
|
|
317
|
+
request_id=request_id,
|
|
193
318
|
)
|
|
194
319
|
if status >= 500:
|
|
195
320
|
return CliError(
|
|
196
|
-
message,
|
|
197
|
-
exit_code=Exit.
|
|
321
|
+
f"The API returned {status}: {message}",
|
|
322
|
+
exit_code=Exit.FAILURE,
|
|
198
323
|
code="server_error",
|
|
199
|
-
hint=
|
|
324
|
+
hint=(
|
|
325
|
+
f"Quote request id {request_id} to support." if request_id else None
|
|
326
|
+
),
|
|
327
|
+
request_id=request_id,
|
|
200
328
|
)
|
|
201
|
-
return CliError(
|
|
329
|
+
return CliError(
|
|
330
|
+
message,
|
|
331
|
+
exit_code=Exit.FAILURE,
|
|
332
|
+
code=code or f"http_{status}",
|
|
333
|
+
request_id=request_id,
|
|
334
|
+
)
|
|
202
335
|
|
|
203
336
|
# -----------------------------------------------------------------------
|
|
204
337
|
# Named endpoints, so commands never carry raw paths
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.2"
|
|
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
|