blog-cli 0.4.2__tar.gz → 0.4.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.
- {blog_cli-0.4.2 → blog_cli-0.4.4}/PKG-INFO +1 -1
- {blog_cli-0.4.2 → blog_cli-0.4.4}/pyproject.toml +1 -1
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/blog_cli.egg-info/PKG-INFO +1 -1
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/cli.py +79 -42
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/formatting.py +14 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/tests/test_cli.py +61 -13
- {blog_cli-0.4.2 → blog_cli-0.4.4}/README.md +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/setup.cfg +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/blog_cli.egg-info/SOURCES.txt +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/blog_cli.egg-info/dependency_links.txt +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/blog_cli.egg-info/entry_points.txt +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/blog_cli.egg-info/requires.txt +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/blog_cli.egg-info/top_level.txt +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/__init__.py +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/__main__.py +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/client.py +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/credentials.py +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/docker_keyring.py +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/src/personal_cli/setup_server.py +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/tests/test_client.py +0 -0
- {blog_cli-0.4.2 → blog_cli-0.4.4}/tests/test_setup_server.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "blog-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.4"
|
|
8
8
|
description = "Agent-friendly CLI for managing a personal site. Articles, projects, services, media, and a bundled ChatGPT/Codex skill."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -13,7 +13,7 @@ from personal_cli.credentials import (
|
|
|
13
13
|
CredentialStore,
|
|
14
14
|
MissingCredentialError,
|
|
15
15
|
)
|
|
16
|
-
from personal_cli.formatting import emit_result, read_markdown_from_source
|
|
16
|
+
from personal_cli.formatting import emit_error, emit_result, read_markdown_from_source
|
|
17
17
|
from personal_cli.setup_server import run_setup
|
|
18
18
|
|
|
19
19
|
app = typer.Typer(help="Agent-facing article CLI.")
|
|
@@ -73,7 +73,7 @@ def _with_article_url(article: dict) -> dict:
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
def _service_url(service: dict) -> str:
|
|
76
|
-
return f"{_site_url()}/services
|
|
76
|
+
return f"{_site_url()}/services?service={service['slug']}"
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def _with_service_url(service: dict) -> dict:
|
|
@@ -127,26 +127,43 @@ def run(coro):
|
|
|
127
127
|
return asyncio.run(coro)
|
|
128
128
|
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
_SETUP_COMMAND = "blog-cli keys setup"
|
|
131
|
+
_AUTH_MESSAGE = (
|
|
132
|
+
"Authentication required. Run `blog-cli keys setup` in an attached process. "
|
|
133
|
+
"Keep it running while the user enters and validates credentials in the browser, "
|
|
134
|
+
"then rerun the original command."
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _run(operation: Callable[[], Result], *, json_output: bool = False) -> Result:
|
|
139
|
+
"""Run an operation and return machine-readable auth failures."""
|
|
140
|
+
try:
|
|
141
|
+
return operation()
|
|
142
|
+
except MissingCredentialError as exc:
|
|
143
|
+
emit_error(
|
|
144
|
+
"authentication_required",
|
|
145
|
+
_AUTH_MESSAGE,
|
|
146
|
+
json_output=True,
|
|
147
|
+
setup_command=_SETUP_COMMAND,
|
|
148
|
+
)
|
|
149
|
+
raise typer.Exit(code=2) from exc
|
|
150
|
+
except CredentialError as exc:
|
|
151
|
+
emit_error(
|
|
152
|
+
"credential_store_unavailable",
|
|
153
|
+
str(exc),
|
|
154
|
+
json_output=True,
|
|
155
|
+
)
|
|
156
|
+
raise typer.Exit(code=2) from exc
|
|
157
|
+
except CLIError as exc:
|
|
158
|
+
if exc.status_code not in (401, 403):
|
|
159
|
+
raise
|
|
160
|
+
emit_error(
|
|
161
|
+
"authentication_required",
|
|
162
|
+
"The stored API key was rejected. Run `blog-cli keys setup`, keep it attached until setup succeeds, then rerun the original command.",
|
|
163
|
+
json_output=True,
|
|
164
|
+
setup_command=_SETUP_COMMAND,
|
|
165
|
+
)
|
|
166
|
+
raise typer.Exit(code=2) from exc
|
|
150
167
|
|
|
151
168
|
|
|
152
169
|
@article_app.command("list")
|
|
@@ -165,7 +182,7 @@ def article_list(
|
|
|
165
182
|
emit_result(articles, json_output=json_output)
|
|
166
183
|
|
|
167
184
|
try:
|
|
168
|
-
_run(_op)
|
|
185
|
+
_run(_op, json_output=json_output)
|
|
169
186
|
except CLIError as exc:
|
|
170
187
|
typer.echo(str(exc), err=True)
|
|
171
188
|
raise typer.Exit(code=1) from exc
|
|
@@ -185,7 +202,7 @@ def article_show(
|
|
|
185
202
|
emit_result(article, json_output=json_output)
|
|
186
203
|
|
|
187
204
|
try:
|
|
188
|
-
_run(_op)
|
|
205
|
+
_run(_op, json_output=json_output)
|
|
189
206
|
except CLIError as exc:
|
|
190
207
|
typer.echo(str(exc), err=True)
|
|
191
208
|
raise typer.Exit(code=1) from exc
|
|
@@ -224,7 +241,7 @@ def blog_create(
|
|
|
224
241
|
emit_result(article, json_output=json_output)
|
|
225
242
|
|
|
226
243
|
try:
|
|
227
|
-
_run(_op)
|
|
244
|
+
_run(_op, json_output=json_output)
|
|
228
245
|
except CLIError as exc:
|
|
229
246
|
typer.echo(str(exc), err=True)
|
|
230
247
|
raise typer.Exit(code=1) from exc
|
|
@@ -266,7 +283,7 @@ def project_create(
|
|
|
266
283
|
emit_result(article, json_output=json_output)
|
|
267
284
|
|
|
268
285
|
try:
|
|
269
|
-
_run(_op)
|
|
286
|
+
_run(_op, json_output=json_output)
|
|
270
287
|
except CLIError as exc:
|
|
271
288
|
typer.echo(str(exc), err=True)
|
|
272
289
|
raise typer.Exit(code=1) from exc
|
|
@@ -330,7 +347,7 @@ def article_update(
|
|
|
330
347
|
emit_result(article, json_output=json_output)
|
|
331
348
|
|
|
332
349
|
try:
|
|
333
|
-
_run(_op)
|
|
350
|
+
_run(_op, json_output=json_output)
|
|
334
351
|
except CLIError as exc:
|
|
335
352
|
typer.echo(str(exc), err=True)
|
|
336
353
|
raise typer.Exit(code=1) from exc
|
|
@@ -351,7 +368,7 @@ def article_publish(
|
|
|
351
368
|
emit_result(article, json_output=json_output)
|
|
352
369
|
|
|
353
370
|
try:
|
|
354
|
-
_run(_op)
|
|
371
|
+
_run(_op, json_output=json_output)
|
|
355
372
|
except CLIError as exc:
|
|
356
373
|
typer.echo(str(exc), err=True)
|
|
357
374
|
raise typer.Exit(code=1) from exc
|
|
@@ -370,7 +387,7 @@ def article_delete(
|
|
|
370
387
|
emit_result(result, json_output=json_output)
|
|
371
388
|
|
|
372
389
|
try:
|
|
373
|
-
_run(_op)
|
|
390
|
+
_run(_op, json_output=json_output)
|
|
374
391
|
except CLIError as exc:
|
|
375
392
|
typer.echo(str(exc), err=True)
|
|
376
393
|
raise typer.Exit(code=1) from exc
|
|
@@ -390,7 +407,7 @@ def article_unarchive(
|
|
|
390
407
|
emit_result(article, json_output=json_output)
|
|
391
408
|
|
|
392
409
|
try:
|
|
393
|
-
_run(_op)
|
|
410
|
+
_run(_op, json_output=json_output)
|
|
394
411
|
except CLIError as exc:
|
|
395
412
|
typer.echo(str(exc), err=True)
|
|
396
413
|
raise typer.Exit(code=1) from exc
|
|
@@ -413,7 +430,7 @@ def article_preview(
|
|
|
413
430
|
emit_result(result, json_output=json_output)
|
|
414
431
|
|
|
415
432
|
try:
|
|
416
|
-
_run(_op)
|
|
433
|
+
_run(_op, json_output=json_output)
|
|
417
434
|
except CLIError as exc:
|
|
418
435
|
typer.echo(str(exc), err=True)
|
|
419
436
|
raise typer.Exit(code=1) from exc
|
|
@@ -433,7 +450,7 @@ def tag_list(
|
|
|
433
450
|
emit_result(result, json_output=json_output)
|
|
434
451
|
|
|
435
452
|
try:
|
|
436
|
-
_run(_op)
|
|
453
|
+
_run(_op, json_output=json_output)
|
|
437
454
|
except CLIError as exc:
|
|
438
455
|
typer.echo(str(exc), err=True)
|
|
439
456
|
raise typer.Exit(code=1) from exc
|
|
@@ -453,7 +470,7 @@ def tag_add(
|
|
|
453
470
|
emit_result(result, json_output=json_output)
|
|
454
471
|
|
|
455
472
|
try:
|
|
456
|
-
_run(_op)
|
|
473
|
+
_run(_op, json_output=json_output)
|
|
457
474
|
except CLIError as exc:
|
|
458
475
|
typer.echo(str(exc), err=True)
|
|
459
476
|
raise typer.Exit(code=1) from exc
|
|
@@ -475,7 +492,7 @@ def tag_remove(
|
|
|
475
492
|
emit_result(result, json_output=json_output)
|
|
476
493
|
|
|
477
494
|
try:
|
|
478
|
-
_run(_op)
|
|
495
|
+
_run(_op, json_output=json_output)
|
|
479
496
|
except CLIError as exc:
|
|
480
497
|
typer.echo(str(exc), err=True)
|
|
481
498
|
raise typer.Exit(code=1) from exc
|
|
@@ -495,7 +512,7 @@ def media_upload(
|
|
|
495
512
|
emit_result(result, json_output=json_output)
|
|
496
513
|
|
|
497
514
|
try:
|
|
498
|
-
_run(_op)
|
|
515
|
+
_run(_op, json_output=json_output)
|
|
499
516
|
except CLIError as exc:
|
|
500
517
|
typer.echo(str(exc), err=True)
|
|
501
518
|
raise typer.Exit(code=1) from exc
|
|
@@ -515,7 +532,7 @@ def media_update(
|
|
|
515
532
|
emit_result(result, json_output=json_output)
|
|
516
533
|
|
|
517
534
|
try:
|
|
518
|
-
_run(_op)
|
|
535
|
+
_run(_op, json_output=json_output)
|
|
519
536
|
except CLIError as exc:
|
|
520
537
|
typer.echo(str(exc), err=True)
|
|
521
538
|
raise typer.Exit(code=1) from exc
|
|
@@ -534,12 +551,32 @@ def media_delete(
|
|
|
534
551
|
emit_result(result, json_output=json_output)
|
|
535
552
|
|
|
536
553
|
try:
|
|
537
|
-
_run(_op)
|
|
554
|
+
_run(_op, json_output=json_output)
|
|
538
555
|
except CLIError as exc:
|
|
539
556
|
typer.echo(str(exc), err=True)
|
|
540
557
|
raise typer.Exit(code=1) from exc
|
|
541
558
|
|
|
542
559
|
|
|
560
|
+
@keys_app.command("setup")
|
|
561
|
+
def keys_setup(
|
|
562
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON output after setup succeeds."),
|
|
563
|
+
) -> None:
|
|
564
|
+
"""Start the browser-based credential setup server and wait for valid credentials."""
|
|
565
|
+
try:
|
|
566
|
+
run_setup(
|
|
567
|
+
CredentialStore(),
|
|
568
|
+
output=_emit,
|
|
569
|
+
prompt="Open this setup URL in your browser",
|
|
570
|
+
)
|
|
571
|
+
emit_result(
|
|
572
|
+
{"configured": True, "message": "Credentials saved."},
|
|
573
|
+
json_output=json_output,
|
|
574
|
+
)
|
|
575
|
+
except CredentialError as exc:
|
|
576
|
+
emit_error("credential_store_unavailable", str(exc), json_output=True)
|
|
577
|
+
raise typer.Exit(code=2) from exc
|
|
578
|
+
|
|
579
|
+
|
|
543
580
|
@keys_app.command("revoke")
|
|
544
581
|
def keys_revoke(
|
|
545
582
|
json_output: bool = typer.Option(False, "--json", help="Emit JSON output."),
|
|
@@ -623,7 +660,7 @@ def service_create(
|
|
|
623
660
|
emit_result(result, json_output=json_output)
|
|
624
661
|
|
|
625
662
|
try:
|
|
626
|
-
_run(_op)
|
|
663
|
+
_run(_op, json_output=json_output)
|
|
627
664
|
except CLIError as exc:
|
|
628
665
|
typer.echo(str(exc), err=True)
|
|
629
666
|
raise typer.Exit(code=1) from exc
|
|
@@ -644,7 +681,7 @@ def service_list(
|
|
|
644
681
|
emit_result(result, json_output=json_output)
|
|
645
682
|
|
|
646
683
|
try:
|
|
647
|
-
_run(_op)
|
|
684
|
+
_run(_op, json_output=json_output)
|
|
648
685
|
except CLIError as exc:
|
|
649
686
|
typer.echo(str(exc), err=True)
|
|
650
687
|
raise typer.Exit(code=1) from exc
|
|
@@ -663,7 +700,7 @@ def service_show(
|
|
|
663
700
|
emit_result(result, json_output=json_output)
|
|
664
701
|
|
|
665
702
|
try:
|
|
666
|
-
_run(_op)
|
|
703
|
+
_run(_op, json_output=json_output)
|
|
667
704
|
except CLIError as exc:
|
|
668
705
|
typer.echo(str(exc), err=True)
|
|
669
706
|
raise typer.Exit(code=1) from exc
|
|
@@ -731,7 +768,7 @@ def service_update(
|
|
|
731
768
|
emit_result(result, json_output=json_output)
|
|
732
769
|
|
|
733
770
|
try:
|
|
734
|
-
_run(_op)
|
|
771
|
+
_run(_op, json_output=json_output)
|
|
735
772
|
except CLIError as exc:
|
|
736
773
|
typer.echo(str(exc), err=True)
|
|
737
774
|
raise typer.Exit(code=1) from exc
|
|
@@ -749,7 +786,7 @@ def service_delete(
|
|
|
749
786
|
emit_result(run(client.delete_service(slug)), json_output=json_output)
|
|
750
787
|
|
|
751
788
|
try:
|
|
752
|
-
_run(_op)
|
|
789
|
+
_run(_op, json_output=json_output)
|
|
753
790
|
except CLIError as exc:
|
|
754
791
|
typer.echo(str(exc), err=True)
|
|
755
792
|
raise typer.Exit(code=1) from exc
|
|
@@ -36,3 +36,17 @@ def emit_result(data: Any, *, json_output: bool = False) -> None:
|
|
|
36
36
|
|
|
37
37
|
print(data)
|
|
38
38
|
|
|
39
|
+
|
|
40
|
+
def emit_error(
|
|
41
|
+
code: str,
|
|
42
|
+
message: str,
|
|
43
|
+
*,
|
|
44
|
+
json_output: bool = False,
|
|
45
|
+
**details: Any,
|
|
46
|
+
) -> None:
|
|
47
|
+
if json_output:
|
|
48
|
+
error: dict[str, Any] = {"code": code, "message": message}
|
|
49
|
+
error.update(details)
|
|
50
|
+
print(json.dumps({"error": error}, ensure_ascii=False, default=str), file=sys.stderr)
|
|
51
|
+
return
|
|
52
|
+
print(message, file=sys.stderr)
|
|
@@ -345,7 +345,7 @@ def test_service_cli_lifecycle(monkeypatch, runner: CliRunner, client: FakeApiCl
|
|
|
345
345
|
assert create_result.exit_code == 0
|
|
346
346
|
created = json.loads(create_result.stdout)
|
|
347
347
|
assert created["slug"] == "web"
|
|
348
|
-
assert created["url"] == "http://testsite/services
|
|
348
|
+
assert created["url"] == "http://testsite/services?service=web"
|
|
349
349
|
assert created["types"] == ["Static websites", "Web apps with a database"]
|
|
350
350
|
assert created["offerings"][0]["title"] == "Static websites"
|
|
351
351
|
|
|
@@ -355,7 +355,7 @@ def test_service_cli_lifecycle(monkeypatch, runner: CliRunner, client: FakeApiCl
|
|
|
355
355
|
|
|
356
356
|
list_result = runner.invoke(app, ["service", "list", "--category", "Web", "--json"])
|
|
357
357
|
assert list_result.exit_code == 0
|
|
358
|
-
assert json.loads(list_result.stdout)[0]["url"] == "http://testsite/services
|
|
358
|
+
assert json.loads(list_result.stdout)[0]["url"] == "http://testsite/services?service=web"
|
|
359
359
|
|
|
360
360
|
|
|
361
361
|
def test_service_cli_does_not_expose_homepage_options(runner: CliRunner) -> None:
|
|
@@ -395,13 +395,33 @@ def test_media_cli_smoke(monkeypatch, runner: CliRunner, client: FakeApiClient,
|
|
|
395
395
|
assert deleted["name"] == "hero-image"
|
|
396
396
|
|
|
397
397
|
|
|
398
|
-
def
|
|
398
|
+
def test_authentication_required_is_structured_and_does_not_start_setup(
|
|
399
399
|
monkeypatch: pytest.MonkeyPatch,
|
|
400
400
|
runner: CliRunner,
|
|
401
401
|
credential_store: FakeKeyringBackend,
|
|
402
402
|
) -> None:
|
|
403
403
|
credential_store.delete_password("personal-cli", "default")
|
|
404
404
|
|
|
405
|
+
setup_calls: list[int] = []
|
|
406
|
+
monkeypatch.setattr("personal_cli.cli.run_setup", lambda *args, **kwargs: setup_calls.append(1))
|
|
407
|
+
monkeypatch.setattr("personal_cli.cli.build_client", lambda *args, **kwargs: (_ for _ in ()).throw(MissingCredentialError("missing")))
|
|
408
|
+
|
|
409
|
+
for args in (["article", "list"], ["article", "list", "--json"]):
|
|
410
|
+
result = runner.invoke(app, args)
|
|
411
|
+
assert result.exit_code == 2
|
|
412
|
+
error = json.loads(result.stderr.strip())
|
|
413
|
+
assert error["error"]["code"] == "authentication_required"
|
|
414
|
+
assert error["error"]["setup_command"] == "blog-cli keys setup"
|
|
415
|
+
assert "http" not in result.stderr
|
|
416
|
+
|
|
417
|
+
assert not setup_calls
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_keys_setup_runs_attached_setup_server(
|
|
421
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
422
|
+
runner: CliRunner,
|
|
423
|
+
credential_store: FakeKeyringBackend,
|
|
424
|
+
) -> None:
|
|
405
425
|
setup_calls: list[int] = []
|
|
406
426
|
|
|
407
427
|
def _fake_run_setup(store, output=None, prompt=""):
|
|
@@ -415,17 +435,45 @@ def test_missing_credentials_triggers_setup(
|
|
|
415
435
|
|
|
416
436
|
monkeypatch.setattr("personal_cli.cli.run_setup", _fake_run_setup)
|
|
417
437
|
|
|
418
|
-
|
|
438
|
+
result = runner.invoke(app, ["keys", "setup", "--json"])
|
|
419
439
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return FakeApiClient()
|
|
440
|
+
assert result.exit_code == 0
|
|
441
|
+
assert setup_calls == [1]
|
|
442
|
+
assert "http://127.0.0.1:3233/setup?token=fake" in result.stderr
|
|
443
|
+
assert json.loads(result.stdout)["configured"] is True
|
|
425
444
|
|
|
426
|
-
monkeypatch.setattr("personal_cli.cli.build_client", _flaky_build_client)
|
|
427
|
-
monkeypatch.setattr("personal_cli.cli.run", lambda coro: __import__("asyncio").run(coro))
|
|
428
445
|
|
|
429
|
-
|
|
430
|
-
|
|
446
|
+
def test_keys_revoke_removes_stored_credentials(
|
|
447
|
+
runner: CliRunner,
|
|
448
|
+
credential_store: FakeKeyringBackend,
|
|
449
|
+
) -> None:
|
|
450
|
+
result = runner.invoke(app, ["keys", "revoke", "--json"])
|
|
451
|
+
|
|
431
452
|
assert result.exit_code == 0
|
|
453
|
+
assert json.loads(result.stdout) == {
|
|
454
|
+
"revoked": True,
|
|
455
|
+
"message": "Credentials revoked.",
|
|
456
|
+
}
|
|
457
|
+
assert credential_store.get_password("personal-cli", "default") is None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_json_credential_store_error_is_structured(
|
|
461
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
462
|
+
runner: CliRunner,
|
|
463
|
+
) -> None:
|
|
464
|
+
def _credential_store_failure(server_url=None, insecure=False):
|
|
465
|
+
raise CredentialError("Could not access the operating system credential store.")
|
|
466
|
+
|
|
467
|
+
monkeypatch.setattr("personal_cli.cli.build_client", _credential_store_failure)
|
|
468
|
+
|
|
469
|
+
result = runner.invoke(app, ["article", "list", "--json"])
|
|
470
|
+
|
|
471
|
+
assert result.exit_code == 2
|
|
472
|
+
error = json.loads(result.stderr.strip())
|
|
473
|
+
assert error == {
|
|
474
|
+
"error": {
|
|
475
|
+
"code": "credential_store_unavailable",
|
|
476
|
+
"message": "Could not access the operating system credential store.",
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
assert "Traceback" not in result.output
|
|
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
|