natsio-natscontext 0.1.0__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.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: natsio-natscontext
3
+ Version: 0.1.0
4
+ Summary: Load `nats` CLI context files (ADR-21) into natsio connect options
5
+ Keywords: nats,nats.io,natscontext,nats-cli,configuration
6
+ Author: Corrupt Mane
7
+ Author-email: Corrupt Mane <corruptmane@gmail.com>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: AsyncIO
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: natsio
16
+ Requires-Python: >=3.13
17
+ Description-Content-Type: text/markdown
18
+
19
+ # natsio-natscontext
20
+
21
+ Connect [natsio](https://pypi.org/project/natsio/) using the connection
22
+ profiles ("contexts") managed by the [`nats` command-line
23
+ tool](https://github.com/nats-io/natscli) — the same JSON files under
24
+ `~/.config/nats/context/`. This is the Python counterpart of the Go helper
25
+ [`orbit.go/natscontext`](https://github.com/synadia-io/orbit.go/tree/main/natscontext),
26
+ following [ADR-21](https://github.com/nats-io/nats-architecture-and-design).
27
+
28
+ ```bash
29
+ pip install natsio-natscontext
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ Create a context with the CLI, then use it from Python:
35
+
36
+ ```bash
37
+ nats context add staging --server nats://staging:4222 --creds ~/staging.creds
38
+ nats context select staging
39
+ ```
40
+
41
+ ```python
42
+ import natsio.natscontext as natscontext
43
+
44
+ # Connect using a named context...
45
+ async with await natscontext.connect("staging") as nc:
46
+ await nc.publish("greet", b"hi")
47
+
48
+ # ...or the currently selected context (from `nats context select`).
49
+ async with await natscontext.connect() as nc:
50
+ ...
51
+
52
+ # Overrides are forwarded to natsio.connect and win over the context:
53
+ nc = await natscontext.connect("staging", name="my-app", ping_interval=30)
54
+ ```
55
+
56
+ Inspect or reuse a context without connecting:
57
+
58
+ ```python
59
+ ctx = natscontext.load("staging") # -> Context
60
+ print(ctx.url, ctx.jetstream_domain)
61
+ kwargs = ctx.connect_kwargs() # dict of natsio.connect(**kwargs)
62
+
63
+ natscontext.list_contexts() # ['prod', 'staging', ...]
64
+ natscontext.selected_context() # 'staging' or None
65
+ ```
66
+
67
+ An absolute path is loaded directly instead of by name:
68
+
69
+ ```python
70
+ ctx = natscontext.load("/etc/nats/prod.json")
71
+ ```
72
+
73
+ ## Discovery
74
+
75
+ Contexts are resolved under `$XDG_CONFIG_HOME/nats` (falling back to
76
+ `~/.config/nats`):
77
+
78
+ | Path | Purpose |
79
+ |---|---|
80
+ | `nats/context/<name>.json` | one context per file |
81
+ | `nats/context.txt` | name of the active/selected context |
82
+
83
+ ## Field mapping
84
+
85
+ | Context field | natsio option | Notes |
86
+ |---|---|---|
87
+ | `url` | `servers` | split on `,` into a tuple |
88
+ | `user` + `password` | `user`, `password` | |
89
+ | `creds` | `credentials` | `~` and `$VAR` expanded |
90
+ | `nkey` | `nkey_seed` | the **file** is read and the seed extracted |
91
+ | `token` | `token` | |
92
+ | `cert` + `key` | `tls` | `SSLContext.load_cert_chain` |
93
+ | `ca` | `tls` | `SSLContext.load_verify_locations` |
94
+ | `tls_first` | `tls.handshake_first` | |
95
+ | `inbox_prefix` | `inbox_prefix` | |
96
+
97
+ **Auth precedence** mirrors the Go helper: `user` › `creds` › `nkey` › `token`.
98
+ natsio does not allow combining mechanisms, so a `token` set *alongside*
99
+ another mechanism is ignored (nats.go would send both).
100
+
101
+ ### Parsed but not applied
102
+
103
+ These fields are read and preserved on the returned `Context` (so you can use
104
+ them yourself — e.g. the JetStream domain when creating a JetStream context),
105
+ but natsio's connection layer has no equivalent and does **not** apply them:
106
+
107
+ | Context field | `Context` attribute | Why not applied |
108
+ |---|---|---|
109
+ | `jetstream_domain` | `.jetstream_domain` | connection-independent; used at the JetStream API layer |
110
+ | `jetstream_api_prefix` | `.jetstream_api_prefix` | JetStream API layer |
111
+ | `jetstream_event_prefix` | `.jetstream_event_prefix` | JetStream API layer |
112
+ | `socks_proxy` | `.socks_proxy` | natsio has no custom-dialer / proxy hook |
113
+ | `nsc` | `.nsc` | would shell out to the `nsc` binary |
114
+ | `user_jwt` | `.user_jwt` | the Go helper parses but never applies it either |
115
+ | `color_scheme` | `.color_scheme` | CLI cosmetic |
116
+ | `description` | `.description` | metadata |
117
+ | `windows_cert_store*` | `.windows_cert_store` | unsupported — `connect_kwargs()` raises `ContextError` |
118
+
119
+ Unrecognised JSON keys are kept in `Context.raw` and reported by
120
+ `Context.unknown_keys`.
121
+
122
+ ## Interop with the `nats` CLI
123
+
124
+ This package only ever **reads** context files; it never writes them. Manage
125
+ contexts with the CLI (`nats context add|select|edit|ls|rm`). Files written by
126
+ this library's format are byte-compatible with what the CLI expects.
127
+
128
+ ## License
129
+
130
+ Apache-2.0.
@@ -0,0 +1,112 @@
1
+ # natsio-natscontext
2
+
3
+ Connect [natsio](https://pypi.org/project/natsio/) using the connection
4
+ profiles ("contexts") managed by the [`nats` command-line
5
+ tool](https://github.com/nats-io/natscli) — the same JSON files under
6
+ `~/.config/nats/context/`. This is the Python counterpart of the Go helper
7
+ [`orbit.go/natscontext`](https://github.com/synadia-io/orbit.go/tree/main/natscontext),
8
+ following [ADR-21](https://github.com/nats-io/nats-architecture-and-design).
9
+
10
+ ```bash
11
+ pip install natsio-natscontext
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Create a context with the CLI, then use it from Python:
17
+
18
+ ```bash
19
+ nats context add staging --server nats://staging:4222 --creds ~/staging.creds
20
+ nats context select staging
21
+ ```
22
+
23
+ ```python
24
+ import natsio.natscontext as natscontext
25
+
26
+ # Connect using a named context...
27
+ async with await natscontext.connect("staging") as nc:
28
+ await nc.publish("greet", b"hi")
29
+
30
+ # ...or the currently selected context (from `nats context select`).
31
+ async with await natscontext.connect() as nc:
32
+ ...
33
+
34
+ # Overrides are forwarded to natsio.connect and win over the context:
35
+ nc = await natscontext.connect("staging", name="my-app", ping_interval=30)
36
+ ```
37
+
38
+ Inspect or reuse a context without connecting:
39
+
40
+ ```python
41
+ ctx = natscontext.load("staging") # -> Context
42
+ print(ctx.url, ctx.jetstream_domain)
43
+ kwargs = ctx.connect_kwargs() # dict of natsio.connect(**kwargs)
44
+
45
+ natscontext.list_contexts() # ['prod', 'staging', ...]
46
+ natscontext.selected_context() # 'staging' or None
47
+ ```
48
+
49
+ An absolute path is loaded directly instead of by name:
50
+
51
+ ```python
52
+ ctx = natscontext.load("/etc/nats/prod.json")
53
+ ```
54
+
55
+ ## Discovery
56
+
57
+ Contexts are resolved under `$XDG_CONFIG_HOME/nats` (falling back to
58
+ `~/.config/nats`):
59
+
60
+ | Path | Purpose |
61
+ |---|---|
62
+ | `nats/context/<name>.json` | one context per file |
63
+ | `nats/context.txt` | name of the active/selected context |
64
+
65
+ ## Field mapping
66
+
67
+ | Context field | natsio option | Notes |
68
+ |---|---|---|
69
+ | `url` | `servers` | split on `,` into a tuple |
70
+ | `user` + `password` | `user`, `password` | |
71
+ | `creds` | `credentials` | `~` and `$VAR` expanded |
72
+ | `nkey` | `nkey_seed` | the **file** is read and the seed extracted |
73
+ | `token` | `token` | |
74
+ | `cert` + `key` | `tls` | `SSLContext.load_cert_chain` |
75
+ | `ca` | `tls` | `SSLContext.load_verify_locations` |
76
+ | `tls_first` | `tls.handshake_first` | |
77
+ | `inbox_prefix` | `inbox_prefix` | |
78
+
79
+ **Auth precedence** mirrors the Go helper: `user` › `creds` › `nkey` › `token`.
80
+ natsio does not allow combining mechanisms, so a `token` set *alongside*
81
+ another mechanism is ignored (nats.go would send both).
82
+
83
+ ### Parsed but not applied
84
+
85
+ These fields are read and preserved on the returned `Context` (so you can use
86
+ them yourself — e.g. the JetStream domain when creating a JetStream context),
87
+ but natsio's connection layer has no equivalent and does **not** apply them:
88
+
89
+ | Context field | `Context` attribute | Why not applied |
90
+ |---|---|---|
91
+ | `jetstream_domain` | `.jetstream_domain` | connection-independent; used at the JetStream API layer |
92
+ | `jetstream_api_prefix` | `.jetstream_api_prefix` | JetStream API layer |
93
+ | `jetstream_event_prefix` | `.jetstream_event_prefix` | JetStream API layer |
94
+ | `socks_proxy` | `.socks_proxy` | natsio has no custom-dialer / proxy hook |
95
+ | `nsc` | `.nsc` | would shell out to the `nsc` binary |
96
+ | `user_jwt` | `.user_jwt` | the Go helper parses but never applies it either |
97
+ | `color_scheme` | `.color_scheme` | CLI cosmetic |
98
+ | `description` | `.description` | metadata |
99
+ | `windows_cert_store*` | `.windows_cert_store` | unsupported — `connect_kwargs()` raises `ContextError` |
100
+
101
+ Unrecognised JSON keys are kept in `Context.raw` and reported by
102
+ `Context.unknown_keys`.
103
+
104
+ ## Interop with the `nats` CLI
105
+
106
+ This package only ever **reads** context files; it never writes them. Manage
107
+ contexts with the CLI (`nats context add|select|edit|ls|rm`). Files written by
108
+ this library's format are byte-compatible with what the CLI expects.
109
+
110
+ ## License
111
+
112
+ Apache-2.0.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.10,<0.11"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "natsio-natscontext"
7
+ version = "0.1.0"
8
+ description = "Load `nats` CLI context files (ADR-21) into natsio connect options"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.13"
12
+ authors = [{ name = "Corrupt Mane", email = "corruptmane@gmail.com" }]
13
+ keywords = ["nats", "nats.io", "natscontext", "nats-cli", "configuration"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: AsyncIO",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Typing :: Typed",
21
+ ]
22
+ dependencies = ["natsio"]
23
+
24
+ [tool.uv.sources]
25
+ natsio = { workspace = true }
26
+
27
+ [tool.uv.build-backend]
28
+ module-name = "natsio.natscontext"
29
+ namespace = true
@@ -0,0 +1,356 @@
1
+ """natsio.natscontext — load ``nats`` CLI context files (ADR-21) into natsio.
2
+
3
+ The `nats` command-line tool stores named connection profiles as JSON under
4
+ ``$XDG_CONFIG_HOME/nats/context/<name>.json`` (falling back to
5
+ ``~/.config/nats``), with the active profile named in a sibling
6
+ ``context.txt``. This extension discovers those files and maps their fields
7
+ onto `natsio.connect` options.
8
+
9
+ import natsio.natscontext as natscontext
10
+
11
+ ctx = natscontext.load("staging") # or load() for the selected one
12
+ async with await natscontext.connect("staging") as nc:
13
+ ...
14
+
15
+ This mirrors the semantics of the official Go helper
16
+ ``github.com/synadia-io/orbit.go/natscontext``. Fields the `nats` CLI
17
+ understands but that a natsio connection cannot express (SOCKS proxy,
18
+ JetStream domain/prefixes, ``nsc`` lookups, CLI-only cosmetics) are still
19
+ parsed and preserved on the returned `Context` — see each field's docstring
20
+ and the ``README`` for what is and isn't applied.
21
+ """
22
+
23
+ import json
24
+ import os
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ import natsio
30
+ from natsio.client import Client
31
+ from natsio.errors import NATSError
32
+ from natsio.options import ConnectOptions, TLSConfig
33
+
34
+ __all__ = [
35
+ "Context",
36
+ "ContextError",
37
+ "ContextMalformedError",
38
+ "ContextNotFoundError",
39
+ "config_dir",
40
+ "connect",
41
+ "context_dir",
42
+ "list_contexts",
43
+ "load",
44
+ "selected_context",
45
+ ]
46
+
47
+
48
+ # --------------------------------------------------------------------------- #
49
+ # Errors
50
+ # --------------------------------------------------------------------------- #
51
+ class ContextError(NATSError):
52
+ """A NATS CLI context could not be resolved, read, or applied."""
53
+
54
+
55
+ class ContextNotFoundError(ContextError, FileNotFoundError):
56
+ """The named context (or the active selection) does not exist."""
57
+
58
+
59
+ class ContextMalformedError(ContextError):
60
+ """A context file exists but is not valid JSON / not a JSON object."""
61
+
62
+
63
+ # --------------------------------------------------------------------------- #
64
+ # Discovery (XDG layout, mirroring orbit.go's parentDir/ctxDir/selectedContext)
65
+ # --------------------------------------------------------------------------- #
66
+ def config_dir() -> Path:
67
+ """The ``nats`` config directory: ``$XDG_CONFIG_HOME/nats`` or ``~/.config/nats``."""
68
+ xdg = os.environ.get("XDG_CONFIG_HOME")
69
+ parent = Path(xdg) if xdg else Path.home() / ".config"
70
+ return parent / "nats"
71
+
72
+
73
+ def context_dir() -> Path:
74
+ """The directory holding ``<name>.json`` context files."""
75
+ return config_dir() / "context"
76
+
77
+
78
+ def _selected_file() -> Path:
79
+ return config_dir() / "context.txt"
80
+
81
+
82
+ def selected_context() -> str | None:
83
+ """The name of the active context (from ``context.txt``), or ``None``."""
84
+ try:
85
+ name = _selected_file().read_text(encoding="utf-8").strip()
86
+ except (FileNotFoundError, NotADirectoryError, OSError):
87
+ return None
88
+ return name or None
89
+
90
+
91
+ def list_contexts() -> list[str]:
92
+ """Sorted names of all known contexts (``<name>.json`` files); ``[]`` if none."""
93
+ try:
94
+ entries = os.listdir(context_dir())
95
+ except (FileNotFoundError, NotADirectoryError, OSError):
96
+ return []
97
+ return sorted(name[: -len(".json")] for name in entries if name.endswith(".json"))
98
+
99
+
100
+ def _valid_name(name: str) -> bool:
101
+ # orbit.go validName: non-empty, no traversal, no path separators.
102
+ return bool(name) and ".." not in name and os.sep not in name and (os.altsep or os.sep) not in name
103
+
104
+
105
+ # --------------------------------------------------------------------------- #
106
+ # Path expansion (orbit.go applies ~ and, for creds, $ENV expansion)
107
+ # --------------------------------------------------------------------------- #
108
+ def _expand(path: str) -> str:
109
+ """Expand ``~`` and ``$VAR`` in a filesystem path, as the CLI does."""
110
+ return os.path.expanduser(os.path.expandvars(path)) if path else path
111
+
112
+
113
+ # --------------------------------------------------------------------------- #
114
+ # Context
115
+ # --------------------------------------------------------------------------- #
116
+ # Every JSON key orbit.go's Settings struct understands, so an unknown key can
117
+ # be flagged instead of silently dropped.
118
+ _KNOWN_KEYS = frozenset(
119
+ {
120
+ "name",
121
+ "description",
122
+ "url",
123
+ "socks_proxy",
124
+ "token",
125
+ "user",
126
+ "password",
127
+ "creds",
128
+ "nkey",
129
+ "cert",
130
+ "key",
131
+ "ca",
132
+ "nsc",
133
+ "jetstream_domain",
134
+ "jetstream_api_prefix",
135
+ "jetstream_event_prefix",
136
+ "inbox_prefix",
137
+ "user_jwt",
138
+ "color_scheme",
139
+ "tls_first",
140
+ "windows_cert_store",
141
+ "windows_cert_match_by",
142
+ "windows_cert_match",
143
+ "windows_ca_certs_match",
144
+ }
145
+ )
146
+
147
+
148
+ @dataclass(frozen=True, slots=True)
149
+ class Context:
150
+ """A parsed NATS CLI context.
151
+
152
+ Holds every field the ``nats`` CLI understands. `connect_kwargs` projects
153
+ the *applicable* subset onto `natsio.connect` keyword arguments; the rest
154
+ are preserved here for callers that need them (notably JetStream
155
+ domain/prefixes, which natsio's connection layer does not carry).
156
+ """
157
+
158
+ #: Friendly context name (file stem, or the ``name`` JSON field).
159
+ name: str
160
+ #: Path the context was loaded from, if any.
161
+ path: str | None = None
162
+
163
+ # -- applied to the natsio connection --
164
+ url: str = ""
165
+ user: str = ""
166
+ password: str = field(default="", repr=False)
167
+ token: str = field(default="", repr=False)
168
+ creds: str = ""
169
+ nkey: str = ""
170
+ cert: str = ""
171
+ key: str = ""
172
+ ca: str = ""
173
+ tls_first: bool = False
174
+ inbox_prefix: str = ""
175
+
176
+ # -- parsed & preserved, but NOT applied to the connection (see README) --
177
+ description: str = ""
178
+ socks_proxy: str = ""
179
+ nsc: str = ""
180
+ user_jwt: str = field(default="", repr=False)
181
+ jetstream_domain: str = ""
182
+ jetstream_api_prefix: str = ""
183
+ jetstream_event_prefix: str = ""
184
+ color_scheme: str = ""
185
+ windows_cert_store: str = ""
186
+
187
+ #: The raw decoded JSON, verbatim (including any unknown keys).
188
+ raw: dict[str, Any] = field(default_factory=dict, repr=False)
189
+
190
+ @classmethod
191
+ def from_dict(cls, data: dict[str, Any], *, name: str, path: str | None = None) -> "Context":
192
+ def s(key: str) -> str:
193
+ value = data.get(key, "")
194
+ return value if isinstance(value, str) else ""
195
+
196
+ return cls(
197
+ name=data.get("name") or name,
198
+ path=path,
199
+ url=s("url"),
200
+ user=s("user"),
201
+ password=s("password"),
202
+ token=s("token"),
203
+ creds=s("creds"),
204
+ nkey=s("nkey"),
205
+ cert=s("cert"),
206
+ key=s("key"),
207
+ ca=s("ca"),
208
+ tls_first=bool(data.get("tls_first", False)),
209
+ inbox_prefix=s("inbox_prefix"),
210
+ description=s("description"),
211
+ socks_proxy=s("socks_proxy"),
212
+ nsc=s("nsc"),
213
+ user_jwt=s("user_jwt"),
214
+ jetstream_domain=s("jetstream_domain"),
215
+ jetstream_api_prefix=s("jetstream_api_prefix"),
216
+ jetstream_event_prefix=s("jetstream_event_prefix"),
217
+ color_scheme=s("color_scheme"),
218
+ windows_cert_store=s("windows_cert_store"),
219
+ raw=data,
220
+ )
221
+
222
+ @property
223
+ def unknown_keys(self) -> frozenset[str]:
224
+ """JSON keys present in the file that this mapper does not recognise."""
225
+ return frozenset(self.raw) - _KNOWN_KEYS
226
+
227
+ def _build_tls(self) -> TLSConfig | None:
228
+ if not (self.cert or self.key or self.ca or self.tls_first):
229
+ return None
230
+ # orbit.go applies a client cert only when *both* cert and key are set;
231
+ # a half-configured pair is a config error rather than a silent drop.
232
+ if bool(self.cert) != bool(self.key):
233
+ raise ContextError("context sets only one of 'cert'/'key'; both are required for a client certificate")
234
+ # natsio's TLSConfig loads the PEM files itself (lazily, per reconnect).
235
+ return TLSConfig(
236
+ certfile=_expand(self.cert) or None,
237
+ keyfile=_expand(self.key) or None,
238
+ cafile=_expand(self.ca) or None,
239
+ handshake_first=self.tls_first,
240
+ )
241
+
242
+ def connect_kwargs(self) -> dict[str, Any]:
243
+ """Project this context onto `natsio.connect` keyword arguments.
244
+
245
+ Raises `ContextError` for settings natsio cannot honour (a Windows
246
+ certificate store, or a half-specified client certificate).
247
+ """
248
+ if self.windows_cert_store:
249
+ raise ContextError("windows certificate stores are not supported")
250
+
251
+ kwargs: dict[str, Any] = {}
252
+
253
+ if self.url:
254
+ # A context may hold a comma-separated server list (nats.Connect
255
+ # splits on ',' too).
256
+ kwargs["servers"] = tuple(part.strip() for part in self.url.split(",") if part.strip())
257
+
258
+ # Auth precedence mirrors orbit.go's switch (user > creds > nkey), with
259
+ # token as the final fallback. natsio forbids combining mechanisms, so —
260
+ # unlike nats.go, which would additionally send a token alongside these —
261
+ # a token set together with another mechanism is ignored (see README).
262
+ if self.user:
263
+ kwargs["user"] = self.user
264
+ kwargs["password"] = self.password
265
+ elif self.creds:
266
+ kwargs["credentials"] = _expand(self.creds)
267
+ elif self.nkey:
268
+ # natsio's NKeyFileAuth reads and parses the seed file itself,
269
+ # re-reading on every reconnect (rotation) instead of once at load.
270
+ kwargs["nkey_file"] = _expand(self.nkey)
271
+ elif self.token:
272
+ kwargs["token"] = self.token
273
+
274
+ tls = self._build_tls()
275
+ if tls is not None:
276
+ kwargs["tls"] = tls
277
+
278
+ if self.inbox_prefix:
279
+ kwargs["inbox_prefix"] = self.inbox_prefix
280
+
281
+ return kwargs
282
+
283
+
284
+ # --------------------------------------------------------------------------- #
285
+ # Loading
286
+ # --------------------------------------------------------------------------- #
287
+ def _load_file(path: Path, *, name: str) -> Context:
288
+ try:
289
+ content = path.read_text(encoding="utf-8")
290
+ except FileNotFoundError as exc:
291
+ raise ContextNotFoundError(f"context file not found: {path}") from exc
292
+ except OSError as exc:
293
+ raise ContextError(f"cannot read context file {path}: {exc}") from exc
294
+ try:
295
+ data = json.loads(content)
296
+ except json.JSONDecodeError as exc:
297
+ raise ContextMalformedError(f"context file {path} is not valid JSON: {exc}") from exc
298
+ if not isinstance(data, dict):
299
+ raise ContextMalformedError(f"context file {path} must contain a JSON object")
300
+ return Context.from_dict(data, name=name, path=str(path))
301
+
302
+
303
+ def load(name: str | None = None) -> Context:
304
+ """Load a context by ``name``, or the selected context when ``name`` is omitted.
305
+
306
+ If ``name`` is an absolute path to a JSON file, that file is loaded directly
307
+ (its stem becomes the context name). Otherwise ``name`` is resolved under
308
+ the XDG context directory; when omitted it falls back to the active
309
+ selection from ``context.txt``.
310
+
311
+ Raises `ContextNotFoundError` when no name/selection is available or the
312
+ named context does not exist, and `ContextMalformedError` for invalid JSON.
313
+ """
314
+ if name and os.path.isabs(name):
315
+ path = Path(name)
316
+ return _load_file(path, name=path.stem)
317
+
318
+ resolved = name or selected_context()
319
+ if not resolved:
320
+ raise ContextNotFoundError(
321
+ "no context name given and no context is selected "
322
+ f"(set one with `nats context select`, or create {_selected_file()})"
323
+ )
324
+ if not _valid_name(resolved):
325
+ raise ContextNotFoundError(f"invalid context name: {resolved!r}")
326
+
327
+ path = context_dir() / f"{resolved}.json"
328
+ if not path.exists():
329
+ raise ContextNotFoundError(f"unknown context {resolved!r} (looked in {context_dir()})")
330
+ return _load_file(path, name=resolved)
331
+
332
+
333
+ # --------------------------------------------------------------------------- #
334
+ # Convenience connect
335
+ # --------------------------------------------------------------------------- #
336
+ async def connect(
337
+ name: str | None = None,
338
+ /,
339
+ *,
340
+ options: ConnectOptions | None = None,
341
+ error_cb: Any = None,
342
+ **overrides: Any,
343
+ ) -> Client:
344
+ """Load a context and connect with it.
345
+
346
+ The context selector is **positional-only** so it does not collide with
347
+ natsio's ``name`` connect kwarg (the client name). ``overrides`` (and an
348
+ ``options=`` base) are forwarded to `natsio.connect` and take precedence
349
+ over the context-derived settings — e.g. ``await connect("staging",
350
+ name="my-app")`` connects with context ``staging`` and client name
351
+ ``my-app``.
352
+ """
353
+ ctx = load(name)
354
+ kwargs = ctx.connect_kwargs()
355
+ kwargs.update(overrides)
356
+ return await natsio.connect(options=options, error_cb=error_cb, **kwargs)