magickmind 0.1.1__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.
Files changed (43) hide show
  1. magick_mind/__init__.py +39 -0
  2. magick_mind/auth/__init__.py +9 -0
  3. magick_mind/auth/base.py +46 -0
  4. magick_mind/auth/email_password.py +268 -0
  5. magick_mind/client.py +188 -0
  6. magick_mind/config.py +28 -0
  7. magick_mind/exceptions.py +107 -0
  8. magick_mind/http/__init__.py +5 -0
  9. magick_mind/http/client.py +313 -0
  10. magick_mind/models/__init__.py +17 -0
  11. magick_mind/models/auth.py +30 -0
  12. magick_mind/models/common.py +32 -0
  13. magick_mind/models/errors.py +73 -0
  14. magick_mind/models/v1/__init__.py +83 -0
  15. magick_mind/models/v1/api_keys.py +115 -0
  16. magick_mind/models/v1/artifact.py +151 -0
  17. magick_mind/models/v1/chat.py +104 -0
  18. magick_mind/models/v1/corpus.py +82 -0
  19. magick_mind/models/v1/end_user.py +75 -0
  20. magick_mind/models/v1/history.py +94 -0
  21. magick_mind/models/v1/mindspace.py +130 -0
  22. magick_mind/models/v1/model.py +25 -0
  23. magick_mind/models/v1/project.py +73 -0
  24. magick_mind/realtime/__init__.py +5 -0
  25. magick_mind/realtime/client.py +202 -0
  26. magick_mind/realtime/handler.py +122 -0
  27. magick_mind/resources/README.md +201 -0
  28. magick_mind/resources/__init__.py +42 -0
  29. magick_mind/resources/base.py +31 -0
  30. magick_mind/resources/v1/__init__.py +19 -0
  31. magick_mind/resources/v1/api_keys.py +181 -0
  32. magick_mind/resources/v1/artifact.py +287 -0
  33. magick_mind/resources/v1/chat.py +120 -0
  34. magick_mind/resources/v1/corpus.py +156 -0
  35. magick_mind/resources/v1/end_user.py +181 -0
  36. magick_mind/resources/v1/history.py +88 -0
  37. magick_mind/resources/v1/mindspace.py +331 -0
  38. magick_mind/resources/v1/model.py +19 -0
  39. magick_mind/resources/v1/project.py +155 -0
  40. magick_mind/routes.py +76 -0
  41. magickmind-0.1.1.dist-info/METADATA +593 -0
  42. magickmind-0.1.1.dist-info/RECORD +43 -0
  43. magickmind-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,73 @@
1
+ """
2
+ Project models for Magick Mind SDK v1 API.
3
+
4
+ Mirrors Bifrost's /v1/projects endpoint request/response schemas.
5
+ """
6
+
7
+ from typing import Optional, List
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from magick_mind.models.v1.end_user import PageInfo
12
+
13
+
14
+ class Project(BaseModel):
15
+ """
16
+ Project schema from Bifrost.
17
+
18
+ Represents an agentic SaaS project with associated corpus IDs.
19
+ """
20
+
21
+ model_config = ConfigDict(populate_by_name=True)
22
+
23
+ id: str = Field(..., description="Project ID")
24
+ name: str = Field(..., description="Project name")
25
+ description: Optional[str] = Field(None, description="Project description")
26
+ corpus_ids: Optional[list[str]] = Field(
27
+ None,
28
+ description="List of corpus IDs associated with this project",
29
+ )
30
+ created_by: str = Field(..., description="User ID of creator")
31
+ created_at: str = Field(..., description="Creation timestamp (ISO8601)")
32
+ updated_at: str = Field(..., description="Last update timestamp (ISO8601)")
33
+
34
+
35
+ class CreateProjectRequest(BaseModel):
36
+ """
37
+ Request schema for creating a new project.
38
+ """
39
+
40
+ name: str = Field(..., description="Project name (required)")
41
+ description: str = Field(
42
+ default="", description="Project description (optional, max 256 chars)"
43
+ )
44
+ corpus_ids: list[str] = Field(
45
+ default_factory=list, description="List of corpus IDs to associate with project"
46
+ )
47
+
48
+
49
+ class GetProjectListResponse(BaseModel):
50
+ """
51
+ Response schema for listing projects.
52
+
53
+ Matches Bifrost's {data: list[Project], paging: PageInfo} structure.
54
+ """
55
+
56
+ data: list[Project] = Field(..., description="List of projects")
57
+ paging: PageInfo = Field(..., description="Pagination information")
58
+
59
+
60
+ class UpdateProjectRequest(BaseModel):
61
+ """
62
+ Request schema for updating a project.
63
+
64
+ Both name and corpus_ids are REQUIRED (matching Bifrost API).
65
+ """
66
+
67
+ name: str = Field(..., description="Project name (required)")
68
+ description: Optional[str] = Field(
69
+ None, description="Project description (optional, max 256 chars)"
70
+ )
71
+ corpus_ids: list[str] = Field(
72
+ ..., description="List of corpus IDs to associate with project (required)"
73
+ )
@@ -0,0 +1,5 @@
1
+ """Realtime module for WebSocket connections using Centrifugo."""
2
+
3
+ from .client import RealtimeClient
4
+
5
+ __all__ = ["RealtimeClient"]
@@ -0,0 +1,202 @@
1
+ """Realtime client implementation using centrifuge-python."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ import logging
7
+ from typing import Optional, List
8
+
9
+ from centrifuge import (
10
+ Client,
11
+ ClientEventHandler,
12
+ PublicationContext,
13
+ SubscriptionEventHandler,
14
+ )
15
+
16
+ from ..auth.base import AuthProvider
17
+ from ..exceptions import MagickMindError
18
+
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _extract_jwt_sub(token: str) -> Optional[str]:
24
+ """
25
+ Decode JWT without verification to extract 'sub'.
26
+ Returns None if parsing fails.
27
+ """
28
+ try:
29
+ parts = token.split(".")
30
+ if len(parts) < 2:
31
+ return None
32
+ payload_b64 = parts[1]
33
+ payload_b64 += "=" * ((4 - len(payload_b64) % 4) % 4)
34
+ payload = json.loads(base64.urlsafe_b64decode(payload_b64).decode("utf-8"))
35
+ sub = payload.get("sub")
36
+ return sub if isinstance(sub, str) else None
37
+ except Exception:
38
+ return None
39
+
40
+
41
+ class _DelegatingSubscriptionHandler(SubscriptionEventHandler):
42
+ """Routes client-side subscription publications to ClientEventHandler.on_publication."""
43
+
44
+ def __init__(self, client_handler: ClientEventHandler, channel: str):
45
+ self._client_handler = client_handler
46
+ self._channel = channel
47
+
48
+ async def on_publication(self, ctx: PublicationContext) -> None:
49
+ """Route client-side publication to the ClientEventHandler."""
50
+ logger.debug(f"Publication on {self._channel}: {ctx.pub.data}")
51
+
52
+ # Wrap in adapter for ClientEventHandler
53
+ server_ctx = _PublicationAdapter(ctx, self._channel)
54
+ try:
55
+ await self._client_handler.on_publication(server_ctx)
56
+ except Exception:
57
+ logger.exception(f"Error in on_publication handler for {self._channel}")
58
+
59
+ async def on_subscribed(self, ctx) -> None:
60
+ logger.info(f"✅ Subscribed to channel: {self._channel}")
61
+
62
+ async def on_unsubscribed(self, ctx) -> None:
63
+ logger.info(f"Unsubscribed from channel: {self._channel}")
64
+
65
+ async def on_error(self, ctx) -> None:
66
+ logger.error(f"Subscription error on {self._channel}: {ctx}")
67
+
68
+
69
+ class _PublicationAdapter:
70
+ """Adapts PublicationContext to look like ServerPublicationContext."""
71
+
72
+ def __init__(self, client_ctx: PublicationContext, channel: str):
73
+ self.pub = client_ctx.pub
74
+ self.channel = channel
75
+
76
+
77
+ class RealtimeClient:
78
+ """
79
+ Async client for real-time features using WebSockets.
80
+ Uses pure client-side subscriptions for reliability.
81
+ """
82
+
83
+ def __init__(self, auth: AuthProvider, ws_url: str):
84
+ self.auth = auth
85
+ self.ws_url = ws_url
86
+ self._client: Optional[Client] = None
87
+ self._events: Optional[ClientEventHandler] = None
88
+
89
+ async def _get_token(self) -> str:
90
+ """Get token wrapper for centrifuge client."""
91
+ try:
92
+ return await self.auth.get_token_async()
93
+ except Exception:
94
+ raise
95
+
96
+ async def connect(self, events: Optional[ClientEventHandler] = None) -> None:
97
+ """Connect to the realtime service."""
98
+ if self._client:
99
+ return
100
+
101
+ self._events = events or ClientEventHandler()
102
+
103
+ self._client = Client(
104
+ self.ws_url,
105
+ events=self._events,
106
+ get_token=self._get_token,
107
+ use_protobuf=False,
108
+ )
109
+
110
+ await self._client.connect()
111
+ await self._client.ready()
112
+
113
+ async def disconnect(self) -> None:
114
+ """Disconnect from the realtime service."""
115
+ if self._client:
116
+ await self._client.disconnect()
117
+ self._client = None
118
+
119
+ async def subscribe(self, target_user_id: str) -> None:
120
+ """
121
+ Subscribe to a user's channel using client-side subscription.
122
+
123
+ Args:
124
+ target_user_id: ID of the user to subscribe to
125
+ """
126
+ if not self._client:
127
+ raise MagickMindError("Realtime client not connected")
128
+
129
+ # Build channel name
130
+ token = await self._get_token()
131
+ service_user_id = _extract_jwt_sub(token)
132
+ if not service_user_id:
133
+ raise MagickMindError("Failed to extract service_user_id from JWT")
134
+
135
+ channel = f"personal:{target_user_id}#{service_user_id}"
136
+ logger.debug(f"Subscribing to channel: {channel}")
137
+
138
+ # Create client-side subscription with handler
139
+ await self._ensure_subscription(channel)
140
+
141
+ async def _ensure_subscription(self, channel: str) -> None:
142
+ """Ensure client-side subscription exists with proper event handler."""
143
+ if not self._client or not self._events:
144
+ return
145
+
146
+ sub_events = _DelegatingSubscriptionHandler(self._events, channel)
147
+
148
+ try:
149
+ existing_sub = self._client.get_subscription(channel)
150
+ if existing_sub:
151
+ state = getattr(existing_sub, "state", None)
152
+ state_name = getattr(state, "name", "")
153
+ if state_name == "UNSUBSCRIBED":
154
+ await existing_sub.subscribe()
155
+ logger.info(f"Resubscribed to {channel}")
156
+ else:
157
+ sub = self._client.new_subscription(channel, events=sub_events)
158
+ await sub.subscribe()
159
+ logger.info(f"Subscribed to {channel}")
160
+ except Exception as e:
161
+ logger.error(f"Subscription failed for {channel}: {e}")
162
+ raise MagickMindError(f"Subscribe failed: {e}")
163
+
164
+ async def subscribe_many(self, target_user_ids: List[str]) -> None:
165
+ """Subscribe to multiple users concurrently."""
166
+ if not target_user_ids:
167
+ return
168
+
169
+ tasks = [self.subscribe(uid) for uid in target_user_ids]
170
+ results = await asyncio.gather(*tasks, return_exceptions=True)
171
+
172
+ errors = [r for r in results if isinstance(r, Exception)]
173
+ if errors:
174
+ raise errors[0]
175
+
176
+ async def unsubscribe(self, target_user_id: str) -> None:
177
+ """Unsubscribe from a user's channel."""
178
+ if not self._client:
179
+ raise MagickMindError("Realtime client not connected")
180
+
181
+ token = await self._get_token()
182
+ service_user_id = _extract_jwt_sub(token)
183
+ if not service_user_id:
184
+ return
185
+
186
+ channel = f"personal:{target_user_id}#{service_user_id}"
187
+ sub = self._client.get_subscription(channel)
188
+ if sub:
189
+ await sub.unsubscribe()
190
+
191
+ async def unsubscribe_many(self, target_user_ids: List[str]) -> None:
192
+ """Unsubscribe from multiple users concurrently."""
193
+ if not target_user_ids:
194
+ return
195
+
196
+ tasks = [self.unsubscribe(uid) for uid in target_user_ids]
197
+ await asyncio.gather(*tasks, return_exceptions=True)
198
+
199
+ @property
200
+ def client(self) -> Optional[Client]:
201
+ """Get underlying centrifuge client."""
202
+ return self._client
@@ -0,0 +1,122 @@
1
+ """
2
+ High-level event handler for Magick Mind Realtime Client.
3
+ Abstracts away Centrifugo channel parsing details.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any, Optional
8
+
9
+ from centrifuge import (
10
+ ClientEventHandler,
11
+ ServerPublicationContext,
12
+ ServerSubscribedContext,
13
+ ServerSubscribingContext,
14
+ ServerUnsubscribedContext,
15
+ ConnectedContext,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class RealtimeEventHandler(ClientEventHandler):
22
+ """
23
+ Abstract base class for handling Realtime SDK events.
24
+
25
+ Subclass this and override `on_message` to handle incoming user updates.
26
+ The SDK handles channel parsing and data extraction for you.
27
+ """
28
+
29
+ async def on_connected(self, ctx: ConnectedContext) -> None:
30
+ """Called when connected to the Realtime Gateway."""
31
+ logger.info(f"✅ Connected to Realtime Gateway (Client ID: {ctx.client})")
32
+
33
+ async def on_subscribed(self, ctx: ServerSubscribedContext) -> None:
34
+ """Called when server-side subscription is established."""
35
+ logger.info(f"✅ Server-side subscribed to channel: {ctx.channel}")
36
+
37
+ async def on_subscribing(self, ctx: ServerSubscribingContext) -> None:
38
+ """Called when server-side subscription is in progress."""
39
+ logger.debug(f"Subscribing to server-side channel: {ctx.channel}")
40
+
41
+ async def on_unsubscribed(self, ctx: ServerUnsubscribedContext) -> None:
42
+ """Called when unsubscribed from server-side subscription."""
43
+ logger.info(f"Unsubscribed from server-side channel: {ctx.channel}")
44
+
45
+ async def on_message(self, user_id: str, payload: Any) -> None:
46
+ """
47
+ Called when a message is received for a specific user.
48
+
49
+ Args:
50
+ user_id: The ID of the end-user this message is for.
51
+ payload: The message content (dict, string, etc).
52
+ """
53
+ pass
54
+
55
+ async def on_raw_message(self, channel: str, payload: Any) -> None:
56
+ """
57
+ Called when a message is received but the user ID could not be parsed,
58
+ or for non-standard channels.
59
+ """
60
+ pass
61
+
62
+ async def on_publication(self, ctx: ServerPublicationContext) -> None:
63
+ """
64
+ Internal handler. Parses channel context and dispatches to on_message.
65
+ """
66
+ logger.info(f"📨 Publication received on channel: {ctx.channel}")
67
+
68
+ channel = self._extract_channel(ctx)
69
+ data = self._extract_data(ctx)
70
+
71
+ logger.debug(f"Channel: {channel}, Data: {data}")
72
+
73
+ user_id = self._extract_user_id(channel)
74
+
75
+ if user_id:
76
+ await self.on_message(user_id, data)
77
+ else:
78
+ await self.on_raw_message(channel, data)
79
+
80
+ def _extract_channel(self, ctx: ServerPublicationContext) -> str:
81
+ """
82
+ Extract channel from context in a version-resilient way.
83
+ """
84
+ # Try direct attribute
85
+ ch = getattr(ctx, "channel", None)
86
+ if ch:
87
+ return ch
88
+
89
+ # Try inside pub object
90
+ pub = getattr(ctx, "pub", None)
91
+ if pub:
92
+ return getattr(pub, "channel", "")
93
+
94
+ return ""
95
+
96
+ def _extract_data(self, ctx: ServerPublicationContext) -> Any:
97
+ """
98
+ Extract data payload from context.
99
+ """
100
+ pub = getattr(ctx, "pub", None)
101
+ return getattr(pub, "data", None) if pub else None
102
+
103
+ def _extract_user_id(self, channel: str) -> Optional[str]:
104
+ """
105
+ Parse User ID from channel string.
106
+ Format confirmed as: personal:<target_user_id>#<service_user_id>
107
+ """
108
+ if not channel:
109
+ return None
110
+
111
+ # 1. Remove namespace (personal:)
112
+ if ":" in channel:
113
+ without_ns = channel.split(":", 1)[1]
114
+ else:
115
+ without_ns = channel
116
+
117
+ # 2. Extract first part before '#'
118
+ # "user_123#service_456" -> "user_123"
119
+ if "#" in without_ns:
120
+ return without_ns.split("#")[0]
121
+
122
+ return without_ns
@@ -0,0 +1,201 @@
1
+ # Extending the SDK with Resource Clients
2
+
3
+ This directory contains the **base structure** for API resource clients.
4
+
5
+ ## Recommended Pattern: Versioned Folders
6
+
7
+ When implementing resources, use **explicit versioned folders** that mirror bifrost's API structure:
8
+
9
+ ```
10
+ magick_mind/resources/
11
+ ├── __init__.py # BaseResource, V1Resources, V2Resources
12
+ ├── base.py # BaseResource class (or in __init__.py)
13
+ ├── v1/
14
+ │ ├── __init__.py
15
+ │ ├── chat.py # ChatResourceV1
16
+ │ └── history.py # HistoryResourceV1
17
+ └── v2/
18
+ ├── __init__.py
19
+ ├── chat.py # ChatResourceV2
20
+ └── history.py # HistoryResourceV2
21
+ ```
22
+
23
+ **Why versioned folders?** They mirror bifrost's API structure and keep versions isolated.
24
+
25
+ ## Beta and Experimental Features
26
+
27
+ Beta features can be organized in two ways depending on how bifrost implements them:
28
+
29
+ ### Pattern A: Beta as Separate Container
30
+
31
+ Use when beta is a **full API version** with breaking changes (preparation for v2):
32
+
33
+ ```
34
+ resources/
35
+ ├── v1/
36
+ │ └── chat.py # ChatResourceV1 → /v1/magickmind/chat
37
+ └── beta/
38
+ └── chat.py # ChatResourceBeta → /beta/magickmind/chat
39
+ ```
40
+
41
+ ```python
42
+ class V1Resources:
43
+ def __init__(self, http_client):
44
+ self.chat = ChatResourceV1(http_client)
45
+
46
+ class BetaResources:
47
+ def __init__(self, http_client):
48
+ self.chat = ChatResourceBeta(http_client) # Breaking changes
49
+
50
+ # Usage
51
+ client.v1.chat.send(message="...") # Stable
52
+ client.beta.chat.send(content={...}) # Different API (breaking)
53
+ ```
54
+
55
+ **When to use:**
56
+ - Beta has different request/response structure
57
+ - Testing major breaking changes
58
+ - Beta will become v2
59
+
60
+ ### Pattern B: Beta as Attribute Within Version
61
+
62
+ Use when beta is a **feature flag** within the same version:
63
+
64
+ ```
65
+ resources/
66
+ └── v1/
67
+ ├── chat.py # ChatResourceV1 → /v1/magickmind/chat
68
+ └── chat_beta.py # ChatBetaResourceV1 → /v1/magickmind/chatbeta
69
+ ```
70
+
71
+ ```python
72
+ class V1Resources:
73
+ def __init__(self, http_client):
74
+ self.chat = ChatResourceV1(http_client)
75
+ self.chat_beta = ChatBetaResourceV1(http_client) # Feature flag
76
+
77
+ # Usage
78
+ client.v1.chat.send(message="...") # Stable
79
+ client.v1.chat_beta.send( # Same structure, new features
80
+ message="...",
81
+ experimental_param=True # New parameter being tested
82
+ )
83
+ ```
84
+
85
+ **When to use:**
86
+ - Beta has same basic structure as stable
87
+ - Testing new parameters/features
88
+ - A/B testing
89
+ - Beta is not a breaking change
90
+
91
+ ### Pattern C: Both (Flexible)
92
+
93
+ You can use both patterns if bifrost has both types of beta features:
94
+
95
+ ```python
96
+ class V1Resources:
97
+ def __init__(self, http_client):
98
+ self.chat = ChatResourceV1(http_client)
99
+ self.chat_beta = ChatBetaResourceV1(http_client) # Feature flag
100
+
101
+ class BetaResources:
102
+ def __init__(self, http_client):
103
+ self.chat = ChatResourceBeta(http_client) # Breaking version
104
+
105
+ # Usage
106
+ client.v1.chat.send(...) # Stable v1
107
+ client.v1.chat_beta.send(...) # Beta feature in v1 (non-breaking)
108
+ client.beta.chat.send(...) # Beta API version (breaking)
109
+ ```
110
+
111
+ **Guideline:** Match the pattern to how bifrost actually implements the endpoint. Check the API path structure first.
112
+
113
+ ## Example: V1 Chat Resource
114
+
115
+ ```python
116
+ # resources/v1/chat.py
117
+ from typing import Optional
118
+ from magick_mind.models.v1.chat import ChatSendRequest, ChatSendResponse
119
+ from magick_mind.resources.base import BaseResource
120
+
121
+ class ChatResourceV1(BaseResource):
122
+ def send(self, message: str, model: str = "gpt-4") -> ChatSendResponse:
123
+ """Send a chat message."""
124
+ payload = ChatSendRequest(message=message, model=model)
125
+
126
+ # self.http gives access to authenticated httpx client
127
+ resp = self.http.post("/v1/chat/completions", json=payload.model_dump())
128
+
129
+ return ChatSendResponse(**resp.json())
130
+ ```
131
+
132
+ ## Resource Containers
133
+
134
+ Create version containers in `resources/__init__.py`:
135
+
136
+ ```python
137
+ from .v1.chat import ChatResourceV1
138
+ from .v1.history import HistoryResourceV1
139
+
140
+ class V1Resources:
141
+ """Container for all v1 API resources."""
142
+ def __init__(self, http_client):
143
+ self.chat = ChatResourceV1(http_client)
144
+ self.history = HistoryResourceV1(http_client)
145
+
146
+ class V2Resources:
147
+ """Container for all v2 API resources."""
148
+ def __init__(self, http_client):
149
+ from .v2.chat import ChatResourceV2
150
+ self.chat = ChatResourceV2(http_client)
151
+ ```
152
+
153
+ ## Wire to Main Client
154
+
155
+ Update `client.py`:
156
+
157
+ ```python
158
+ from magick_mind.resources import V1Resources, V2Resources
159
+
160
+ class MagickMind:
161
+ def __init__(self, ...):
162
+ # ... auth setup ...
163
+
164
+ # Initialize resources with http client
165
+ self.v1 = V1Resources(self.http)
166
+ self.v2 = V2Resources(self.http)
167
+
168
+ # Default alias
169
+ self.chat = self.v1.chat
170
+ ```
171
+
172
+ ## Usage
173
+
174
+ ```python
175
+ from magick_mind import MagickMind
176
+
177
+ client = MagickMind(email="...", password="...", base_url="...")
178
+
179
+ # Explicit version (recommended)
180
+ response = client.v1.chat.send(
181
+ api_key="sk-...",
182
+ message="Hello!",
183
+ chat_id="123",
184
+ sender_id="user-456"
185
+ )
186
+
187
+ # Or default alias
188
+ response = client.chat.send(...)
189
+ ```
190
+
191
+ ## Complete Example
192
+
193
+ See **[docs/examples/chat_implementation/](../../../docs/examples/chat_implementation/)** for a complete working reference implementation.
194
+
195
+ ## Benefits
196
+
197
+ 1. **Explicit versioning** - `client.v1.chat` vs `client.v2.chat`
198
+ 2. **Type safety** - Pydantic models validate requests/responses
199
+ 3. **Mirrors Bifrost** - SDK structure matches API structure
200
+ 4. **Safe evolution** - Breaking changes require explicit opt-in
201
+
@@ -0,0 +1,42 @@
1
+ """Resource clients for API endpoints."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from magick_mind.resources.base import BaseResource
6
+
7
+ if TYPE_CHECKING:
8
+ from magick_mind.http import HTTPClient
9
+
10
+
11
+ class V1Resources:
12
+ """Container for all v1 API resources."""
13
+
14
+ def __init__(self, http_client: "HTTPClient"):
15
+ """
16
+ Initialize V1 resources.
17
+
18
+ Args:
19
+ http_client: HTTP client for making API requests
20
+ """
21
+ from magick_mind.resources.v1.artifact import ArtifactResourceV1
22
+ from magick_mind.resources.v1.api_keys import ApiKeysResourceV1
23
+ from magick_mind.resources.v1.chat import ChatResourceV1
24
+ from magick_mind.resources.v1.corpus import CorpusResourceV1
25
+ from magick_mind.resources.v1.end_user import EndUserResourceV1
26
+ from magick_mind.resources.v1.history import HistoryResourceV1
27
+ from magick_mind.resources.v1.mindspace import MindspaceResourceV1
28
+ from magick_mind.resources.v1.project import ProjectResourceV1
29
+ from magick_mind.resources.v1.model import ModelsResourceV1
30
+
31
+ self.artifact = ArtifactResourceV1(http_client)
32
+ self.api_keys = ApiKeysResourceV1(http_client)
33
+ self.chat = ChatResourceV1(http_client)
34
+ self.corpus = CorpusResourceV1(http_client)
35
+ self.end_user = EndUserResourceV1(http_client)
36
+ self.history = HistoryResourceV1(http_client)
37
+ self.mindspace = MindspaceResourceV1(http_client)
38
+ self.project = ProjectResourceV1(http_client)
39
+ self.models = ModelsResourceV1(http_client)
40
+
41
+
42
+ __all__ = ["BaseResource", "V1Resources"]
@@ -0,0 +1,31 @@
1
+ """Base class for API resource clients."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from magick_mind.http import HTTPClient
7
+
8
+
9
+ class BaseResource:
10
+ """
11
+ Base class for all resource clients.
12
+
13
+ Resource clients encapsulate API endpoints for specific domains
14
+ (e.g., chat, history, users, mindspaces).
15
+
16
+ For a complete implementation example, see docs/contributing/resource_implementation_guide/
17
+
18
+ Example:
19
+ class ChatResourceV1(BaseResource):
20
+ def send(self, api_key: str, message: str, **kwargs):
21
+ return self._http.post("/v1/magickmind/chat", json={...})
22
+ """
23
+
24
+ def __init__(self, http_client: "HTTPClient"):
25
+ """
26
+ Initialize resource client.
27
+
28
+ Args:
29
+ http_client: HTTP client for making API requests
30
+ """
31
+ self._http = http_client
@@ -0,0 +1,19 @@
1
+ """V1 API resources."""
2
+
3
+ from magick_mind.resources.v1.artifact import ArtifactResourceV1
4
+ from magick_mind.resources.v1.api_keys import ApiKeysResourceV1
5
+ from magick_mind.resources.v1.corpus import CorpusResourceV1
6
+ from magick_mind.resources.v1.chat import ChatResourceV1
7
+ from magick_mind.resources.v1.end_user import EndUserResourceV1
8
+ from magick_mind.resources.v1.mindspace import MindspaceResourceV1
9
+ from magick_mind.resources.v1.project import ProjectResourceV1
10
+
11
+ __all__ = [
12
+ "ArtifactResourceV1",
13
+ "ApiKeysResourceV1",
14
+ "CorpusResourceV1",
15
+ "ChatResourceV1",
16
+ "EndUserResourceV1",
17
+ "MindspaceResourceV1",
18
+ "ProjectResourceV1",
19
+ ]