Skip to content

Commit 61e539c

Browse files
authored
Added get_demo_api_key() to SDK (#36)
* Added `get_demo_api_key()` to SDK * Added test for demo api key * Fixed LangChain error message
1 parent 53ed8eb commit 61e539c

File tree

5 files changed

+314
-7
lines changed

5 files changed

+314
-7
lines changed

cyborgdb/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
# Re-export from encrypted_index.py
99
from .client.encrypted_index import EncryptedIndex
1010

11+
# Re-export demo functionality
12+
from .demo import get_demo_api_key
13+
1114
# Try to import LangChain integration (optional dependency)
1215
try:
1316
from .integrations.langchain import CyborgVectorStore
@@ -35,4 +38,5 @@ def __class_getitem__(cls, item):
3538
"IndexIVFPQ",
3639
"IndexIVFFlat",
3740
"CyborgVectorStore",
41+
"get_demo_api_key",
3842
]

cyborgdb/demo.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Demo API key generation for CyborgDB.
3+
4+
This module provides functionality to generate temporary demo API keys
5+
from the CyborgDB demo API service.
6+
"""
7+
8+
from datetime import datetime, timezone, timedelta
9+
import os
10+
import logging
11+
from typing import Optional
12+
import requests
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
def get_demo_api_key(description: Optional[str] = None) -> str:
18+
"""
19+
Generate a temporary demo API key from the CyborgDB demo API service.
20+
21+
This function generates a temporary API key that can be used for demo purposes.
22+
The endpoint can be configured via the CYBORGDB_DEMO_ENDPOINT environment variable.
23+
24+
Args:
25+
description: Optional description for the demo API key.
26+
Defaults to "Temporary demo API key" if not provided.
27+
28+
Returns:
29+
str: The generated demo API key.
30+
31+
Raises:
32+
ValueError: If the demo API key could not be generated.
33+
34+
Example:
35+
>>> import cyborgdb
36+
>>> demo_key = cyborgdb.get_demo_api_key()
37+
>>> client = cyborgdb.Client("https://your-instance.com", demo_key)
38+
"""
39+
40+
# Use environment variable if set, otherwise use default endpoint
41+
endpoint = os.getenv(
42+
"CYBORGDB_DEMO_ENDPOINT",
43+
"https://api.cyborgdb.co/v1/api-key/manage/create-demo-key",
44+
)
45+
46+
# Set default description if not provided
47+
if description is None:
48+
description = "Temporary demo API key"
49+
50+
# Prepare the request payload
51+
payload = {"description": description}
52+
53+
# Prepare headers
54+
headers = {"Content-Type": "application/json", "Accept": "application/json"}
55+
56+
try:
57+
# Make the POST request (no authentication required)
58+
response = requests.post(endpoint, json=payload, headers=headers, timeout=30)
59+
60+
# Check if request was successful
61+
response.raise_for_status()
62+
63+
# Parse the response
64+
data = response.json()
65+
66+
# Extract the API key
67+
api_key = data.get("apiKey", None)
68+
if not api_key:
69+
error_msg = "Demo API key not found in response."
70+
logger.error(error_msg)
71+
raise ValueError(error_msg)
72+
73+
# Log expiration info if available
74+
expires_at = data.get("expiresAt", None)
75+
if expires_at:
76+
# Calculate time left until expiration
77+
expires_at_dt = datetime.fromtimestamp(expires_at, tz=timezone.utc)
78+
now = datetime.now(timezone.utc)
79+
time_left = expires_at_dt - now
80+
81+
# Remove microseconds for cleaner display
82+
time_left = time_left - time_left % timedelta(seconds=1)
83+
logger.info("Demo API key will expire in %s", time_left)
84+
85+
return api_key
86+
87+
except requests.exceptions.RequestException as e:
88+
error_msg = f"Failed to generate demo API key: {e}"
89+
logger.error(error_msg)
90+
raise ValueError(error_msg)

cyborgdb/integrations/langchain.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""
2-
LangChain integration for CyborgDB-py REST API.
2+
LangChain integration for CyborgDB Python SDK.
33
44
This module provides a LangChain VectorStore implementation for CyborgDB,
55
enabling seamless integration with LangChain applications.
66
77
Requirements:
8-
pip install cyborgdb-py[langchain]
8+
pip install cyborgdb[langchain]
99
"""
1010

1111
import json
@@ -954,8 +954,8 @@ def from_documents(
954954

955955
def _missing_dependency_error():
956956
raise ImportError(
957-
f"To use the LangChain integration with cyborgdb-py, "
958-
f"please install the required dependencies: pip install cyborgdb-py[langchain]\n"
957+
f"To use the LangChain integration with cyborgdb, "
958+
f"please install the required dependencies: pip install cyborgdb[langchain]\n"
959959
f"Original error: {_original_error}"
960960
)
961961

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "cyborgdb"
77
dynamic = ["version"]
88
description = "Python Client SDK for CyborgDB: The Confidential Vector Database"
99
readme = "README.md"
10-
requires-python = ">= 3.8"
10+
requires-python = ">= 3.10"
1111
license = {text = "MIT"}
1212
authors = [
1313
{name = "Cyborg Inc."}
@@ -20,18 +20,18 @@ dependencies = [
2020
"typing-extensions >= 4.7.1",
2121
"python-dotenv >= 0.19.0",
2222
"numpy >= 1.21.0",
23+
"requests >= 2.25.0",
2324
]
2425
classifiers = [
2526
"Intended Audience :: Developers",
2627
"Intended Audience :: Information Technology",
2728
"Operating System :: OS Independent",
2829
"Programming Language :: Python :: 3",
29-
"Programming Language :: Python :: 3.8",
30-
"Programming Language :: Python :: 3.9",
3130
"Programming Language :: Python :: 3.10",
3231
"Programming Language :: Python :: 3.11",
3332
"Programming Language :: Python :: 3.12",
3433
"Programming Language :: Python :: 3.13",
34+
"Programming Language :: Python :: 3.14",
3535
"Programming Language :: C++",
3636
"Topic :: Database",
3737
"Topic :: Database :: Database Engines/Servers",

tests/test_demo.py

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import os
2+
import unittest
3+
from unittest.mock import patch, MagicMock
4+
from datetime import datetime, timezone
5+
6+
from cyborgdb.demo import get_demo_api_key
7+
8+
9+
class TestGetDemoApiKey(unittest.TestCase):
10+
"""Unit tests for the get_demo_api_key function."""
11+
12+
def setUp(self):
13+
"""Set up test environment."""
14+
# Store original env var to restore later
15+
self.original_env = os.environ.get("CYBORGDB_DEMO_ENDPOINT")
16+
17+
def tearDown(self):
18+
"""Clean up test environment."""
19+
# Restore original env var
20+
if self.original_env is not None:
21+
os.environ["CYBORGDB_DEMO_ENDPOINT"] = self.original_env
22+
elif "CYBORGDB_DEMO_ENDPOINT" in os.environ:
23+
del os.environ["CYBORGDB_DEMO_ENDPOINT"]
24+
25+
@patch("cyborgdb.demo.requests.post")
26+
def test_get_demo_api_key_success(self, mock_post):
27+
"""Test successful demo API key generation."""
28+
# Mock successful response
29+
mock_response = MagicMock()
30+
mock_response.status_code = 200
31+
mock_response.json.return_value = {
32+
"apiKey": "demo_test_key_12345",
33+
"expiresAt": datetime.now(timezone.utc).timestamp() + 3600,
34+
}
35+
mock_post.return_value = mock_response
36+
37+
# Call the function
38+
api_key = get_demo_api_key()
39+
40+
# Verify the result
41+
self.assertEqual(api_key, "demo_test_key_12345")
42+
43+
# Verify the request was made correctly
44+
mock_post.assert_called_once()
45+
call_args = mock_post.call_args
46+
self.assertEqual(
47+
call_args.kwargs["json"], {"description": "Temporary demo API key"}
48+
)
49+
self.assertEqual(
50+
call_args.kwargs["headers"],
51+
{"Content-Type": "application/json", "Accept": "application/json"},
52+
)
53+
54+
@patch("cyborgdb.demo.requests.post")
55+
def test_get_demo_api_key_with_custom_description(self, mock_post):
56+
"""Test demo API key generation with custom description."""
57+
# Mock successful response
58+
mock_response = MagicMock()
59+
mock_response.status_code = 200
60+
mock_response.json.return_value = {
61+
"apiKey": "demo_test_key_67890",
62+
}
63+
mock_post.return_value = mock_response
64+
65+
# Call the function with custom description
66+
custom_description = "My custom demo key"
67+
api_key = get_demo_api_key(description=custom_description)
68+
69+
# Verify the result
70+
self.assertEqual(api_key, "demo_test_key_67890")
71+
72+
# Verify the custom description was used
73+
call_args = mock_post.call_args
74+
self.assertEqual(call_args.kwargs["json"], {"description": custom_description})
75+
76+
@patch("cyborgdb.demo.requests.post")
77+
def test_get_demo_api_key_uses_default_endpoint(self, mock_post):
78+
"""Test that default endpoint is used when env var is not set."""
79+
# Ensure env var is not set
80+
if "CYBORGDB_DEMO_ENDPOINT" in os.environ:
81+
del os.environ["CYBORGDB_DEMO_ENDPOINT"]
82+
83+
# Mock successful response
84+
mock_response = MagicMock()
85+
mock_response.status_code = 200
86+
mock_response.json.return_value = {
87+
"apiKey": "demo_test_key_default",
88+
}
89+
mock_post.return_value = mock_response
90+
91+
# Call the function
92+
api_key = get_demo_api_key()
93+
94+
# Verify default endpoint was used
95+
call_args = mock_post.call_args
96+
self.assertEqual(
97+
call_args.args[0],
98+
"https://api.cyborgdb.co/v1/api-key/manage/create-demo-key",
99+
)
100+
self.assertEqual(api_key, "demo_test_key_default")
101+
102+
@patch("cyborgdb.demo.requests.post")
103+
def test_get_demo_api_key_uses_env_endpoint(self, mock_post):
104+
"""Test that custom endpoint from env var is used."""
105+
# Set custom endpoint
106+
custom_endpoint = "https://custom.api.example.com/demo-key"
107+
os.environ["CYBORGDB_DEMO_ENDPOINT"] = custom_endpoint
108+
109+
# Mock successful response
110+
mock_response = MagicMock()
111+
mock_response.status_code = 200
112+
mock_response.json.return_value = {
113+
"apiKey": "demo_test_key_custom",
114+
}
115+
mock_post.return_value = mock_response
116+
117+
# Call the function
118+
api_key = get_demo_api_key()
119+
120+
# Verify custom endpoint was used
121+
call_args = mock_post.call_args
122+
self.assertEqual(call_args.args[0], custom_endpoint)
123+
self.assertEqual(api_key, "demo_test_key_custom")
124+
125+
@patch("cyborgdb.demo.requests.post")
126+
def test_get_demo_api_key_missing_api_key_in_response(self, mock_post):
127+
"""Test handling of response missing apiKey field."""
128+
# Mock response without apiKey
129+
mock_response = MagicMock()
130+
mock_response.status_code = 200
131+
mock_response.json.return_value = {
132+
"success": True,
133+
}
134+
mock_post.return_value = mock_response
135+
136+
# Call the function and expect ValueError
137+
with self.assertRaises(ValueError) as context:
138+
get_demo_api_key()
139+
140+
self.assertIn("Demo API key not found in response", str(context.exception))
141+
142+
@patch("cyborgdb.demo.requests.post")
143+
def test_get_demo_api_key_http_error(self, mock_post):
144+
"""Test handling of HTTP errors."""
145+
# Mock HTTP error
146+
import requests
147+
148+
mock_post.side_effect = requests.exceptions.HTTPError("404 Not Found")
149+
150+
# Call the function and expect ValueError
151+
with self.assertRaises(ValueError) as context:
152+
get_demo_api_key()
153+
154+
self.assertIn("Failed to generate demo API key", str(context.exception))
155+
156+
@patch("cyborgdb.demo.requests.post")
157+
def test_get_demo_api_key_connection_error(self, mock_post):
158+
"""Test handling of connection errors."""
159+
# Mock connection error
160+
import requests
161+
162+
mock_post.side_effect = requests.exceptions.ConnectionError(
163+
"Failed to connect"
164+
)
165+
166+
# Call the function and expect ValueError
167+
with self.assertRaises(ValueError) as context:
168+
get_demo_api_key()
169+
170+
self.assertIn("Failed to generate demo API key", str(context.exception))
171+
172+
@patch("cyborgdb.demo.requests.post")
173+
def test_get_demo_api_key_timeout_error(self, mock_post):
174+
"""Test handling of timeout errors."""
175+
# Mock timeout error
176+
import requests
177+
178+
mock_post.side_effect = requests.exceptions.Timeout("Request timed out")
179+
180+
# Call the function and expect ValueError
181+
with self.assertRaises(ValueError) as context:
182+
get_demo_api_key()
183+
184+
self.assertIn("Failed to generate demo API key", str(context.exception))
185+
186+
@patch("cyborgdb.demo.requests.post")
187+
def test_get_demo_api_key_with_expiration_info(self, mock_post):
188+
"""Test that expiration info is logged correctly."""
189+
# Mock successful response with expiration
190+
mock_response = MagicMock()
191+
mock_response.status_code = 200
192+
future_timestamp = datetime.now(timezone.utc).timestamp() + 7200 # 2 hours
193+
mock_response.json.return_value = {
194+
"apiKey": "demo_test_key_expires",
195+
"expiresAt": future_timestamp,
196+
}
197+
mock_post.return_value = mock_response
198+
199+
# Call the function
200+
with self.assertLogs("cyborgdb.demo", level="INFO") as log_context:
201+
api_key = get_demo_api_key()
202+
203+
# Verify the result
204+
self.assertEqual(api_key, "demo_test_key_expires")
205+
206+
# Verify expiration was logged
207+
self.assertTrue(
208+
any("Demo API key will expire in" in log for log in log_context.output)
209+
)
210+
211+
212+
if __name__ == "__main__":
213+
unittest.main()

0 commit comments

Comments
 (0)