aster-cli 0.1.2__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.
Files changed (42) hide show
  1. aster_cli-0.1.2/PKG-INFO +10 -0
  2. aster_cli-0.1.2/aster_cli/__init__.py +1 -0
  3. aster_cli-0.1.2/aster_cli/access.py +270 -0
  4. aster_cli-0.1.2/aster_cli/aster_service.py +300 -0
  5. aster_cli-0.1.2/aster_cli/codegen.py +828 -0
  6. aster_cli-0.1.2/aster_cli/codegen_typescript.py +819 -0
  7. aster_cli-0.1.2/aster_cli/contract.py +1112 -0
  8. aster_cli-0.1.2/aster_cli/credentials.py +87 -0
  9. aster_cli-0.1.2/aster_cli/enroll.py +315 -0
  10. aster_cli-0.1.2/aster_cli/handle_validation.py +53 -0
  11. aster_cli-0.1.2/aster_cli/identity.py +194 -0
  12. aster_cli-0.1.2/aster_cli/init.py +104 -0
  13. aster_cli-0.1.2/aster_cli/join.py +442 -0
  14. aster_cli-0.1.2/aster_cli/keygen.py +203 -0
  15. aster_cli-0.1.2/aster_cli/main.py +15 -0
  16. aster_cli-0.1.2/aster_cli/mcp/__init__.py +13 -0
  17. aster_cli-0.1.2/aster_cli/mcp/schema.py +205 -0
  18. aster_cli-0.1.2/aster_cli/mcp/security.py +108 -0
  19. aster_cli-0.1.2/aster_cli/mcp/server.py +407 -0
  20. aster_cli-0.1.2/aster_cli/profile.py +334 -0
  21. aster_cli-0.1.2/aster_cli/publish.py +598 -0
  22. aster_cli-0.1.2/aster_cli/shell/__init__.py +17 -0
  23. aster_cli-0.1.2/aster_cli/shell/app.py +2390 -0
  24. aster_cli-0.1.2/aster_cli/shell/commands.py +1624 -0
  25. aster_cli-0.1.2/aster_cli/shell/completer.py +156 -0
  26. aster_cli-0.1.2/aster_cli/shell/display.py +405 -0
  27. aster_cli-0.1.2/aster_cli/shell/guide.py +230 -0
  28. aster_cli-0.1.2/aster_cli/shell/hooks.py +255 -0
  29. aster_cli-0.1.2/aster_cli/shell/invoker.py +430 -0
  30. aster_cli-0.1.2/aster_cli/shell/plugin.py +185 -0
  31. aster_cli-0.1.2/aster_cli/shell/vfs.py +438 -0
  32. aster_cli-0.1.2/aster_cli/signer.py +150 -0
  33. aster_cli-0.1.2/aster_cli/templates/llm/python.md +578 -0
  34. aster_cli-0.1.2/aster_cli/trust.py +244 -0
  35. aster_cli-0.1.2/aster_cli.egg-info/PKG-INFO +10 -0
  36. aster_cli-0.1.2/aster_cli.egg-info/SOURCES.txt +40 -0
  37. aster_cli-0.1.2/aster_cli.egg-info/dependency_links.txt +1 -0
  38. aster_cli-0.1.2/aster_cli.egg-info/entry_points.txt +2 -0
  39. aster_cli-0.1.2/aster_cli.egg-info/requires.txt +5 -0
  40. aster_cli-0.1.2/aster_cli.egg-info/top_level.txt +1 -0
  41. aster_cli-0.1.2/pyproject.toml +25 -0
  42. aster_cli-0.1.2/setup.cfg +4 -0
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: aster-cli
3
+ Version: 0.1.2
4
+ Summary: Aster RPC framework command-line tools
5
+ Requires-Python: <3.14,>=3.9
6
+ Requires-Dist: aster-rpc>=0.1.2
7
+ Requires-Dist: prompt-toolkit>=3.0
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: mcp>=1.0
10
+ Requires-Dist: keyring>=25.0
@@ -0,0 +1 @@
1
+ """aster_cli -- Aster RPC framework command-line tools."""
@@ -0,0 +1,270 @@
1
+ """Access-control CLI commands for the Day 0 @aster service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+
9
+ from aster_cli.aster_service import (
10
+ build_signed_envelope,
11
+ generate_nonce,
12
+ now_epoch_seconds,
13
+ open_aster_service,
14
+ parse_duration_seconds,
15
+ )
16
+ from aster_cli.profile import get_active_profile
17
+
18
+
19
+ def _require_verified_handle() -> str:
20
+ _profile_name, profile, _config = get_active_profile()
21
+ handle = str(profile.get("handle", "")).strip()
22
+ if profile.get("handle_status") != "verified" or not handle:
23
+ raise RuntimeError(
24
+ "access commands require a verified handle. Finish `aster join` / `aster verify` first."
25
+ )
26
+ return handle
27
+
28
+
29
+ def _signed_payload(action: str, **fields: object) -> dict[str, object]:
30
+ payload: dict[str, object] = {
31
+ "action": action,
32
+ **fields,
33
+ "timestamp": now_epoch_seconds(),
34
+ "nonce": generate_nonce(),
35
+ }
36
+ return payload
37
+
38
+
39
+ def cmd_access_grant(args: argparse.Namespace) -> int:
40
+ try:
41
+ return asyncio.run(_grant_remote(args, handle=_require_verified_handle()))
42
+ except Exception as exc:
43
+ print(f"Error: {exc}")
44
+ return 1
45
+
46
+
47
+ async def _grant_remote(args: argparse.Namespace, *, handle: str) -> int:
48
+ runtime = await open_aster_service(getattr(args, "aster", None))
49
+ try:
50
+ access_client = await runtime.access_client()
51
+ payload: dict[str, object] = _signed_payload(
52
+ "grant_access",
53
+ handle=handle,
54
+ service_name=args.service,
55
+ consumer_handle=args.consumer,
56
+ role=args.role,
57
+ scope=args.scope,
58
+ scope_node_id=args.scope_node_id,
59
+ )
60
+ envelope = build_signed_envelope(payload, root_key_file=getattr(args, "root_key", None))
61
+ result = await access_client.grant_access(runtime.signed_request(envelope))
62
+ finally:
63
+ await runtime.close()
64
+
65
+ print(
66
+ f"Granted {getattr(result, 'role', args.role)} access to "
67
+ f"@{getattr(result, 'consumer_handle', args.consumer)} for {args.service} "
68
+ f"({getattr(result, 'scope', args.scope)})."
69
+ )
70
+ return 0
71
+
72
+
73
+ def cmd_access_revoke(args: argparse.Namespace) -> int:
74
+ try:
75
+ return asyncio.run(_revoke_remote(args, handle=_require_verified_handle()))
76
+ except Exception as exc:
77
+ print(f"Error: {exc}")
78
+ return 1
79
+
80
+
81
+ async def _revoke_remote(args: argparse.Namespace, *, handle: str) -> int:
82
+ runtime = await open_aster_service(getattr(args, "aster", None))
83
+ try:
84
+ access_client = await runtime.access_client()
85
+ payload = _signed_payload(
86
+ "revoke_access",
87
+ handle=handle,
88
+ service_name=args.service,
89
+ consumer_handle=args.consumer,
90
+ )
91
+ envelope = build_signed_envelope(payload, root_key_file=getattr(args, "root_key", None))
92
+ result = await access_client.revoke_access(runtime.signed_request(envelope))
93
+ finally:
94
+ await runtime.close()
95
+
96
+ if not getattr(result, "revoked", False):
97
+ print(f"No access grant found for @{args.consumer} on {args.service}.")
98
+ return 1
99
+ print(f"Revoked access for @{getattr(result, 'consumer_handle', args.consumer)} on {args.service}.")
100
+ return 0
101
+
102
+
103
+ def cmd_access_list(args: argparse.Namespace) -> int:
104
+ try:
105
+ return asyncio.run(_list_remote(args, handle=_require_verified_handle()))
106
+ except Exception as exc:
107
+ print(f"Error: {exc}")
108
+ return 1
109
+
110
+
111
+ async def _list_remote(args: argparse.Namespace, *, handle: str) -> int:
112
+ runtime = await open_aster_service(getattr(args, "aster", None))
113
+ try:
114
+ access_client = await runtime.access_client()
115
+ payload = _signed_payload(
116
+ "list_access",
117
+ handle=handle,
118
+ service_name=args.service,
119
+ )
120
+ envelope = build_signed_envelope(payload, root_key_file=getattr(args, "root_key", None))
121
+ result = await access_client.list_access(runtime.signed_request(envelope))
122
+ finally:
123
+ await runtime.close()
124
+
125
+ grants = [
126
+ {
127
+ "consumer_handle": getattr(entry, "consumer_handle", ""),
128
+ "role": getattr(entry, "role", ""),
129
+ "scope": getattr(entry, "scope", ""),
130
+ "granted_at": getattr(entry, "granted_at", ""),
131
+ }
132
+ for entry in getattr(result, "grants", [])
133
+ ]
134
+
135
+ if getattr(args, "raw_json", False):
136
+ print(json.dumps({"service_name": args.service, "grants": grants}, indent=2))
137
+ return 0
138
+
139
+ print(f"{args.service}: {len(grants)} grant(s)")
140
+ for grant in grants:
141
+ print(
142
+ f" @{grant['consumer_handle']} "
143
+ f"{grant['role'] or 'consumer'} "
144
+ f"{grant['scope'] or 'handle'} "
145
+ f"{grant['granted_at'] or ''}".rstrip()
146
+ )
147
+ return 0
148
+
149
+
150
+ def _delegation_roles(args: argparse.Namespace) -> list[str]:
151
+ if getattr(args, "role", None):
152
+ return sorted(set(args.role))
153
+ return ["consumer"]
154
+
155
+
156
+ def cmd_access_delegation(args: argparse.Namespace) -> int:
157
+ try:
158
+ return asyncio.run(_delegation_remote(args, handle=_require_verified_handle()))
159
+ except Exception as exc:
160
+ print(f"Error: {exc}")
161
+ return 1
162
+
163
+
164
+ async def _delegation_remote(args: argparse.Namespace, *, handle: str) -> int:
165
+ runtime = await open_aster_service(getattr(args, "aster", None))
166
+ try:
167
+ access_client = await runtime.access_client()
168
+ payload = _signed_payload(
169
+ "update_delegation",
170
+ handle=handle,
171
+ service_name=args.service,
172
+ delegation={
173
+ "authority": "consumer",
174
+ "mode": "closed" if args.closed else "open",
175
+ "token_ttl": parse_duration_seconds(args.token_ttl, default=300),
176
+ "rate_limit": args.rate_limit,
177
+ "roles": _delegation_roles(args),
178
+ },
179
+ )
180
+ envelope = build_signed_envelope(payload, root_key_file=getattr(args, "root_key", None))
181
+ result = await access_client.update_delegation(runtime.signed_request(envelope))
182
+ finally:
183
+ await runtime.close()
184
+
185
+ delegation = getattr(result, "delegation", None)
186
+ mode = getattr(delegation, "mode", "closed" if args.closed else "open")
187
+ print(f"Updated @{handle}/{args.service} delegation to {mode}.")
188
+ return 0
189
+
190
+
191
+ def cmd_access_public_private(args: argparse.Namespace) -> int:
192
+ from aster_cli.publish import cmd_set_visibility
193
+
194
+ visibility = "public" if args.access_command == "public" else "private"
195
+ return cmd_set_visibility(
196
+ argparse.Namespace(
197
+ command="visibility",
198
+ service=args.service,
199
+ visibility=visibility,
200
+ aster=getattr(args, "aster", None),
201
+ root_key=getattr(args, "root_key", None),
202
+ )
203
+ )
204
+
205
+
206
+ def register_access_subparser(subparsers: argparse._SubParsersAction) -> None:
207
+ access_parser = subparsers.add_parser("access", help="Manage Day 0 @aster service access grants")
208
+ access_subparsers = access_parser.add_subparsers(dest="access_command")
209
+
210
+ grant_parser = access_subparsers.add_parser("grant", help="Grant a consumer access to a service")
211
+ grant_parser.add_argument("service", help="Published service name")
212
+ grant_parser.add_argument("consumer", help="Consumer handle to grant")
213
+ grant_parser.add_argument("--role", default="consumer", help="Delegated role (default: consumer)")
214
+ grant_parser.add_argument(
215
+ "--scope",
216
+ choices=["handle", "node"],
217
+ default="handle",
218
+ help="Grant scope (default: handle)",
219
+ )
220
+ grant_parser.add_argument("--scope-node-id", default=None, help="Required when --scope node")
221
+ grant_parser.add_argument("--aster", default=None, help="Override @aster service address")
222
+ grant_parser.add_argument("--root-key", default=None, help="Path to root key JSON backup")
223
+
224
+ revoke_parser = access_subparsers.add_parser("revoke", help="Revoke a consumer's service access")
225
+ revoke_parser.add_argument("service", help="Published service name")
226
+ revoke_parser.add_argument("consumer", help="Consumer handle to revoke")
227
+ revoke_parser.add_argument("--aster", default=None, help="Override @aster service address")
228
+ revoke_parser.add_argument("--root-key", default=None, help="Path to root key JSON backup")
229
+
230
+ list_parser = access_subparsers.add_parser("list", help="List access grants for a published service")
231
+ list_parser.add_argument("service", help="Published service name")
232
+ list_parser.add_argument("--aster", default=None, help="Override @aster service address")
233
+ list_parser.add_argument("--root-key", default=None, help="Path to root key JSON backup")
234
+ list_parser.add_argument("--json", action="store_true", dest="raw_json", help="Output raw JSON")
235
+
236
+ delegation_parser = access_subparsers.add_parser("delegation", help="Update delegated access mode for a service")
237
+ delegation_parser.add_argument("service", help="Published service name")
238
+ delegation_mode = delegation_parser.add_mutually_exclusive_group()
239
+ delegation_mode.add_argument("--open", action="store_true", help="Allow open enrollment")
240
+ delegation_mode.add_argument("--closed", action="store_true", help="Require explicit grants")
241
+ delegation_parser.add_argument("--token-ttl", default="5m", help="Delegated token TTL (default: 5m)")
242
+ delegation_parser.add_argument("--rate-limit", default=None, help='Delegated issuance rate limit like "1/60m"')
243
+ delegation_parser.add_argument("--role", action="append", default=[], help="Delegated role (repeatable)")
244
+ delegation_parser.add_argument("--aster", default=None, help="Override @aster service address")
245
+ delegation_parser.add_argument("--root-key", default=None, help="Path to root key JSON backup")
246
+
247
+ public_parser = access_subparsers.add_parser("public", help="Make a published service discoverable")
248
+ public_parser.add_argument("service", help="Published service name")
249
+ public_parser.add_argument("--aster", default=None, help="Override @aster service address")
250
+ public_parser.add_argument("--root-key", default=None, help="Path to root key JSON backup")
251
+
252
+ private_parser = access_subparsers.add_parser("private", help="Hide a published service from discovery")
253
+ private_parser.add_argument("service", help="Published service name")
254
+ private_parser.add_argument("--aster", default=None, help="Override @aster service address")
255
+ private_parser.add_argument("--root-key", default=None, help="Path to root key JSON backup")
256
+
257
+
258
+ def run_access_command(args: argparse.Namespace) -> int:
259
+ if args.access_command == "grant":
260
+ return cmd_access_grant(args)
261
+ if args.access_command == "revoke":
262
+ return cmd_access_revoke(args)
263
+ if args.access_command == "list":
264
+ return cmd_access_list(args)
265
+ if args.access_command == "delegation":
266
+ return cmd_access_delegation(args)
267
+ if args.access_command in {"public", "private"}:
268
+ return cmd_access_public_private(args)
269
+ print("Usage: aster access [grant|revoke|list] ...")
270
+ return 1
@@ -0,0 +1,300 @@
1
+ """Helpers for talking to the Day 0 @aster service from the CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import importlib
7
+ import json
8
+ import os
9
+ import secrets
10
+ import sys
11
+ import tempfile
12
+ import time
13
+ import asyncio
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from aster.trust.signing import load_private_key
19
+ from aster_cli.codegen import generate_python_clients
20
+ from aster_cli.credentials import get_root_privkey
21
+ from aster_cli.profile import get_active_profile, get_aster_service_config
22
+
23
+
24
+ def now_epoch_seconds() -> int:
25
+ return int(time.time())
26
+
27
+
28
+ def generate_nonce() -> str:
29
+ return secrets.token_hex(16)
30
+
31
+
32
+ def canonical_payload_json(payload: dict[str, Any]) -> str:
33
+ """Serialize payloads in a stable form before signing."""
34
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
35
+
36
+
37
+ def load_root_private_key_hex(
38
+ *,
39
+ root_key_file: str | None = None,
40
+ profile_name: str | None = None,
41
+ ) -> tuple[str, str]:
42
+ """Return (profile_name, private_key_hex) for the active profile."""
43
+ active_name, _profile, _config = get_active_profile()
44
+ name = profile_name or active_name
45
+
46
+ priv_hex = get_root_privkey(name)
47
+ if priv_hex:
48
+ return name, priv_hex
49
+
50
+ key_path = Path(os.path.expanduser(root_key_file or "~/.aster/root.key"))
51
+ if key_path.exists():
52
+ data = json.loads(key_path.read_text())
53
+ priv_hex = str(data.get("private_key", "")).strip()
54
+ if priv_hex:
55
+ return name, priv_hex
56
+
57
+ raise RuntimeError(
58
+ f"no root private key found for profile '{name}'. "
59
+ "Run `aster keygen root` first, or pass --root-key."
60
+ )
61
+
62
+
63
+ def resolve_aster_service_address(explicit: str | None = None) -> str:
64
+ """Resolve the @aster service address from CLI arg, env, or config."""
65
+ if explicit:
66
+ return explicit
67
+
68
+ env_addr = os.environ.get("ASTER_SERVICE_ADDR", "").strip()
69
+ if env_addr:
70
+ return env_addr
71
+
72
+ _name, _profile, config = get_active_profile()
73
+ service_cfg = get_aster_service_config(config)
74
+ if not service_cfg.get("enabled", True):
75
+ raise RuntimeError(
76
+ "@aster service access is disabled in config. "
77
+ "Pass --aster to override, or re-enable `[aster_service].enabled`."
78
+ )
79
+
80
+ addr = str(service_cfg.get("node_id", "")).strip()
81
+ if addr:
82
+ return addr
83
+
84
+ raise RuntimeError(
85
+ "no @aster service address configured. "
86
+ "Set `[aster_service].node_id`, export `ASTER_SERVICE_ADDR`, or pass --aster."
87
+ )
88
+
89
+
90
+ def parse_duration_seconds(value: str | int | None, *, default: int) -> int:
91
+ """Parse a simple duration like 300, 5m, 1h, or 1d."""
92
+ if value is None:
93
+ return default
94
+ if isinstance(value, int):
95
+ return value
96
+
97
+ raw = str(value).strip().lower()
98
+ if not raw:
99
+ return default
100
+ if raw.isdigit():
101
+ return int(raw)
102
+
103
+ unit = raw[-1]
104
+ number = raw[:-1]
105
+ if not number.isdigit():
106
+ raise ValueError(f"invalid duration: {value!r}")
107
+ count = int(number)
108
+ if unit == "s":
109
+ return count
110
+ if unit == "m":
111
+ return count * 60
112
+ if unit == "h":
113
+ return count * 3600
114
+ if unit == "d":
115
+ return count * 86400
116
+ raise ValueError(f"invalid duration: {value!r}")
117
+
118
+
119
+ def load_local_endpoint_id(identity_path: str | None = None) -> str | None:
120
+ """Load the current node endpoint_id from .aster-identity if present."""
121
+ path = Path(identity_path or ".aster-identity")
122
+ if not path.exists():
123
+ return None
124
+
125
+ from aster_cli.identity import load_identity
126
+
127
+ data = load_identity(path)
128
+ node = data.get("node", {})
129
+ endpoint_id = str(node.get("endpoint_id", "")).strip()
130
+ return endpoint_id or None
131
+
132
+
133
+ @dataclass
134
+ class SignedEnvelope:
135
+ payload: dict[str, Any]
136
+ payload_json: str
137
+ signer_pubkey: str
138
+ signature: str
139
+
140
+
141
+ def build_signed_envelope(
142
+ payload: dict[str, Any],
143
+ *,
144
+ root_key_file: str | None = None,
145
+ profile_name: str | None = None,
146
+ signer_pubkey: str | None = None,
147
+ ) -> SignedEnvelope:
148
+ """Sign a payload for the Day 0 @aster SignedRequest wrapper."""
149
+ active_name, profile, _config = get_active_profile()
150
+ name, priv_hex = load_root_private_key_hex(
151
+ root_key_file=root_key_file,
152
+ profile_name=profile_name or active_name,
153
+ )
154
+ pub_hex = signer_pubkey or str(profile.get("root_pubkey", "")).strip()
155
+ if not pub_hex:
156
+ raise RuntimeError(
157
+ f"profile '{name}' has no root public key configured. "
158
+ "Run `aster keygen root` first."
159
+ )
160
+
161
+ payload_json = canonical_payload_json(payload)
162
+ signature = load_private_key(bytes.fromhex(priv_hex)).sign(payload_json.encode("utf-8")).hex()
163
+ return SignedEnvelope(
164
+ payload=payload,
165
+ payload_json=payload_json,
166
+ signer_pubkey=pub_hex,
167
+ signature=signature,
168
+ )
169
+
170
+
171
+ class AsterServiceRuntime:
172
+ """Runtime-generated typed clients for the current @aster node."""
173
+
174
+ def __init__(self, address: str):
175
+ self.address = address
176
+ self._peer: PeerConnection | None = None
177
+ self._package_name: str | None = None
178
+ self._temp_dir: str | None = None
179
+ self._loaded: bool = False
180
+ self._types_signed_request: type[Any] | None = None
181
+ self._profile_client_cls: type[Any] | None = None
182
+ self._publication_client_cls: type[Any] | None = None
183
+ self._access_client_cls: type[Any] | None = None
184
+ self._clients: dict[str, Any] = {}
185
+
186
+ async def connect(self) -> None:
187
+ from aster_cli.shell.app import PeerConnection
188
+
189
+ peer = PeerConnection(self.address)
190
+ await peer.connect()
191
+ manifests: dict[str, dict[str, Any]] = {}
192
+ last_error: Exception | None = None
193
+ for _attempt in range(4):
194
+ try:
195
+ await peer._fetch_manifests()
196
+ except Exception as exc:
197
+ last_error = exc
198
+ manifests = peer.get_manifests()
199
+ if manifests:
200
+ break
201
+ await asyncio.sleep(0.5)
202
+ if not manifests:
203
+ if last_error is not None:
204
+ raise RuntimeError(f"no service manifests available from @aster ({last_error})")
205
+ raise RuntimeError("no service manifests available from @aster")
206
+
207
+ temp_dir = tempfile.mkdtemp(prefix="aster-cli-runtime-")
208
+ package_name = f"aster_cli_runtime_{os.getpid()}"
209
+ generate_python_clients(
210
+ manifests,
211
+ out_dir=temp_dir,
212
+ namespace=package_name,
213
+ source=self.address,
214
+ )
215
+
216
+ sys.path.insert(0, temp_dir)
217
+ try:
218
+ signed_mod = importlib.import_module(
219
+ f"{package_name}.types.signed_request"
220
+ )
221
+ profile_mod = importlib.import_module(
222
+ f"{package_name}.services.profile_service_v1"
223
+ )
224
+ publication_mod = importlib.import_module(
225
+ f"{package_name}.services.publication_service_v1"
226
+ )
227
+ access_mod = importlib.import_module(
228
+ f"{package_name}.services.access_service_v1"
229
+ )
230
+ except Exception:
231
+ with contextlib.suppress(ValueError):
232
+ sys.path.remove(temp_dir)
233
+ raise
234
+
235
+ self._peer = peer
236
+ self._temp_dir = temp_dir
237
+ self._package_name = package_name
238
+ self._types_signed_request = signed_mod.SignedRequest
239
+ self._profile_client_cls = profile_mod.ProfileServiceClient
240
+ self._publication_client_cls = publication_mod.PublicationServiceClient
241
+ self._access_client_cls = access_mod.AccessServiceClient
242
+ self._loaded = True
243
+
244
+ async def close(self) -> None:
245
+ if self._peer and getattr(self._peer, "_aster_client", None) is not None:
246
+ with contextlib.suppress(Exception):
247
+ await self._peer._aster_client.close()
248
+ self._clients.clear()
249
+ if self._temp_dir:
250
+ with contextlib.suppress(ValueError):
251
+ sys.path.remove(self._temp_dir)
252
+
253
+ async def __aenter__(self) -> AsterServiceRuntime:
254
+ await self.connect()
255
+ return self
256
+
257
+ async def __aexit__(self, exc_type, exc, tb) -> None:
258
+ await self.close()
259
+
260
+ def signed_request(self, envelope: SignedEnvelope) -> Any:
261
+ if not self._loaded or self._types_signed_request is None:
262
+ raise RuntimeError("runtime clients not loaded")
263
+ return self._types_signed_request(
264
+ payload_json=envelope.payload_json,
265
+ signer_pubkey=envelope.signer_pubkey,
266
+ signature=envelope.signature,
267
+ )
268
+
269
+ async def profile_client(self) -> Any:
270
+ if "profile" not in self._clients:
271
+ if self._profile_client_cls is None or self._peer is None:
272
+ raise RuntimeError("runtime clients not loaded")
273
+ self._clients["profile"] = await self._profile_client_cls.from_connection(
274
+ self._peer._aster_client
275
+ )
276
+ return self._clients["profile"]
277
+
278
+ async def publication_client(self) -> Any:
279
+ if "publication" not in self._clients:
280
+ if self._publication_client_cls is None or self._peer is None:
281
+ raise RuntimeError("runtime clients not loaded")
282
+ self._clients["publication"] = await self._publication_client_cls.from_connection(
283
+ self._peer._aster_client
284
+ )
285
+ return self._clients["publication"]
286
+
287
+ async def access_client(self) -> Any:
288
+ if "access" not in self._clients:
289
+ if self._access_client_cls is None or self._peer is None:
290
+ raise RuntimeError("runtime clients not loaded")
291
+ self._clients["access"] = await self._access_client_cls.from_connection(
292
+ self._peer._aster_client
293
+ )
294
+ return self._clients["access"]
295
+
296
+
297
+ async def open_aster_service(explicit_address: str | None = None) -> AsterServiceRuntime:
298
+ runtime = AsterServiceRuntime(resolve_aster_service_address(explicit_address))
299
+ await runtime.connect()
300
+ return runtime