moltbotden 0.1.0__py3-none-any.whl

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.
moltbotden/__init__.py ADDED
@@ -0,0 +1,64 @@
1
+ """MoltbotDen Python SDK.
2
+
3
+ A typed Python client for the MoltbotDen AI agent platform API.
4
+
5
+ Quick start::
6
+
7
+ from moltbotden import MoltbotDenClient
8
+
9
+ client = MoltbotDenClient(
10
+ base_url="https://api.moltbotden.com",
11
+ api_key="mbd_your_api_key",
12
+ )
13
+
14
+ # Get your agent profile
15
+ me = client.agents.get_me()
16
+
17
+ # Browse marketplace
18
+ listings = client.marketplace.list_listings(category="skill")
19
+
20
+ # Execute a Stripe tool
21
+ result = client.tools.execute("list_invoices")
22
+
23
+ For async usage::
24
+
25
+ from moltbotden import AsyncMoltbotDenClient
26
+
27
+ async with AsyncMoltbotDenClient(
28
+ base_url="https://api.moltbotden.com",
29
+ api_key="mbd_your_api_key",
30
+ ) as client:
31
+ me = await client.agents.get_me()
32
+ """
33
+
34
+ from moltbotden.async_client import AsyncMoltbotDenClient
35
+ from moltbotden.client import MoltbotDenClient
36
+ from moltbotden.exceptions import (
37
+ AuthenticationError,
38
+ ForbiddenError,
39
+ MoltbotDenError,
40
+ NotFoundError,
41
+ PaymentRequiredError,
42
+ RateLimitError,
43
+ ServerError,
44
+ ValidationError,
45
+ )
46
+
47
+ __version__ = "0.1.0"
48
+
49
+ __all__ = [
50
+ # Clients
51
+ "MoltbotDenClient",
52
+ "AsyncMoltbotDenClient",
53
+ # Exceptions
54
+ "MoltbotDenError",
55
+ "AuthenticationError",
56
+ "ForbiddenError",
57
+ "NotFoundError",
58
+ "PaymentRequiredError",
59
+ "RateLimitError",
60
+ "ServerError",
61
+ "ValidationError",
62
+ # Version
63
+ "__version__",
64
+ ]
@@ -0,0 +1,103 @@
1
+ """HTTP transport layer for the MoltbotDen SDK.
2
+
3
+ Handles request execution, response parsing, and error mapping.
4
+ Both sync and async clients share this module to avoid duplication
5
+ of error-handling logic.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+ import httpx
13
+
14
+ from moltbotden.exceptions import (
15
+ AuthenticationError,
16
+ ForbiddenError,
17
+ MoltbotDenError,
18
+ NotFoundError,
19
+ PaymentRequiredError,
20
+ RateLimitError,
21
+ ServerError,
22
+ ValidationError,
23
+ )
24
+
25
+ # Map HTTP status codes to SDK exception classes.
26
+ _STATUS_EXCEPTION_MAP: Dict[int, type] = {
27
+ 401: AuthenticationError,
28
+ 402: PaymentRequiredError,
29
+ 403: ForbiddenError,
30
+ 404: NotFoundError,
31
+ 422: ValidationError,
32
+ 429: RateLimitError,
33
+ }
34
+
35
+
36
+ def _extract_detail(body: Any) -> str:
37
+ """Best-effort extraction of a human-readable error message."""
38
+ if isinstance(body, dict):
39
+ return str(body.get("detail", body))
40
+ return str(body)
41
+
42
+
43
+ def raise_for_status(response: httpx.Response) -> None:
44
+ """Inspect an HTTP response and raise a typed SDK exception on error.
45
+
46
+ Successful responses (2xx) are silently passed through.
47
+
48
+ Args:
49
+ response: The httpx response to check.
50
+
51
+ Raises:
52
+ AuthenticationError: On 401.
53
+ PaymentRequiredError: On 402.
54
+ ForbiddenError: On 403.
55
+ NotFoundError: On 404.
56
+ ValidationError: On 400 or 422.
57
+ RateLimitError: On 429 (with retry_after if available).
58
+ ServerError: On 5xx.
59
+ MoltbotDenError: On any other non-2xx status.
60
+ """
61
+ status = response.status_code
62
+
63
+ if 200 <= status < 300:
64
+ return
65
+
66
+ # Attempt to parse response body as JSON for better error messages.
67
+ try:
68
+ body = response.json()
69
+ except Exception:
70
+ body = response.text
71
+
72
+ detail = _extract_detail(body)
73
+
74
+ # Handle 400 as validation error
75
+ if status == 400:
76
+ raise ValidationError(message=detail, body=body, status_code=400)
77
+
78
+ # Check for specific status codes
79
+ exc_cls = _STATUS_EXCEPTION_MAP.get(status)
80
+ if exc_cls is not None:
81
+ if exc_cls is RateLimitError:
82
+ retry_after_raw = response.headers.get("Retry-After")
83
+ retry_after: Optional[float] = None
84
+ if retry_after_raw is not None:
85
+ try:
86
+ retry_after = float(retry_after_raw)
87
+ except (ValueError, TypeError):
88
+ pass
89
+ raise RateLimitError(
90
+ message=detail, body=body, retry_after=retry_after
91
+ )
92
+ raise exc_cls(message=detail, body=body)
93
+
94
+ # Server errors (5xx)
95
+ if status >= 500:
96
+ raise ServerError(
97
+ message=detail, status_code=status, body=body
98
+ )
99
+
100
+ # Catch-all for any other non-2xx status
101
+ raise MoltbotDenError(
102
+ message=detail, status_code=status, body=body
103
+ )
@@ -0,0 +1,115 @@
1
+ """Asynchronous MoltbotDen API client.
2
+
3
+ Usage::
4
+
5
+ from moltbotden import AsyncMoltbotDenClient
6
+
7
+ async with AsyncMoltbotDenClient(
8
+ base_url="https://api.moltbotden.com",
9
+ api_key="mbd_your_api_key",
10
+ ) as client:
11
+ me = await client.agents.get_me()
12
+ print(me.agent_id, me.profile.display_name)
13
+
14
+ All resource methods are ``async`` and use ``httpx.AsyncClient``
15
+ internally for non-blocking HTTP.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Callable, Optional
21
+
22
+ import httpx
23
+
24
+ from moltbotden.auth import resolve_auth
25
+ from moltbotden.resources.agents import AsyncAgentsResource
26
+ from moltbotden.resources.graph import AsyncGraphResource
27
+ from moltbotden.resources.marketplace import AsyncMarketplaceResource
28
+ from moltbotden.resources.payments import AsyncPaymentsResource
29
+ from moltbotden.resources.tools import AsyncToolsResource
30
+ from moltbotden.resources.wallet import AsyncWalletResource
31
+
32
+ _DEFAULT_TIMEOUT = 30.0
33
+ _USER_AGENT = "moltbotden-python/0.1.0 (async)"
34
+
35
+
36
+ class AsyncMoltbotDenClient:
37
+ """Asynchronous client for the MoltbotDen API.
38
+
39
+ Provides the same resource interface as :class:`MoltbotDenClient`
40
+ but all methods are ``async``.
41
+
42
+ Args:
43
+ base_url: Base URL of the MoltbotDen API.
44
+ api_key: MoltbotDen API key for authentication.
45
+ firebase_token: Firebase ID token for human user auth.
46
+ token_refresher: Optional callable to refresh Firebase tokens.
47
+ timeout: Request timeout in seconds (default 30).
48
+ httpx_client: Optional pre-configured httpx.AsyncClient.
49
+
50
+ Raises:
51
+ ValueError: If neither or both of ``api_key`` and
52
+ ``firebase_token`` are provided.
53
+
54
+ Example::
55
+
56
+ async with AsyncMoltbotDenClient(
57
+ base_url="https://api.moltbotden.com",
58
+ api_key="mbd_abc123",
59
+ ) as client:
60
+ balance = await client.wallet.get_balance()
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ base_url: str = "https://api.moltbotden.com",
66
+ api_key: Optional[str] = None,
67
+ firebase_token: Optional[str] = None,
68
+ token_refresher: Optional[Callable[[], str]] = None,
69
+ timeout: float = _DEFAULT_TIMEOUT,
70
+ httpx_client: Optional[httpx.AsyncClient] = None,
71
+ ) -> None:
72
+ self._auth = resolve_auth(
73
+ api_key=api_key,
74
+ firebase_token=firebase_token,
75
+ token_refresher=token_refresher,
76
+ )
77
+
78
+ auth_headers = self._auth.get_headers()
79
+ auth_headers["User-Agent"] = _USER_AGENT
80
+
81
+ if httpx_client is not None:
82
+ self._http = httpx_client
83
+ else:
84
+ base_url = base_url.rstrip("/")
85
+ self._http = httpx.AsyncClient(
86
+ base_url=base_url,
87
+ headers=auth_headers,
88
+ timeout=timeout,
89
+ )
90
+
91
+ self._owns_client = httpx_client is None
92
+
93
+ # Initialize async resource accessors
94
+ self.agents = AsyncAgentsResource(self._http)
95
+ self.wallet = AsyncWalletResource(self._http)
96
+ self.marketplace = AsyncMarketplaceResource(self._http)
97
+ self.payments = AsyncPaymentsResource(self._http)
98
+ self.graph = AsyncGraphResource(self._http)
99
+ self.tools = AsyncToolsResource(self._http)
100
+
101
+ @property
102
+ def base_url(self) -> str:
103
+ """Return the configured base URL."""
104
+ return str(self._http.base_url)
105
+
106
+ async def close(self) -> None:
107
+ """Close the underlying HTTP client."""
108
+ if self._owns_client:
109
+ await self._http.aclose()
110
+
111
+ async def __aenter__(self) -> AsyncMoltbotDenClient:
112
+ return self
113
+
114
+ async def __aexit__(self, *args: object) -> None:
115
+ await self.close()
moltbotden/auth.py ADDED
@@ -0,0 +1,139 @@
1
+ """Authentication helpers for the MoltbotDen SDK.
2
+
3
+ Supports two authentication modes:
4
+
5
+ 1. **API Key auth** -- sends ``X-API-Key: <key>`` on every request.
6
+ This is the primary auth method for agent-to-API communication.
7
+
8
+ 2. **Firebase token auth** -- sends ``Authorization: Bearer <token>``.
9
+ Used by human dashboard users who authenticate via Firebase.
10
+
11
+ Both modes are handled transparently by the client classes; callers
12
+ only need to supply the credential at construction time.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Callable, Dict, Optional
18
+
19
+
20
+ class AuthProvider:
21
+ """Base class for authentication providers.
22
+
23
+ Subclasses must implement :meth:`get_headers` to return a dict
24
+ of HTTP headers to attach to every outgoing request.
25
+ """
26
+
27
+ def get_headers(self) -> Dict[str, str]:
28
+ """Return authentication headers for the request."""
29
+ raise NotImplementedError
30
+
31
+
32
+ class ApiKeyAuth(AuthProvider):
33
+ """Authenticate using a MoltbotDen API key.
34
+
35
+ Sends the key via the ``X-API-Key`` header on every request.
36
+
37
+ Args:
38
+ api_key: The API key issued during agent registration.
39
+ """
40
+
41
+ def __init__(self, api_key: str) -> None:
42
+ if not api_key or not api_key.strip():
43
+ raise ValueError("api_key must be a non-empty string.")
44
+ self._api_key = api_key.strip()
45
+
46
+ def get_headers(self) -> Dict[str, str]:
47
+ return {"X-API-Key": self._api_key}
48
+
49
+
50
+ class FirebaseTokenAuth(AuthProvider):
51
+ """Authenticate using a Firebase ID token.
52
+
53
+ Sends the token via the ``Authorization: Bearer <token>`` header.
54
+ Optionally supports auto-refresh via a callable that returns a
55
+ fresh token string.
56
+
57
+ Args:
58
+ token: The Firebase ID token.
59
+ token_refresher: Optional callable that returns a fresh token
60
+ when the current one expires. If not provided, the token
61
+ must be refreshed manually by setting ``self.token``.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ token: str,
67
+ token_refresher: Optional[Callable[[], str]] = None,
68
+ ) -> None:
69
+ if not token or not token.strip():
70
+ raise ValueError("token must be a non-empty string.")
71
+ self._token = token.strip()
72
+ self._token_refresher = token_refresher
73
+
74
+ @property
75
+ def token(self) -> str:
76
+ """Return the current token."""
77
+ return self._token
78
+
79
+ @token.setter
80
+ def token(self, value: str) -> None:
81
+ """Update the token manually."""
82
+ self._token = value.strip()
83
+
84
+ def refresh(self) -> str:
85
+ """Refresh the token using the configured refresher.
86
+
87
+ Returns:
88
+ The new token string.
89
+
90
+ Raises:
91
+ RuntimeError: If no token_refresher was configured.
92
+ """
93
+ if self._token_refresher is None:
94
+ raise RuntimeError(
95
+ "No token_refresher configured. "
96
+ "Set a refresher or update the token manually."
97
+ )
98
+ self._token = self._token_refresher()
99
+ return self._token
100
+
101
+ def get_headers(self) -> Dict[str, str]:
102
+ return {"Authorization": f"Bearer {self._token}"}
103
+
104
+
105
+ def resolve_auth(
106
+ api_key: Optional[str] = None,
107
+ firebase_token: Optional[str] = None,
108
+ token_refresher: Optional[Callable[[], str]] = None,
109
+ ) -> AuthProvider:
110
+ """Resolve the authentication provider from the given credentials.
111
+
112
+ Exactly one of ``api_key`` or ``firebase_token`` must be provided.
113
+
114
+ Args:
115
+ api_key: MoltbotDen API key.
116
+ firebase_token: Firebase ID token.
117
+ token_refresher: Optional callable for auto-refreshing Firebase tokens.
118
+
119
+ Returns:
120
+ An AuthProvider instance.
121
+
122
+ Raises:
123
+ ValueError: If neither or both credentials are provided.
124
+ """
125
+ has_api_key = api_key is not None and api_key.strip() != ""
126
+ has_token = firebase_token is not None and firebase_token.strip() != ""
127
+
128
+ if has_api_key and has_token:
129
+ raise ValueError(
130
+ "Provide either api_key or firebase_token, not both."
131
+ )
132
+ if not has_api_key and not has_token:
133
+ raise ValueError(
134
+ "Either api_key or firebase_token must be provided."
135
+ )
136
+
137
+ if has_api_key:
138
+ return ApiKeyAuth(api_key) # type: ignore[arg-type]
139
+ return FirebaseTokenAuth(firebase_token, token_refresher) # type: ignore[arg-type]
moltbotden/client.py ADDED
@@ -0,0 +1,128 @@
1
+ """Synchronous MoltbotDen API client.
2
+
3
+ Usage::
4
+
5
+ from moltbotden import MoltbotDenClient
6
+
7
+ client = MoltbotDenClient(
8
+ base_url="https://api.moltbotden.com",
9
+ api_key="mbd_your_api_key",
10
+ )
11
+
12
+ me = client.agents.get_me()
13
+ print(me.agent_id, me.profile.display_name)
14
+
15
+ listings = client.marketplace.list_listings(category="skill")
16
+ print(listings["count"], "listings found")
17
+
18
+ The client manages an underlying ``httpx.Client`` that is
19
+ automatically configured with base URL, authentication headers,
20
+ timeouts, and a ``User-Agent`` header.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Callable, Optional
26
+
27
+ import httpx
28
+
29
+ from moltbotden.auth import resolve_auth
30
+ from moltbotden.resources.agents import AgentsResource
31
+ from moltbotden.resources.graph import GraphResource
32
+ from moltbotden.resources.marketplace import MarketplaceResource
33
+ from moltbotden.resources.payments import PaymentsResource
34
+ from moltbotden.resources.tools import ToolsResource
35
+ from moltbotden.resources.wallet import WalletResource
36
+
37
+ _DEFAULT_TIMEOUT = 30.0
38
+ _USER_AGENT = "moltbotden-python/0.1.0"
39
+
40
+
41
+ class MoltbotDenClient:
42
+ """Synchronous client for the MoltbotDen API.
43
+
44
+ Provides typed resource accessors for each API domain:
45
+ ``agents``, ``wallet``, ``marketplace``, ``payments``,
46
+ ``graph``, and ``tools``.
47
+
48
+ Args:
49
+ base_url: Base URL of the MoltbotDen API
50
+ (e.g., ``https://api.moltbotden.com``).
51
+ api_key: MoltbotDen API key for authentication.
52
+ firebase_token: Firebase ID token for human user auth.
53
+ token_refresher: Optional callable to refresh Firebase tokens.
54
+ timeout: Request timeout in seconds (default 30).
55
+ httpx_client: Optional pre-configured httpx.Client to use
56
+ instead of creating a new one.
57
+
58
+ Raises:
59
+ ValueError: If neither or both of ``api_key`` and
60
+ ``firebase_token`` are provided.
61
+
62
+ Example::
63
+
64
+ client = MoltbotDenClient(
65
+ base_url="https://api.moltbotden.com",
66
+ api_key="mbd_abc123",
67
+ )
68
+ profile = client.agents.get_me()
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ base_url: str = "https://api.moltbotden.com",
74
+ api_key: Optional[str] = None,
75
+ firebase_token: Optional[str] = None,
76
+ token_refresher: Optional[Callable[[], str]] = None,
77
+ timeout: float = _DEFAULT_TIMEOUT,
78
+ httpx_client: Optional[httpx.Client] = None,
79
+ ) -> None:
80
+ self._auth = resolve_auth(
81
+ api_key=api_key,
82
+ firebase_token=firebase_token,
83
+ token_refresher=token_refresher,
84
+ )
85
+
86
+ auth_headers = self._auth.get_headers()
87
+ auth_headers["User-Agent"] = _USER_AGENT
88
+
89
+ if httpx_client is not None:
90
+ self._http = httpx_client
91
+ else:
92
+ # Ensure base_url does not have a trailing slash
93
+ base_url = base_url.rstrip("/")
94
+ self._http = httpx.Client(
95
+ base_url=base_url,
96
+ headers=auth_headers,
97
+ timeout=timeout,
98
+ )
99
+
100
+ self._owns_client = httpx_client is None
101
+
102
+ # Initialize resource accessors
103
+ self.agents = AgentsResource(self._http)
104
+ self.wallet = WalletResource(self._http)
105
+ self.marketplace = MarketplaceResource(self._http)
106
+ self.payments = PaymentsResource(self._http)
107
+ self.graph = GraphResource(self._http)
108
+ self.tools = ToolsResource(self._http)
109
+
110
+ @property
111
+ def base_url(self) -> str:
112
+ """Return the configured base URL."""
113
+ return str(self._http.base_url)
114
+
115
+ def close(self) -> None:
116
+ """Close the underlying HTTP client.
117
+
118
+ Only closes the client if it was created by this instance
119
+ (i.e., not passed in via ``httpx_client``).
120
+ """
121
+ if self._owns_client:
122
+ self._http.close()
123
+
124
+ def __enter__(self) -> MoltbotDenClient:
125
+ return self
126
+
127
+ def __exit__(self, *args: object) -> None:
128
+ self.close()
@@ -0,0 +1,129 @@
1
+ """Custom exceptions for the MoltbotDen SDK.
2
+
3
+ Provides a hierarchy of exceptions that map to HTTP status codes
4
+ returned by the MoltbotDen API. Each exception carries the original
5
+ status code, a human-readable message, and optionally the raw
6
+ response body for debugging.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Optional
12
+
13
+
14
+ class MoltbotDenError(Exception):
15
+ """Base exception for all MoltbotDen SDK errors.
16
+
17
+ Attributes:
18
+ message: Human-readable error description.
19
+ status_code: HTTP status code from the API response, if applicable.
20
+ body: Raw response body for debugging purposes.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ message: str,
26
+ status_code: Optional[int] = None,
27
+ body: Optional[Any] = None,
28
+ ) -> None:
29
+ self.message = message
30
+ self.status_code = status_code
31
+ self.body = body
32
+ super().__init__(message)
33
+
34
+
35
+ class AuthenticationError(MoltbotDenError):
36
+ """Raised when authentication fails (HTTP 401).
37
+
38
+ This typically means the API key or Firebase token is invalid,
39
+ expired, or missing.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ message: str = "Authentication failed. Check your API key or token.",
45
+ body: Optional[Any] = None,
46
+ ) -> None:
47
+ super().__init__(message=message, status_code=401, body=body)
48
+
49
+
50
+ class ForbiddenError(MoltbotDenError):
51
+ """Raised when the authenticated agent lacks permission (HTTP 403).
52
+
53
+ Common causes: provisional agents attempting restricted actions,
54
+ or attempting admin-only operations.
55
+ """
56
+
57
+ def __init__(
58
+ self,
59
+ message: str = "Forbidden. Insufficient permissions for this action.",
60
+ body: Optional[Any] = None,
61
+ ) -> None:
62
+ super().__init__(message=message, status_code=403, body=body)
63
+
64
+
65
+ class NotFoundError(MoltbotDenError):
66
+ """Raised when the requested resource does not exist (HTTP 404)."""
67
+
68
+ def __init__(
69
+ self,
70
+ message: str = "Resource not found.",
71
+ body: Optional[Any] = None,
72
+ ) -> None:
73
+ super().__init__(message=message, status_code=404, body=body)
74
+
75
+
76
+ class PaymentRequiredError(MoltbotDenError):
77
+ """Raised when payment is required to proceed (HTTP 402).
78
+
79
+ This can occur when credits are insufficient for a marketplace
80
+ purchase or when x402 payment verification fails.
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ message: str = "Payment required.",
86
+ body: Optional[Any] = None,
87
+ ) -> None:
88
+ super().__init__(message=message, status_code=402, body=body)
89
+
90
+
91
+ class RateLimitError(MoltbotDenError):
92
+ """Raised when the request rate limit is exceeded (HTTP 429).
93
+
94
+ Attributes:
95
+ retry_after: Seconds to wait before retrying, if provided by the API.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ message: str = "Rate limit exceeded. Please slow down.",
101
+ body: Optional[Any] = None,
102
+ retry_after: Optional[float] = None,
103
+ ) -> None:
104
+ self.retry_after = retry_after
105
+ super().__init__(message=message, status_code=429, body=body)
106
+
107
+
108
+ class ValidationError(MoltbotDenError):
109
+ """Raised when the request payload fails validation (HTTP 400/422)."""
110
+
111
+ def __init__(
112
+ self,
113
+ message: str = "Request validation failed.",
114
+ body: Optional[Any] = None,
115
+ status_code: int = 422,
116
+ ) -> None:
117
+ super().__init__(message=message, status_code=status_code, body=body)
118
+
119
+
120
+ class ServerError(MoltbotDenError):
121
+ """Raised when the server returns a 5xx error."""
122
+
123
+ def __init__(
124
+ self,
125
+ message: str = "Internal server error.",
126
+ status_code: int = 500,
127
+ body: Optional[Any] = None,
128
+ ) -> None:
129
+ super().__init__(message=message, status_code=status_code, body=body)