sanka-cli 0.2.2__tar.gz → 0.2.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.
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/PKG-INFO +1 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/pyproject.toml +1 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/cli/__init__.py +45 -3
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/cli/_output.py +18 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/store.py +31 -24
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/__init__.py +1 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_cli.py +68 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_extension_store.py +76 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_mcp.py +2 -2
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_sanka_facade.py +1 -1
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/.gitignore +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/LICENSES/AGPL-3.0-only.txt +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/LICENSES/Apache-2.0.txt +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/NOTICE +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/README.md +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/_client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/cli/_research.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/cli/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/connector/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/connector/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/__about__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/concurrency.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/connector_client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/connector_host.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/engine.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/continuous.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/errors.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/local.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/model.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/report_codec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/routes.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/scope.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/state.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/execution/validate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/discovery.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/lifecycle.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/model.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/extensions/runner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/hashing.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/errors.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/mapping_candidates.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/model.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/owner_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/pending_relationships.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/mapping/record_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/planner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/private_sqlite.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/registry.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/safe_local_io.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/spec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka/runtime/state.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/__main__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/bundle.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/ai.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/auth.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/cloud_migrate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/code.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/migrate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/profiles.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/resources.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/skill.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/commands/workflows.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/config.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/main.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/mcp/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/mcp/client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/mcp/render.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/mcp/server.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/output.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/runtime.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/skills/sanka-cli/SKILL.md +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_cli/state.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/__about__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/credentials.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/errors.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/protocols.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/provisioning.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/py.typed +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/records.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/registration.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/src/sanka_connector/schema.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/conftest.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/api/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/api/serializers.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/api/views.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/config/__init__.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/config/settings.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/config/urls.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_drf_project/manage.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_marketplace/drf-to-fastapi.json +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_marketplace/marketplace.json +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_cli_developer_experience.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_code_bundle.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_code_commands.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_concurrency.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_connector_client.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_connector_host.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_credential_provider.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_engine_e2e.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_engine_execution_adoption.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_engine_pg_to_clickhouse_e2e.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_engine_postgres_e2e.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_continuous.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_exact_scope.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_local_store.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_report_codec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_routes.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_scope.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_seam.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_execution_validate.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_extension_discovery.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_extension_lifecycle.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_extension_runner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_extension_wheel_acceptance.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_hashing.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_hosted_cli.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_mapping_candidates.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_mapping_owner_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_mapping_pending_relationships.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_mapping_record_mapping.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_planner.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_registry.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_safe_local_io.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_skill_commands.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_spec.py +0 -0
- {sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/test_state_store.py +0 -0
|
@@ -669,9 +669,51 @@ def _extension_result(
|
|
|
669
669
|
data = {"operation": operation, "records": records}
|
|
670
670
|
if args.json:
|
|
671
671
|
_print_json(_json_result("extension", data, migration_state="not_started"))
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
672
|
+
return 0
|
|
673
|
+
|
|
674
|
+
terminal = _terminal(args)
|
|
675
|
+
if operation == "list":
|
|
676
|
+
terminal.heading("Extensions")
|
|
677
|
+
terminal.table(
|
|
678
|
+
("Extension", "Kind", "Version", "Status", "Marketplace"),
|
|
679
|
+
[
|
|
680
|
+
(
|
|
681
|
+
str(record.get("id", "")),
|
|
682
|
+
str(record.get("kind", "")),
|
|
683
|
+
str(record.get("version", "")),
|
|
684
|
+
", ".join(map(str, record.get("status", []))),
|
|
685
|
+
str(record.get("marketplace", "")),
|
|
686
|
+
)
|
|
687
|
+
for record in records
|
|
688
|
+
],
|
|
689
|
+
)
|
|
690
|
+
elif operation == "marketplace_list":
|
|
691
|
+
terminal.heading("Marketplaces")
|
|
692
|
+
terminal.table(
|
|
693
|
+
("Marketplace", "Source", "Commit", "Trusted"),
|
|
694
|
+
[
|
|
695
|
+
(
|
|
696
|
+
str(record.get("name", "")),
|
|
697
|
+
str(record.get("source", "")),
|
|
698
|
+
str(record.get("resolved_commit") or "-")[:12],
|
|
699
|
+
"yes" if record.get("trusted") else "no",
|
|
700
|
+
)
|
|
701
|
+
for record in records
|
|
702
|
+
],
|
|
703
|
+
)
|
|
704
|
+
elif operation == "add":
|
|
705
|
+
record = records[0]
|
|
706
|
+
terminal.success(f"Installed {record['id']} {record['version']}")
|
|
707
|
+
elif operation == "remove":
|
|
708
|
+
terminal.success(f"Removed {records[0]['id']}")
|
|
709
|
+
elif operation == "marketplace_add":
|
|
710
|
+
terminal.success(f"Added marketplace {records[0]['name']}")
|
|
711
|
+
elif operation == "marketplace_upgrade":
|
|
712
|
+
terminal.success(
|
|
713
|
+
"Updated marketplace " + ", ".join(str(record["name"]) for record in records)
|
|
714
|
+
)
|
|
715
|
+
elif operation == "marketplace_remove":
|
|
716
|
+
terminal.success(f"Removed marketplace {records[0]['name']}")
|
|
675
717
|
return 0
|
|
676
718
|
|
|
677
719
|
|
|
@@ -57,6 +57,24 @@ class TerminalOutput:
|
|
|
57
57
|
if not self.quiet:
|
|
58
58
|
print(self.style(value, "cyan"), file=self.stdout)
|
|
59
59
|
|
|
60
|
+
def table(self, headers: tuple[str, ...], rows: list[tuple[str, ...]]) -> None:
|
|
61
|
+
if self.quiet:
|
|
62
|
+
return
|
|
63
|
+
widths = [
|
|
64
|
+
max(len(header), *(len(row[index]) for row in rows))
|
|
65
|
+
for index, header in enumerate(headers)
|
|
66
|
+
]
|
|
67
|
+
print(
|
|
68
|
+
" ".join(header.ljust(width) for header, width in zip(headers, widths, strict=True)),
|
|
69
|
+
file=self.stdout,
|
|
70
|
+
)
|
|
71
|
+
print(" ".join("-" * width for width in widths), file=self.stdout)
|
|
72
|
+
for row in rows:
|
|
73
|
+
print(
|
|
74
|
+
" ".join(value.ljust(width) for value, width in zip(row, widths, strict=True)),
|
|
75
|
+
file=self.stdout,
|
|
76
|
+
)
|
|
77
|
+
|
|
60
78
|
def success(self, value: str) -> None:
|
|
61
79
|
print(self.style(f"✓ OK {value}", "green"), file=self.stdout)
|
|
62
80
|
|
|
@@ -529,6 +529,13 @@ def _create_venv(
|
|
|
529
529
|
)
|
|
530
530
|
|
|
531
531
|
|
|
532
|
+
def _venv_launcher_names() -> tuple[str, ...]:
|
|
533
|
+
names = ("python", "python3", f"python{sys.version_info.major}.{sys.version_info.minor}")
|
|
534
|
+
if sys.version_info[:2] == (3, 14) and sys.getfilesystemencoding() == "utf-8":
|
|
535
|
+
names += ("𝜋thon",)
|
|
536
|
+
return names
|
|
537
|
+
|
|
538
|
+
|
|
532
539
|
def _normalize_venv_launchers(environment_descriptor: int) -> None:
|
|
533
540
|
if os.name == "nt":
|
|
534
541
|
return
|
|
@@ -543,10 +550,9 @@ def _normalize_venv_launchers(environment_descriptor: int) -> None:
|
|
|
543
550
|
)
|
|
544
551
|
os.unlink("lib64", dir_fd=environment_descriptor)
|
|
545
552
|
interpreter = Path(sys.executable).resolve()
|
|
546
|
-
names = ("python", "python3", f"python{sys.version_info.major}.{sys.version_info.minor}")
|
|
547
553
|
with _directory_at(environment_descriptor, "bin", Path("bin")) as bin_descriptor:
|
|
548
554
|
assert bin_descriptor is not None
|
|
549
|
-
for name in
|
|
555
|
+
for name in _venv_launcher_names():
|
|
550
556
|
with suppress(FileNotFoundError):
|
|
551
557
|
status = os.stat(name, dir_fd=bin_descriptor, follow_symlinks=False)
|
|
552
558
|
if not (stat.S_ISLNK(status.st_mode) or stat.S_ISREG(status.st_mode)):
|
|
@@ -2830,24 +2836,29 @@ class ExtensionStore:
|
|
|
2830
2836
|
missing_ok=True,
|
|
2831
2837
|
) as existing:
|
|
2832
2838
|
if existing is not None:
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
)
|
|
2841
|
-
if digest == expected_digest:
|
|
2842
|
-
try:
|
|
2843
|
-
self._verify_environment_artifacts(
|
|
2844
|
-
root, executable, wheels, providers=providers
|
|
2839
|
+
try:
|
|
2840
|
+
digest = content_hash(
|
|
2841
|
+
_tree_records(
|
|
2842
|
+
existing,
|
|
2843
|
+
error_code="SANKA_EXTENSION_PATH",
|
|
2844
|
+
subject="Extension environment",
|
|
2845
|
+
allowed_symlinks=self._environment_symlinks(),
|
|
2845
2846
|
)
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2847
|
+
)
|
|
2848
|
+
except ExtensionError as error:
|
|
2849
|
+
if error.code != "SANKA_EXTENSION_PATH" or expected_digest is not None:
|
|
2850
|
+
raise
|
|
2851
|
+
else:
|
|
2852
|
+
if digest == expected_digest:
|
|
2853
|
+
try:
|
|
2854
|
+
self._verify_environment_artifacts(
|
|
2855
|
+
root, executable, wheels, providers=providers
|
|
2856
|
+
)
|
|
2857
|
+
except ExtensionError as error:
|
|
2858
|
+
if error.code != "SANKA_EXTENSION_HASH_MISMATCH":
|
|
2859
|
+
raise
|
|
2860
|
+
else:
|
|
2861
|
+
return root
|
|
2851
2862
|
replace_existing = True
|
|
2852
2863
|
if replace_existing:
|
|
2853
2864
|
shutil.rmtree(environment_name, dir_fd=environments)
|
|
@@ -3212,11 +3223,7 @@ class ExtensionStore:
|
|
|
3212
3223
|
def _environment_symlinks() -> dict[str, Path]:
|
|
3213
3224
|
interpreter = Path(sys.executable).resolve()
|
|
3214
3225
|
return dict.fromkeys(
|
|
3215
|
-
(
|
|
3216
|
-
"bin/python",
|
|
3217
|
-
"bin/python3",
|
|
3218
|
-
f"bin/python{sys.version_info.major}.{sys.version_info.minor}",
|
|
3219
|
-
),
|
|
3226
|
+
(f"bin/{name}" for name in _venv_launcher_names()),
|
|
3220
3227
|
interpreter,
|
|
3221
3228
|
)
|
|
3222
3229
|
|
|
@@ -163,6 +163,73 @@ def test_extension_management_uses_stable_json_envelopes(
|
|
|
163
163
|
]
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
def test_extension_lists_use_human_readable_tables(
|
|
167
|
+
tmp_path: Path,
|
|
168
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
169
|
+
capsys: pytest.CaptureFixture[str],
|
|
170
|
+
) -> None:
|
|
171
|
+
class Record:
|
|
172
|
+
def __init__(self, data: dict[str, object]) -> None:
|
|
173
|
+
self.data = data
|
|
174
|
+
|
|
175
|
+
def to_dict(self) -> dict[str, object]:
|
|
176
|
+
return self.data
|
|
177
|
+
|
|
178
|
+
class Store:
|
|
179
|
+
def __init__(self, project_root: Path) -> None:
|
|
180
|
+
assert project_root == tmp_path
|
|
181
|
+
|
|
182
|
+
def list_extensions(self) -> tuple[Record, ...]:
|
|
183
|
+
return (
|
|
184
|
+
Record(
|
|
185
|
+
{
|
|
186
|
+
"id": "sanka/drf-to-fastapi",
|
|
187
|
+
"kind": "migration",
|
|
188
|
+
"version": "0.1.0a3",
|
|
189
|
+
"status": ["installed", "enabled"],
|
|
190
|
+
"marketplace": "official",
|
|
191
|
+
"wheels": [{"sha256": "secret-noise"}],
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def marketplaces(self) -> tuple[Record, ...]:
|
|
197
|
+
return (
|
|
198
|
+
Record(
|
|
199
|
+
{
|
|
200
|
+
"name": "official",
|
|
201
|
+
"source": "https://github.com/sankaHQ/extensions.git",
|
|
202
|
+
"resolved_commit": "a44ab1794924d45e8eeaeec456ce5c28c1c17d56",
|
|
203
|
+
"trusted": True,
|
|
204
|
+
"snapshot_root": "/private/cache/noise",
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
monkeypatch.chdir(tmp_path)
|
|
210
|
+
monkeypatch.setattr(cli, "ExtensionStore", Store)
|
|
211
|
+
|
|
212
|
+
assert main(["extension", "list", "--no-color"]) == 0
|
|
213
|
+
extensions = capsys.readouterr().out
|
|
214
|
+
assert "Extension" in extensions
|
|
215
|
+
assert "sanka/drf-to-fastapi" in extensions
|
|
216
|
+
assert "installed, enabled" in extensions
|
|
217
|
+
assert "wheels" not in extensions
|
|
218
|
+
assert "secret-noise" not in extensions
|
|
219
|
+
assert not extensions.lstrip().startswith("{")
|
|
220
|
+
|
|
221
|
+
assert main(["extension", "marketplace", "list", "--no-color"]) == 0
|
|
222
|
+
marketplaces = capsys.readouterr().out
|
|
223
|
+
assert "Marketplace" in marketplaces
|
|
224
|
+
assert "official" in marketplaces
|
|
225
|
+
assert "github.com/sankaHQ/extensions" in marketplaces
|
|
226
|
+
assert "a44ab179492" in marketplaces
|
|
227
|
+
assert "yes" in marketplaces
|
|
228
|
+
assert "snapshot_root" not in marketplaces
|
|
229
|
+
assert "private/cache/noise" not in marketplaces
|
|
230
|
+
assert not marketplaces.lstrip().startswith("{")
|
|
231
|
+
|
|
232
|
+
|
|
166
233
|
def test_extension_errors_preserve_stable_code_and_details(
|
|
167
234
|
tmp_path: Path,
|
|
168
235
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -550,7 +617,7 @@ def test_research_client_uses_public_branded_base_and_unwraps_data() -> None:
|
|
|
550
617
|
"after": ["2027-01"],
|
|
551
618
|
"locale": ["ja"],
|
|
552
619
|
}
|
|
553
|
-
assert request.get_header("User-agent") == "sanka-cli/0.2.
|
|
620
|
+
assert request.get_header("User-agent") == "sanka-cli/0.2.3"
|
|
554
621
|
assert timeout == 10.0
|
|
555
622
|
|
|
556
623
|
|
|
@@ -1748,6 +1748,82 @@ def test_venv_normalization_rejects_a_nonstandard_lib64_alias(tmp_path: Path) ->
|
|
|
1748
1748
|
os.close(descriptor)
|
|
1749
1749
|
|
|
1750
1750
|
|
|
1751
|
+
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1752
|
+
def test_venv_normalization_accepts_python_314_unicode_launcher(
|
|
1753
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1754
|
+
) -> None:
|
|
1755
|
+
environment = tmp_path / "environment"
|
|
1756
|
+
(environment / "bin").mkdir(parents=True)
|
|
1757
|
+
(environment / "bin" / "𝜋thon").symlink_to("python3.14")
|
|
1758
|
+
|
|
1759
|
+
class VersionInfo(tuple[object, ...]):
|
|
1760
|
+
major = 3
|
|
1761
|
+
minor = 14
|
|
1762
|
+
|
|
1763
|
+
version_info = VersionInfo((3, 14, 0, "final", 0))
|
|
1764
|
+
monkeypatch.setattr(sys, "version_info", version_info)
|
|
1765
|
+
monkeypatch.setattr(sys, "getfilesystemencoding", lambda: "utf-8")
|
|
1766
|
+
|
|
1767
|
+
descriptor = os.open(environment, os.O_RDONLY)
|
|
1768
|
+
try:
|
|
1769
|
+
extension_store._normalize_venv_launchers(descriptor)
|
|
1770
|
+
extension_store._tree_records(
|
|
1771
|
+
descriptor,
|
|
1772
|
+
error_code="SANKA_EXTENSION_PATH",
|
|
1773
|
+
subject="Extension environment",
|
|
1774
|
+
allowed_symlinks=ExtensionStore._environment_symlinks(),
|
|
1775
|
+
)
|
|
1776
|
+
finally:
|
|
1777
|
+
os.close(descriptor)
|
|
1778
|
+
|
|
1779
|
+
launcher = environment / "bin" / "𝜋thon"
|
|
1780
|
+
assert launcher.is_symlink()
|
|
1781
|
+
assert Path(os.readlink(launcher)) == Path(sys.executable).resolve()
|
|
1782
|
+
|
|
1783
|
+
|
|
1784
|
+
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1785
|
+
def test_add_rebuilds_an_orphaned_environment_with_an_unverified_symlink(
|
|
1786
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1787
|
+
) -> None:
|
|
1788
|
+
source, wheel = _marketplace(tmp_path / "source")
|
|
1789
|
+
store = ExtensionStore(tmp_path / "project", user_root=tmp_path / "home")
|
|
1790
|
+
store.add_marketplace(source, name="fixtures", trust=True)
|
|
1791
|
+
manifest = load_marketplace(store.marketplaces()[0].snapshot_root)[0]
|
|
1792
|
+
_responses(monkeypatch, {manifest.wheels[0].name: wheel})
|
|
1793
|
+
environment = store.user_root / "environments" / store._manifest_artifact_digest(manifest)
|
|
1794
|
+
(environment / "bin").mkdir(parents=True)
|
|
1795
|
+
stale_launcher = environment / "bin" / "stale-python"
|
|
1796
|
+
stale_launcher.symlink_to("python")
|
|
1797
|
+
|
|
1798
|
+
lock = store.add_extension("example/demo")
|
|
1799
|
+
|
|
1800
|
+
assert lock.artifact_digest == environment.name
|
|
1801
|
+
assert not stale_launcher.exists() and not stale_launcher.is_symlink()
|
|
1802
|
+
assert store.resolve_locked("example/demo") == lock
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1806
|
+
def test_add_rejects_an_unverified_symlink_in_a_recorded_environment(
|
|
1807
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
1808
|
+
) -> None:
|
|
1809
|
+
source, wheel = _marketplace(tmp_path / "source")
|
|
1810
|
+
store = ExtensionStore(tmp_path / "project", user_root=tmp_path / "home")
|
|
1811
|
+
store.add_marketplace(source, name="fixtures", trust=True)
|
|
1812
|
+
manifest = load_marketplace(store.marketplaces()[0].snapshot_root)[0]
|
|
1813
|
+
_responses(monkeypatch, {manifest.wheels[0].name: wheel})
|
|
1814
|
+
lock = store.add_extension("example/demo")
|
|
1815
|
+
stale_launcher = (
|
|
1816
|
+
store.user_root / "environments" / lock.artifact_digest / "bin" / "stale-python"
|
|
1817
|
+
)
|
|
1818
|
+
stale_launcher.symlink_to("python")
|
|
1819
|
+
|
|
1820
|
+
with pytest.raises(ExtensionError) as raised:
|
|
1821
|
+
store.add_extension("example/demo")
|
|
1822
|
+
|
|
1823
|
+
assert raised.value.code == "SANKA_EXTENSION_PATH"
|
|
1824
|
+
assert stale_launcher.is_symlink()
|
|
1825
|
+
|
|
1826
|
+
|
|
1751
1827
|
@pytest.mark.skipif(os.name != "posix", reason="POSIX venv launcher sealing only")
|
|
1752
1828
|
def test_real_posix_materializer_installs_without_a_venv_test_hook(
|
|
1753
1829
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
@@ -21,7 +21,7 @@ def test_root_version_uses_unified_public_version(runner: CliRunner) -> None:
|
|
|
21
21
|
result = runner.invoke(cli, ["--version"])
|
|
22
22
|
|
|
23
23
|
assert result.exit_code == 0
|
|
24
|
-
assert result.output == "sanka, version 0.2.
|
|
24
|
+
assert result.output == "sanka, version 0.2.3\n"
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def test_mcp_client_uses_the_cli_version_in_its_user_agent() -> None:
|
|
@@ -29,7 +29,7 @@ def test_mcp_client_uses_the_cli_version_in_its_user_agent() -> None:
|
|
|
29
29
|
|
|
30
30
|
client = SankaMigrateApiClient()
|
|
31
31
|
|
|
32
|
-
assert client._client.headers["User-Agent"] == "sanka-cli/0.2.
|
|
32
|
+
assert client._client.headers["User-Agent"] == "sanka-cli/0.2.3"
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def test_mcp_without_extra_has_install_hint(
|
|
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
|
|
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
|
|
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
|
{sanka_cli-0.2.2 → sanka_cli-0.2.3}/tests/fixtures/extension_marketplace/drf-to-fastapi.json
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|