memorysync-cli 1.1.2__tar.gz → 1.1.4__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.4}/PKG-INFO +1 -1
- memorysync_cli-1.1.4/src/memorysync_cli/_version.py +1 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/commands/admin.py +2 -2
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/commands/init.py +18 -1
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/commands/memory.py +6 -3
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/http.py +149 -16
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/main.py +21 -18
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/output.py +27 -2
- memorysync_cli-1.1.2/src/memorysync_cli/_version.py +0 -1
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/.gitignore +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/LICENSE +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/README.md +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/pyproject.toml +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/__init__.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/__main__.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/args.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/commands/__init__.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/commands/source.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/commands/tooling.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/completions.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/config.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/credentials.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/errors.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/evaluation.py +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/registry.json +0 -0
- {memorysync_cli-1.1.2 → memorysync_cli-1.1.4}/src/memorysync_cli/registry.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.4"
|
|
@@ -24,7 +24,7 @@ from .. import credentials
|
|
|
24
24
|
from .._version import __version__
|
|
25
25
|
from ..errors import usage_error
|
|
26
26
|
from ..evaluation import is_evaluation_key
|
|
27
|
-
from ..output import render_table, style
|
|
27
|
+
from ..output import dumps, render_table, style
|
|
28
28
|
|
|
29
29
|
#: Keys ``config set`` / ``config unset`` accept. Identical to the Node CLI's
|
|
30
30
|
#: ``SETTABLE``; ``output`` was previously missing here, so `config set output table`
|
|
@@ -457,7 +457,7 @@ def config_command(ctx: dict) -> dict:
|
|
|
457
457
|
f"{style.dim('active profile ')} {data.get('current_profile')}",
|
|
458
458
|
f"{style.dim('key storage ')} {credentials.describe_storage()}",
|
|
459
459
|
"",
|
|
460
|
-
|
|
460
|
+
dumps(data.get("profiles", {})),
|
|
461
461
|
]
|
|
462
462
|
),
|
|
463
463
|
}
|
|
@@ -147,10 +147,27 @@ def _init_by_claiming(ctx: dict, profile_name: str) -> dict:
|
|
|
147
147
|
"Your existing key keeps working and no longer expires. "
|
|
148
148
|
"Everything stored during the evaluation is still there."
|
|
149
149
|
),
|
|
150
|
+
"",
|
|
151
|
+
# Owning the account and being able to open the dashboard are
|
|
152
|
+
# two different things, and this used to stop before saying
|
|
153
|
+
# so. Without ``--password`` there is no credential yet, and
|
|
154
|
+
# the only way in is a link labelled "Forgot password" —
|
|
155
|
+
# which nobody should have to guess at for a password they
|
|
156
|
+
# never had. The server says which case this is; print its
|
|
157
|
+
# sentence rather than deciding here, so the two CLIs and the
|
|
158
|
+
# API cannot drift on the instruction.
|
|
159
|
+
style.yellow(claimed["next_step"])
|
|
160
|
+
if claimed.get("next_step")
|
|
161
|
+
else style.dim(
|
|
162
|
+
"Sign in at https://app.memorysync.io/login with this address."
|
|
163
|
+
),
|
|
150
164
|
]
|
|
151
165
|
)
|
|
152
166
|
|
|
153
|
-
|
|
167
|
+
# The profile now records that the account is owned, so the reminder must
|
|
168
|
+
# be re-evaluated from disk rather than from the settings resolved before
|
|
169
|
+
# this ran.
|
|
170
|
+
return {"data": payload, "text": render_complete, "rescope": True}
|
|
154
171
|
|
|
155
172
|
# The value ``main`` already resolved from --api-key, the environment, then the
|
|
156
173
|
# stored profile. Resolving it again here would be a second precedence order
|
|
@@ -22,7 +22,7 @@ from typing import Any
|
|
|
22
22
|
|
|
23
23
|
from ..errors import quota_error, usage_error
|
|
24
24
|
from ..http import ApiClient
|
|
25
|
-
from ..output import render_table, style
|
|
25
|
+
from ..output import dumps, render_table, style
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def _require_user(ctx: dict) -> str:
|
|
@@ -608,12 +608,15 @@ def export_memories(ctx: dict) -> dict:
|
|
|
608
608
|
|
|
609
609
|
|
|
610
610
|
def _serialize_export(memories: list[dict], fmt: str) -> str:
|
|
611
|
+
# Through the shared helper: an export is a file a customer keeps, and
|
|
612
|
+
# escaping every accent to ``\\uXXXX`` made this CLI's export a different
|
|
613
|
+
# document from the Node CLI's for the same memories.
|
|
611
614
|
if fmt == "json":
|
|
612
|
-
return
|
|
615
|
+
return dumps(memories) + "\n"
|
|
613
616
|
if fmt == "jsonl":
|
|
614
617
|
if not memories:
|
|
615
618
|
return ""
|
|
616
|
-
return "\n".join(
|
|
619
|
+
return "\n".join(dumps(item, indent=None) for item in memories) + "\n"
|
|
617
620
|
|
|
618
621
|
# CSV, quoted by hand rather than through the csv module so the output is
|
|
619
622
|
# identical to the Node CLI's, which also writes it directly.
|
|
@@ -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
|
|
@@ -24,6 +24,7 @@ from .errors import CliError, Exit, auth_error, usage_error
|
|
|
24
24
|
from .evaluation import claim_reminder
|
|
25
25
|
from .http import ApiClient
|
|
26
26
|
from .output import (
|
|
27
|
+
dumps,
|
|
27
28
|
emit,
|
|
28
29
|
error_envelope,
|
|
29
30
|
set_colour_enabled,
|
|
@@ -163,7 +164,7 @@ def _fail(*, command: str, error: CliError, agent_mode: bool, started: float, se
|
|
|
163
164
|
duration_ms = int((time.monotonic() - started) * 1000)
|
|
164
165
|
if agent_mode:
|
|
165
166
|
write(
|
|
166
|
-
|
|
167
|
+
dumps(
|
|
167
168
|
error_envelope(
|
|
168
169
|
command=command,
|
|
169
170
|
error=error,
|
|
@@ -277,37 +278,39 @@ def dispatch(argv: list[str]) -> int:
|
|
|
277
278
|
result = handler(ctx)
|
|
278
279
|
duration_ms = int((time.monotonic() - started) * 1000)
|
|
279
280
|
|
|
281
|
+
# A command that rewrote the profile has invalidated the settings resolved
|
|
282
|
+
# before it ran, so re-read them once and use them for everything below.
|
|
283
|
+
#
|
|
284
|
+
# Two things depend on this. `init --agent` stores a new default end-user
|
|
285
|
+
# id, and reporting the previous one in `scope` told an agent to keep
|
|
286
|
+
# writing under the identity it had just replaced. And
|
|
287
|
+
# `init --email ... --code` records that the account is now owned, without
|
|
288
|
+
# which the CLI printed "This is an unclaimed evaluation key and expires"
|
|
289
|
+
# immediately after a successful claim — nagging about the exact thing the
|
|
290
|
+
# user had just done.
|
|
291
|
+
effective = _resolve_settings(flags) if result.get("rescope") else settings
|
|
292
|
+
|
|
280
293
|
# `help` is a discovery contract, not a data result, so its JSON is the
|
|
281
294
|
# tree itself rather than an envelope around it. An agent bootstrapping
|
|
282
295
|
# from `help --json` should not have to reach into `.data[0]` for the
|
|
283
296
|
# command list.
|
|
284
297
|
if command_name == "help" and (agent_mode or fmt == "json"):
|
|
285
|
-
write(
|
|
286
|
-
_note_unclaimed_key(api_key,
|
|
298
|
+
write(dumps(result["data"]))
|
|
299
|
+
_note_unclaimed_key(api_key, effective)
|
|
287
300
|
return Exit.OK
|
|
288
301
|
|
|
289
302
|
if agent_mode:
|
|
290
303
|
write(
|
|
291
|
-
|
|
304
|
+
dumps(
|
|
292
305
|
success_envelope(
|
|
293
306
|
command=command_name,
|
|
294
307
|
data=result["data"],
|
|
295
308
|
duration_ms=duration_ms,
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
# case that matters: it mints an account and stores a new
|
|
299
|
-
# default end-user id, and reporting the previous one told
|
|
300
|
-
# an agent to keep writing under the identity it just
|
|
301
|
-
# replaced. Memories then split across two ids, which is the
|
|
302
|
-
# one failure this whole flow exists to avoid.
|
|
303
|
-
scope=_scope_of(
|
|
304
|
-
_resolve_settings(flags) if result.get("rescope") else settings
|
|
305
|
-
),
|
|
306
|
-
),
|
|
307
|
-
indent=2,
|
|
309
|
+
scope=_scope_of(effective),
|
|
310
|
+
)
|
|
308
311
|
)
|
|
309
312
|
)
|
|
310
|
-
_note_unclaimed_key(api_key,
|
|
313
|
+
_note_unclaimed_key(api_key, effective)
|
|
311
314
|
return result.get("exit_code", Exit.OK)
|
|
312
315
|
|
|
313
316
|
# `completion` and `mcp` emit text that must not be decorated or reshaped.
|
|
@@ -321,7 +324,7 @@ def dispatch(argv: list[str]) -> int:
|
|
|
321
324
|
return result.get("exit_code", Exit.OK)
|
|
322
325
|
|
|
323
326
|
emit(fmt=fmt, data=result["data"], text=result["text"])
|
|
324
|
-
_note_unclaimed_key(api_key,
|
|
327
|
+
_note_unclaimed_key(api_key, effective)
|
|
325
328
|
return result.get("exit_code", Exit.OK)
|
|
326
329
|
|
|
327
330
|
except CliError as error:
|
|
@@ -204,7 +204,9 @@ def _yaml_scalar(value: Any) -> str:
|
|
|
204
204
|
# Sexagesimal: `12:30` is a number to a 1.1 parser.
|
|
205
205
|
or _SEXAGESIMAL.match(text) is not None
|
|
206
206
|
)
|
|
207
|
-
|
|
207
|
+
# Quoted through the shared helper so a value needing quotes in YAML is not
|
|
208
|
+
# also ASCII-escaped, which the Node CLI does not do.
|
|
209
|
+
return dumps(text, indent=None) if ambiguous else text
|
|
208
210
|
|
|
209
211
|
|
|
210
212
|
def to_yaml(value: Any, indent: int = 0) -> str:
|
|
@@ -316,13 +318,36 @@ def write(text: str) -> None:
|
|
|
316
318
|
sys.stdout.write(f"{text}\n")
|
|
317
319
|
|
|
318
320
|
|
|
321
|
+
def dumps(data: Any, *, indent: int | None = 2) -> str:
|
|
322
|
+
"""JSON exactly as the Node CLI's ``JSON.stringify`` writes it.
|
|
323
|
+
|
|
324
|
+
``ensure_ascii=False`` is the whole point. Python escapes non-ASCII by
|
|
325
|
+
default, so an em-dash came out as ``\\u2014`` here and as the character
|
|
326
|
+
itself there — the same document in different bytes, from two CLIs published
|
|
327
|
+
as interchangeable. It showed up on a claim response whose sentence contains
|
|
328
|
+
one, and it would have applied to every ``--json`` payload carrying any
|
|
329
|
+
non-ASCII text, which for a memory store is most of them: any customer
|
|
330
|
+
memory containing an accent, a dash or an emoji.
|
|
331
|
+
|
|
332
|
+
Separators are pinned too, because Python's defaults leave a trailing space
|
|
333
|
+
after ``,`` when no indent is given and JavaScript does not.
|
|
334
|
+
|
|
335
|
+
Every user-facing JSON write goes through here so the two cannot drift again.
|
|
336
|
+
Request bodies deliberately do not: those are read by our own API, which
|
|
337
|
+
decodes both spellings to the same string, and no user ever sees them.
|
|
338
|
+
"""
|
|
339
|
+
if indent is None:
|
|
340
|
+
return json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
|
341
|
+
return json.dumps(data, indent=indent, ensure_ascii=False)
|
|
342
|
+
|
|
343
|
+
|
|
319
344
|
def emit(*, fmt: str, data: Any, text: Callable[[], str]) -> None:
|
|
320
345
|
"""Render one result.
|
|
321
346
|
|
|
322
347
|
``text`` stays lazy so a JSON caller never pays to build a table it discards.
|
|
323
348
|
"""
|
|
324
349
|
if fmt == "json":
|
|
325
|
-
write(
|
|
350
|
+
write(dumps(data))
|
|
326
351
|
return
|
|
327
352
|
if fmt == "yaml":
|
|
328
353
|
write(to_yaml(data))
|
|
@@ -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
|