Skip to content

Commit 64722b5

Browse files
authored
feat: connect() falls back to CAPISCIO_API_KEY env var (#62)
* feat: connect() falls back to CAPISCIO_API_KEY env var CapiscIO.connect() now reads api_key, agent_id, and server_url from environment variables when not passed explicitly: export CAPISCIO_API_KEY=sk_live_... agent = CapiscIO.connect() # just works This makes the zero-argument call the simplest path for users who have already set their env vars. from_env() still exists for the additional CAPISCIO_AGENT_NAME and CAPISCIO_DEV_MODE env vars. * fix: handle empty CAPISCIO_SERVER_URL env var + add connect() env fallback tests - Empty/whitespace CAPISCIO_SERVER_URL now falls back to PROD_REGISTRY instead of passing empty string to httpx - Added 4 unit tests for connect() env var fallback behavior: reads from env, raises when missing, handles empty server_url, reads custom server_url
1 parent eb637f3 commit 64722b5

2 files changed

Lines changed: 67 additions & 24 deletions

File tree

capiscio_sdk/connect.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,11 @@ class CapiscIO:
263263
@classmethod
264264
def connect(
265265
cls,
266-
api_key: str,
266+
api_key: Optional[str] = None,
267267
*,
268268
name: Optional[str] = None,
269269
agent_id: Optional[str] = None,
270-
server_url: str = PROD_REGISTRY,
270+
server_url: Optional[str] = None,
271271
keys_dir: Optional[Path] = None,
272272
auto_badge: bool = True,
273273
dev_mode: bool = False,
@@ -285,10 +285,13 @@ def connect(
285285
5. Sets up auto-renewal
286286
287287
Args:
288-
api_key: Your CapiscIO API key (sk_live_... or sk_test_...)
288+
api_key: Your CapiscIO API key (sk_live_... or sk_test_...).
289+
Falls back to CAPISCIO_API_KEY env var if not provided.
289290
name: Agent name (auto-generated if omitted)
290-
agent_id: Specific agent ID to use (auto-discovered if omitted)
291-
server_url: Registry server URL (default: production)
291+
agent_id: Specific agent ID to use (auto-discovered if omitted).
292+
Falls back to CAPISCIO_AGENT_ID env var if not provided.
293+
server_url: Registry server URL.
294+
Falls back to CAPISCIO_SERVER_URL env var, then production default.
292295
keys_dir: Directory for keys (default: ~/.capiscio/keys/{agent_id}/)
293296
auto_badge: Whether to automatically request a badge
294297
dev_mode: Use self-signed badges (Trust Level 0)
@@ -299,10 +302,28 @@ def connect(
299302
AgentIdentity with full credentials and methods
300303
301304
Example:
305+
# Explicit API key
302306
agent = CapiscIO.connect(api_key="sk_live_abc123")
307+
308+
# From environment (CAPISCIO_API_KEY)
309+
agent = CapiscIO.connect()
310+
303311
print(f"Agent DID: {agent.did}")
304312
agent.emit("agent_started", {"version": "1.0"})
305313
"""
314+
if api_key is None:
315+
api_key = os.environ.get("CAPISCIO_API_KEY")
316+
if not api_key:
317+
raise ValueError(
318+
"api_key is required. Pass it directly or set the "
319+
"CAPISCIO_API_KEY environment variable. "
320+
"Get your API key at https://app.capisc.io"
321+
)
322+
if agent_id is None:
323+
agent_id = os.environ.get("CAPISCIO_AGENT_ID")
324+
if server_url is None:
325+
server_url = os.environ.get("CAPISCIO_SERVER_URL") or PROD_REGISTRY
326+
306327
connector = _Connector(
307328
api_key=api_key,
308329
name=name,
@@ -329,19 +350,13 @@ def from_env(cls, **kwargs) -> AgentIdentity:
329350
- CAPISCIO_DEV_MODE (optional, default: false)
330351
- CAPISCIO_AGENT_PRIVATE_KEY_JWK (optional — JSON-encoded Ed25519
331352
private JWK for ephemeral environments; printed on first generation)
332-
"""
333-
api_key = os.environ.get("CAPISCIO_API_KEY")
334-
if not api_key:
335-
raise ValueError(
336-
"CAPISCIO_API_KEY environment variable is required. "
337-
"Get your API key at https://app.capisc.io"
338-
)
339353
354+
Note: connect() also reads CAPISCIO_API_KEY, CAPISCIO_AGENT_ID, and
355+
CAPISCIO_SERVER_URL from the environment when not passed explicitly.
356+
from_env() additionally reads CAPISCIO_AGENT_NAME and CAPISCIO_DEV_MODE.
357+
"""
340358
return cls.connect(
341-
api_key=api_key,
342-
agent_id=os.environ.get("CAPISCIO_AGENT_ID"),
343359
name=os.environ.get("CAPISCIO_AGENT_NAME"),
344-
server_url=os.environ.get("CAPISCIO_SERVER_URL", PROD_REGISTRY),
345360
dev_mode=os.environ.get("CAPISCIO_DEV_MODE", "").lower() in ("true", "1", "yes"),
346361
**kwargs,
347362
)

tests/unit/test_connect.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,42 @@ def test_connect_calls_connector(self):
258258
mock_connect.assert_called_once()
259259
assert result == mock_identity
260260

261-
262-
class TestCapiscIOFromEnv:
261+
def test_connect_reads_api_key_from_env(self):
262+
"""Test connect() reads CAPISCIO_API_KEY from env when not passed."""
263+
with patch.dict(os.environ, {"CAPISCIO_API_KEY": "sk_from_env"}, clear=False):
264+
with patch.object(_Connector, "__init__", return_value=None) as mock_init:
265+
with patch.object(_Connector, "connect", return_value=MagicMock()):
266+
CapiscIO.connect()
267+
call_kwargs = mock_init.call_args[1]
268+
assert call_kwargs["api_key"] == "sk_from_env"
269+
270+
def test_connect_raises_without_api_key(self):
271+
"""Test connect() raises ValueError when no api_key and no env var."""
272+
with patch.dict(os.environ, {}, clear=False):
273+
os.environ.pop("CAPISCIO_API_KEY", None)
274+
with pytest.raises(ValueError, match="api_key is required"):
275+
CapiscIO.connect()
276+
277+
def test_connect_server_url_falls_back_to_prod(self):
278+
"""Test connect() uses PROD_REGISTRY when CAPISCIO_SERVER_URL is empty."""
279+
with patch.dict(os.environ, {"CAPISCIO_API_KEY": "sk_test", "CAPISCIO_SERVER_URL": ""}, clear=False):
280+
with patch.object(_Connector, "__init__", return_value=None) as mock_init:
281+
with patch.object(_Connector, "connect", return_value=MagicMock()):
282+
CapiscIO.connect()
283+
call_kwargs = mock_init.call_args[1]
284+
assert call_kwargs["server_url"] == PROD_REGISTRY
285+
286+
def test_connect_reads_server_url_from_env(self):
287+
"""Test connect() reads CAPISCIO_SERVER_URL from env."""
288+
with patch.dict(os.environ, {
289+
"CAPISCIO_API_KEY": "sk_test",
290+
"CAPISCIO_SERVER_URL": "https://custom.server.io",
291+
}, clear=False):
292+
with patch.object(_Connector, "__init__", return_value=None) as mock_init:
293+
with patch.object(_Connector, "connect", return_value=MagicMock()):
294+
CapiscIO.connect()
295+
call_kwargs = mock_init.call_args[1]
296+
assert call_kwargs["server_url"] == "https://custom.server.io"
263297
"""Tests for CapiscIO.from_env() class method."""
264298

265299
def test_from_env_requires_api_key(self):
@@ -268,7 +302,7 @@ def test_from_env_requires_api_key(self):
268302
# Remove the key if it exists
269303
os.environ.pop("CAPISCIO_API_KEY", None)
270304

271-
with pytest.raises(ValueError, match="CAPISCIO_API_KEY environment variable is required"):
305+
with pytest.raises(ValueError, match="CAPISCIO_API_KEY"):
272306
CapiscIO.from_env()
273307

274308
def test_from_env_reads_env_vars(self):
@@ -288,10 +322,7 @@ def test_from_env_reads_env_vars(self):
288322
CapiscIO.from_env()
289323

290324
mock_connect.assert_called_once_with(
291-
api_key="sk_test_env",
292-
agent_id="env-agent-id",
293325
name="Env Agent",
294-
server_url="https://env.server.com",
295326
dev_mode=True,
296327
)
297328

@@ -308,10 +339,7 @@ def test_from_env_defaults(self):
308339
CapiscIO.from_env()
309340

310341
mock_connect.assert_called_once_with(
311-
api_key="sk_test_only",
312-
agent_id=None,
313342
name=None,
314-
server_url=PROD_REGISTRY,
315343
dev_mode=False,
316344
)
317345

0 commit comments

Comments
 (0)