circle-so-python-sdk 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.
Files changed (48) hide show
  1. circle/__init__.py +24 -0
  2. circle/api/__init__.py +1 -0
  3. circle/api/admin_access_groups.py +133 -0
  4. circle/api/admin_community.py +122 -0
  5. circle/api/admin_courses.py +172 -0
  6. circle/api/admin_events.py +101 -0
  7. circle/api/admin_misc.py +305 -0
  8. circle/api/admin_posts.py +200 -0
  9. circle/api/admin_spaces.py +301 -0
  10. circle/api/admin_tags.py +146 -0
  11. circle/api/auth.py +63 -0
  12. circle/api/headless_chat_notif_members.py +323 -0
  13. circle/api/headless_misc.py +218 -0
  14. circle/api/headless_spaces_posts.py +279 -0
  15. circle/client.py +197 -0
  16. circle/constants.py +5 -0
  17. circle/exceptions.py +34 -0
  18. circle/http.py +227 -0
  19. circle/models/__init__.py +5 -0
  20. circle/models/admin/__init__.py +47 -0
  21. circle/models/admin/community.py +82 -0
  22. circle/models/admin/courses.py +30 -0
  23. circle/models/admin/events.py +67 -0
  24. circle/models/admin/members.py +105 -0
  25. circle/models/admin/misc.py +240 -0
  26. circle/models/admin/posts.py +172 -0
  27. circle/models/admin/spaces.py +146 -0
  28. circle/models/admin/tags.py +85 -0
  29. circle/models/auth.py +21 -0
  30. circle/models/base.py +28 -0
  31. circle/models/headless/__init__.py +50 -0
  32. circle/models/headless/chat.py +134 -0
  33. circle/models/headless/comments.py +60 -0
  34. circle/models/headless/courses.py +109 -0
  35. circle/models/headless/members.py +105 -0
  36. circle/models/headless/misc.py +120 -0
  37. circle/models/headless/notifications.py +66 -0
  38. circle/models/headless/posts.py +137 -0
  39. circle/models/headless/spaces.py +80 -0
  40. circle/pagination.py +116 -0
  41. circle/py.typed +1 -0
  42. circle/rate_limit.py +36 -0
  43. circle/validation.py +67 -0
  44. circle/webhooks.py +39 -0
  45. circle_so_python_sdk-0.1.0.dist-info/METADATA +120 -0
  46. circle_so_python_sdk-0.1.0.dist-info/RECORD +48 -0
  47. circle_so_python_sdk-0.1.0.dist-info/WHEEL +4 -0
  48. circle_so_python_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,279 @@
1
+ """Headless Client API -- Spaces, Posts, Comments."""
2
+ from __future__ import annotations
3
+ from typing import Any, Dict, List, Optional
4
+ from circle.constants import HEADLESS_V1_PREFIX as _P
5
+ from circle.http import AsyncTransport, SyncTransport
6
+ from circle.models.headless.spaces import (
7
+ HeadlessSpace, HeadlessSpaceList, SpaceNotificationDetail, SpaceBookmarkList, HeadlessSpaceTopicList,
8
+ )
9
+ from circle.models.headless.posts import HeadlessPost, HeadlessPostList, HeadlessImagePost
10
+ from circle.models.headless.comments import HeadlessComment, HeadlessCommentList, UserLikeList
11
+
12
+
13
+ def _list_posts_params(page=1, per_page=10, sort=None, status=None, topics=None, **kw):
14
+ p: Dict[str, Any] = {"page": page, "per_page": per_page, **kw}
15
+ if sort: p["sort"] = sort
16
+ if status: p["status"] = status
17
+ if topics: p["topics"] = topics
18
+ return p
19
+
20
+
21
+ def _comment_params(page=1, per_page=60, sort=None):
22
+ p: Dict[str, Any] = {"page": page, "per_page": per_page}
23
+ if sort: p["sort"] = sort
24
+ return p
25
+
26
+
27
+ def _home_params(page=1, per_page=10, sort=None):
28
+ p: Dict[str, Any] = {"page": page, "per_page": per_page}
29
+ if sort: p["sort"] = sort
30
+ return p
31
+
32
+
33
+ class HeadlessSpacesPostsClient:
34
+ def __init__(self, transport: SyncTransport) -> None:
35
+ self._t = transport
36
+
37
+ def list_spaces(self) -> List[HeadlessSpace]:
38
+ return [HeadlessSpace.model_validate(s) for s in self._t.request("GET", f"{_P}/spaces")]
39
+
40
+ def get_space(self, space_id: int) -> HeadlessSpace:
41
+ return HeadlessSpace.model_validate(self._t.request("GET", f"{_P}/spaces/{space_id}"))
42
+
43
+ def get_home_space(self) -> HeadlessSpace:
44
+ return HeadlessSpace.model_validate(self._t.request("GET", f"{_P}/spaces/home"))
45
+
46
+ def join_space(self, space_id: int) -> HeadlessSpace:
47
+ return HeadlessSpace.model_validate(self._t.request("POST", f"{_P}/spaces/{space_id}/join"))
48
+
49
+ def leave_space(self, space_id: int) -> HeadlessSpace:
50
+ return HeadlessSpace.model_validate(self._t.request("POST", f"{_P}/spaces/{space_id}/leave"))
51
+
52
+ def get_space_topics(self, space_id: int, *, page: int = 1, per_page: int = 10) -> HeadlessSpaceTopicList:
53
+ return HeadlessSpaceTopicList.model_validate(
54
+ self._t.request("GET", f"{_P}/spaces/{space_id}/topics", params={"page": page, "per_page": per_page}))
55
+
56
+ def get_space_bookmarks(self, space_id: int, *, page: int = 1, per_page: int = 10) -> SpaceBookmarkList:
57
+ return SpaceBookmarkList.model_validate(
58
+ self._t.request("GET", f"{_P}/spaces/{space_id}/bookmarks", params={"page": page, "per_page": per_page}))
59
+
60
+ def get_space_notification_details(self, *, space_ids: Optional[str] = None) -> List[SpaceNotificationDetail]:
61
+ p: Dict[str, Any] = {}
62
+ if space_ids: p["space_ids"] = space_ids
63
+ return [SpaceNotificationDetail.model_validate(d) for d in
64
+ self._t.request("GET", f"{_P}/space_notification_details", params=p)]
65
+
66
+ def list_posts(self, space_id: int, **kw: Any) -> HeadlessPostList:
67
+ return HeadlessPostList.model_validate(
68
+ self._t.request("GET", f"{_P}/spaces/{space_id}/posts", params=_list_posts_params(**kw)))
69
+
70
+ def create_post(self, space_id: int, **kw: Any) -> HeadlessPost:
71
+ return HeadlessPost.model_validate(self._t.request("POST", f"{_P}/spaces/{space_id}/posts", json=kw))
72
+
73
+ def get_post(self, space_id: int, post_id: int) -> HeadlessPost:
74
+ return HeadlessPost.model_validate(self._t.request("GET", f"{_P}/spaces/{space_id}/posts/{post_id}"))
75
+
76
+ def delete_post(self, space_id: int, post_id: int) -> Dict[str, Any]:
77
+ return self._t.request("DELETE", f"{_P}/spaces/{space_id}/posts/{post_id}")
78
+
79
+ def create_image_post(self, space_id: int, **kw: Any) -> HeadlessImagePost:
80
+ return HeadlessImagePost.model_validate(
81
+ self._t.request("POST", f"{_P}/spaces/{space_id}/images/posts", json=kw))
82
+
83
+ def update_image_post(self, space_id: int, post_id: int, **kw: Any) -> HeadlessImagePost:
84
+ return HeadlessImagePost.model_validate(
85
+ self._t.request("PUT", f"{_P}/spaces/{space_id}/images/posts/{post_id}", json=kw))
86
+
87
+ def follow_post(self, post_id: int) -> Dict[str, Any]:
88
+ return self._t.request("POST", f"{_P}/posts/{post_id}/post_followers")
89
+
90
+ def unfollow_post(self, post_id: int) -> Dict[str, Any]:
91
+ return self._t.request("DELETE", f"{_P}/posts/{post_id}/post_followers")
92
+
93
+ def like_post(self, post_id: int) -> Dict[str, Any]:
94
+ return self._t.request("POST", f"{_P}/posts/{post_id}/user_likes")
95
+
96
+ def unlike_post(self, post_id: int) -> Dict[str, Any]:
97
+ return self._t.request("DELETE", f"{_P}/posts/{post_id}/user_likes")
98
+
99
+ def get_post_likes(self, post_id: int, *, page: int = 1, per_page: int = 10) -> UserLikeList:
100
+ return UserLikeList.model_validate(
101
+ self._t.request("GET", f"{_P}/posts/{post_id}/user_likes", params={"page": page, "per_page": per_page}))
102
+
103
+ def get_home_posts(self, **kw: Any) -> HeadlessPostList:
104
+ return HeadlessPostList.model_validate(self._t.request("GET", f"{_P}/home", params=_home_params(**kw)))
105
+
106
+ def list_comments(self, post_id: int, **kw: Any) -> HeadlessCommentList:
107
+ return HeadlessCommentList.model_validate(
108
+ self._t.request("GET", f"{_P}/posts/{post_id}/comments", params=_comment_params(**kw)))
109
+
110
+ def create_comment(self, post_id: int, **kw: Any) -> HeadlessComment:
111
+ return HeadlessComment.model_validate(
112
+ self._t.request("POST", f"{_P}/posts/{post_id}/comments", json={"comment": kw}))
113
+
114
+ def get_comment(self, post_id: int, comment_id: int) -> HeadlessComment:
115
+ return HeadlessComment.model_validate(
116
+ self._t.request("GET", f"{_P}/posts/{post_id}/comments/{comment_id}"))
117
+
118
+ def update_comment(self, post_id: int, comment_id: int, **kw: Any) -> HeadlessComment:
119
+ return HeadlessComment.model_validate(
120
+ self._t.request("PATCH", f"{_P}/posts/{post_id}/comments/{comment_id}", json={"comment": kw}))
121
+
122
+ def delete_comment(self, post_id: int, comment_id: int) -> Dict[str, Any]:
123
+ return self._t.request("DELETE", f"{_P}/posts/{post_id}/comments/{comment_id}")
124
+
125
+ def list_replies(self, comment_id: int, **kw: Any) -> HeadlessCommentList:
126
+ return HeadlessCommentList.model_validate(
127
+ self._t.request("GET", f"{_P}/comments/{comment_id}/replies", params=_comment_params(**kw)))
128
+
129
+ def create_reply(self, comment_id: int, **kw: Any) -> HeadlessComment:
130
+ return HeadlessComment.model_validate(
131
+ self._t.request("POST", f"{_P}/comments/{comment_id}/replies", json={"comment": kw}))
132
+
133
+ def get_reply(self, comment_id: int, reply_id: int) -> HeadlessComment:
134
+ return HeadlessComment.model_validate(
135
+ self._t.request("GET", f"{_P}/comments/{comment_id}/replies/{reply_id}"))
136
+
137
+ def update_reply(self, comment_id: int, reply_id: int, **kw: Any) -> HeadlessComment:
138
+ return HeadlessComment.model_validate(
139
+ self._t.request("PATCH", f"{_P}/comments/{comment_id}/replies/{reply_id}", json={"comment": kw}))
140
+
141
+ def delete_reply(self, comment_id: int, reply_id: int) -> Dict[str, Any]:
142
+ return self._t.request("DELETE", f"{_P}/comments/{comment_id}/replies/{reply_id}")
143
+
144
+ def like_comment(self, comment_id: int) -> Dict[str, Any]:
145
+ return self._t.request("POST", f"{_P}/comments/{comment_id}/user_likes")
146
+
147
+ def unlike_comment(self, comment_id: int) -> Dict[str, Any]:
148
+ return self._t.request("DELETE", f"{_P}/comments/{comment_id}/user_likes")
149
+
150
+ def get_comment_likes(self, comment_id: int, *, page: int = 1, per_page: int = 10) -> UserLikeList:
151
+ return UserLikeList.model_validate(
152
+ self._t.request("GET", f"{_P}/comments/{comment_id}/user_likes",
153
+ params={"page": page, "per_page": per_page}))
154
+
155
+
156
+ class AsyncHeadlessSpacesPostsClient:
157
+ def __init__(self, transport: AsyncTransport) -> None:
158
+ self._t = transport
159
+
160
+ async def list_spaces(self) -> List[HeadlessSpace]:
161
+ return [HeadlessSpace.model_validate(s) for s in await self._t.request("GET", f"{_P}/spaces")]
162
+
163
+ async def get_space(self, space_id: int) -> HeadlessSpace:
164
+ return HeadlessSpace.model_validate(await self._t.request("GET", f"{_P}/spaces/{space_id}"))
165
+
166
+ async def get_home_space(self) -> HeadlessSpace:
167
+ return HeadlessSpace.model_validate(await self._t.request("GET", f"{_P}/spaces/home"))
168
+
169
+ async def join_space(self, space_id: int) -> HeadlessSpace:
170
+ return HeadlessSpace.model_validate(await self._t.request("POST", f"{_P}/spaces/{space_id}/join"))
171
+
172
+ async def leave_space(self, space_id: int) -> HeadlessSpace:
173
+ return HeadlessSpace.model_validate(await self._t.request("POST", f"{_P}/spaces/{space_id}/leave"))
174
+
175
+ async def get_space_topics(self, space_id: int, *, page: int = 1, per_page: int = 10) -> HeadlessSpaceTopicList:
176
+ return HeadlessSpaceTopicList.model_validate(
177
+ await self._t.request("GET", f"{_P}/spaces/{space_id}/topics",
178
+ params={"page": page, "per_page": per_page}))
179
+
180
+ async def get_space_bookmarks(self, space_id: int, *, page: int = 1, per_page: int = 10) -> SpaceBookmarkList:
181
+ return SpaceBookmarkList.model_validate(
182
+ await self._t.request("GET", f"{_P}/spaces/{space_id}/bookmarks",
183
+ params={"page": page, "per_page": per_page}))
184
+
185
+ async def get_space_notification_details(self, *, space_ids: Optional[str] = None) -> List[SpaceNotificationDetail]:
186
+ p: Dict[str, Any] = {}
187
+ if space_ids: p["space_ids"] = space_ids
188
+ return [SpaceNotificationDetail.model_validate(d) for d in
189
+ await self._t.request("GET", f"{_P}/space_notification_details", params=p)]
190
+
191
+ async def list_posts(self, space_id: int, **kw: Any) -> HeadlessPostList:
192
+ return HeadlessPostList.model_validate(
193
+ await self._t.request("GET", f"{_P}/spaces/{space_id}/posts", params=_list_posts_params(**kw)))
194
+
195
+ async def create_post(self, space_id: int, **kw: Any) -> HeadlessPost:
196
+ return HeadlessPost.model_validate(await self._t.request("POST", f"{_P}/spaces/{space_id}/posts", json=kw))
197
+
198
+ async def get_post(self, space_id: int, post_id: int) -> HeadlessPost:
199
+ return HeadlessPost.model_validate(await self._t.request("GET", f"{_P}/spaces/{space_id}/posts/{post_id}"))
200
+
201
+ async def delete_post(self, space_id: int, post_id: int) -> Dict[str, Any]:
202
+ return await self._t.request("DELETE", f"{_P}/spaces/{space_id}/posts/{post_id}")
203
+
204
+ async def create_image_post(self, space_id: int, **kw: Any) -> HeadlessImagePost:
205
+ return HeadlessImagePost.model_validate(
206
+ await self._t.request("POST", f"{_P}/spaces/{space_id}/images/posts", json=kw))
207
+
208
+ async def update_image_post(self, space_id: int, post_id: int, **kw: Any) -> HeadlessImagePost:
209
+ return HeadlessImagePost.model_validate(
210
+ await self._t.request("PUT", f"{_P}/spaces/{space_id}/images/posts/{post_id}", json=kw))
211
+
212
+ async def follow_post(self, post_id: int) -> Dict[str, Any]:
213
+ return await self._t.request("POST", f"{_P}/posts/{post_id}/post_followers")
214
+
215
+ async def unfollow_post(self, post_id: int) -> Dict[str, Any]:
216
+ return await self._t.request("DELETE", f"{_P}/posts/{post_id}/post_followers")
217
+
218
+ async def like_post(self, post_id: int) -> Dict[str, Any]:
219
+ return await self._t.request("POST", f"{_P}/posts/{post_id}/user_likes")
220
+
221
+ async def unlike_post(self, post_id: int) -> Dict[str, Any]:
222
+ return await self._t.request("DELETE", f"{_P}/posts/{post_id}/user_likes")
223
+
224
+ async def get_post_likes(self, post_id: int, *, page: int = 1, per_page: int = 10) -> UserLikeList:
225
+ return UserLikeList.model_validate(
226
+ await self._t.request("GET", f"{_P}/posts/{post_id}/user_likes",
227
+ params={"page": page, "per_page": per_page}))
228
+
229
+ async def get_home_posts(self, **kw: Any) -> HeadlessPostList:
230
+ return HeadlessPostList.model_validate(await self._t.request("GET", f"{_P}/home", params=_home_params(**kw)))
231
+
232
+ async def list_comments(self, post_id: int, **kw: Any) -> HeadlessCommentList:
233
+ return HeadlessCommentList.model_validate(
234
+ await self._t.request("GET", f"{_P}/posts/{post_id}/comments", params=_comment_params(**kw)))
235
+
236
+ async def create_comment(self, post_id: int, **kw: Any) -> HeadlessComment:
237
+ return HeadlessComment.model_validate(
238
+ await self._t.request("POST", f"{_P}/posts/{post_id}/comments", json={"comment": kw}))
239
+
240
+ async def get_comment(self, post_id: int, comment_id: int) -> HeadlessComment:
241
+ return HeadlessComment.model_validate(
242
+ await self._t.request("GET", f"{_P}/posts/{post_id}/comments/{comment_id}"))
243
+
244
+ async def update_comment(self, post_id: int, comment_id: int, **kw: Any) -> HeadlessComment:
245
+ return HeadlessComment.model_validate(
246
+ await self._t.request("PATCH", f"{_P}/posts/{post_id}/comments/{comment_id}", json={"comment": kw}))
247
+
248
+ async def delete_comment(self, post_id: int, comment_id: int) -> Dict[str, Any]:
249
+ return await self._t.request("DELETE", f"{_P}/posts/{post_id}/comments/{comment_id}")
250
+
251
+ async def list_replies(self, comment_id: int, **kw: Any) -> HeadlessCommentList:
252
+ return HeadlessCommentList.model_validate(
253
+ await self._t.request("GET", f"{_P}/comments/{comment_id}/replies", params=_comment_params(**kw)))
254
+
255
+ async def create_reply(self, comment_id: int, **kw: Any) -> HeadlessComment:
256
+ return HeadlessComment.model_validate(
257
+ await self._t.request("POST", f"{_P}/comments/{comment_id}/replies", json={"comment": kw}))
258
+
259
+ async def get_reply(self, comment_id: int, reply_id: int) -> HeadlessComment:
260
+ return HeadlessComment.model_validate(
261
+ await self._t.request("GET", f"{_P}/comments/{comment_id}/replies/{reply_id}"))
262
+
263
+ async def update_reply(self, comment_id: int, reply_id: int, **kw: Any) -> HeadlessComment:
264
+ return HeadlessComment.model_validate(
265
+ await self._t.request("PATCH", f"{_P}/comments/{comment_id}/replies/{reply_id}", json={"comment": kw}))
266
+
267
+ async def delete_reply(self, comment_id: int, reply_id: int) -> Dict[str, Any]:
268
+ return await self._t.request("DELETE", f"{_P}/comments/{comment_id}/replies/{reply_id}")
269
+
270
+ async def like_comment(self, comment_id: int) -> Dict[str, Any]:
271
+ return await self._t.request("POST", f"{_P}/comments/{comment_id}/user_likes")
272
+
273
+ async def unlike_comment(self, comment_id: int) -> Dict[str, Any]:
274
+ return await self._t.request("DELETE", f"{_P}/comments/{comment_id}/user_likes")
275
+
276
+ async def get_comment_likes(self, comment_id: int, *, page: int = 1, per_page: int = 10) -> UserLikeList:
277
+ return UserLikeList.model_validate(
278
+ await self._t.request("GET", f"{_P}/comments/{comment_id}/user_likes",
279
+ params={"page": page, "per_page": per_page}))
circle/client.py ADDED
@@ -0,0 +1,197 @@
1
+ """Top-level CircleClient and AsyncCircleClient facade."""
2
+ from __future__ import annotations
3
+ import os
4
+ from typing import Optional
5
+
6
+ from circle.http import DEFAULT_BASE_URL, AsyncTransport, SyncTransport
7
+ from circle.api.auth import AsyncHeadlessAuthClient, HeadlessAuthClient
8
+ from circle.api.admin_access_groups import AccessGroupsClient, AsyncAccessGroupsClient
9
+ from circle.api.admin_community import CommunityClient, AsyncCommunityClient
10
+ from circle.api.admin_spaces import SpacesClient, AsyncSpacesClient
11
+ from circle.api.admin_posts import PostsClient, AsyncPostsClient
12
+ from circle.api.admin_events import EventsClient, AsyncEventsClient
13
+ from circle.api.admin_courses import CoursesClient, AsyncCoursesClient
14
+ from circle.api.admin_tags import TagsClient, AsyncTagsClient
15
+ from circle.api.admin_misc import AdminMiscClient, AsyncAdminMiscClient
16
+ from circle.api.headless_spaces_posts import HeadlessSpacesPostsClient, AsyncHeadlessSpacesPostsClient
17
+ from circle.api.headless_chat_notif_members import HeadlessChatNotifMembersClient, AsyncHeadlessChatNotifMembersClient
18
+ from circle.api.headless_misc import HeadlessMiscClient, AsyncHeadlessMiscClient
19
+
20
+
21
+ class _AdminNamespace:
22
+ """Groups all Admin API V2 sub-clients under client.admin.*"""
23
+ def __init__(self, transport: SyncTransport) -> None:
24
+ self.access_groups = AccessGroupsClient(transport)
25
+ self.community = CommunityClient(transport)
26
+ self.spaces = SpacesClient(transport)
27
+ self.posts = PostsClient(transport)
28
+ self.events = EventsClient(transport)
29
+ self.courses = CoursesClient(transport)
30
+ self.tags = TagsClient(transport)
31
+ self.misc = AdminMiscClient(transport)
32
+
33
+ # Convenience delegates to community sub-client
34
+ def get_community(self, **kw):
35
+ return self.community.get_community(**kw)
36
+
37
+ def list_community_members(self, **kw):
38
+ return self.community.list_community_members(**kw)
39
+
40
+
41
+ class _AsyncAdminNamespace:
42
+ def __init__(self, transport: AsyncTransport) -> None:
43
+ self.access_groups = AsyncAccessGroupsClient(transport)
44
+ self.community = AsyncCommunityClient(transport)
45
+ self.spaces = AsyncSpacesClient(transport)
46
+ self.posts = AsyncPostsClient(transport)
47
+ self.events = AsyncEventsClient(transport)
48
+ self.courses = AsyncCoursesClient(transport)
49
+ self.tags = AsyncTagsClient(transport)
50
+ self.misc = AsyncAdminMiscClient(transport)
51
+
52
+ async def get_community(self, **kw):
53
+ return await self.community.get_community(**kw)
54
+
55
+ async def list_community_members(self, **kw):
56
+ return await self.community.list_community_members(**kw)
57
+
58
+
59
+ class _HeadlessNamespace:
60
+ """Groups all Headless Client V1 sub-clients under client.headless.*"""
61
+ def __init__(self, transport: SyncTransport) -> None:
62
+ self.spaces_posts = HeadlessSpacesPostsClient(transport)
63
+ self.chat_notif_members = HeadlessChatNotifMembersClient(transport)
64
+ self.misc = HeadlessMiscClient(transport)
65
+
66
+ # Convenience delegates
67
+ def list_spaces(self, **kw):
68
+ return self.spaces_posts.list_spaces(**kw)
69
+
70
+ def list_posts(self, space_id, **kw):
71
+ return self.spaces_posts.list_posts(space_id, **kw)
72
+
73
+
74
+ class _AsyncHeadlessNamespace:
75
+ def __init__(self, transport: AsyncTransport) -> None:
76
+ self.spaces_posts = AsyncHeadlessSpacesPostsClient(transport)
77
+ self.chat_notif_members = AsyncHeadlessChatNotifMembersClient(transport)
78
+ self.misc = AsyncHeadlessMiscClient(transport)
79
+
80
+ async def list_spaces(self, **kw):
81
+ return await self.spaces_posts.list_spaces(**kw)
82
+
83
+ async def list_posts(self, space_id, **kw):
84
+ return await self.spaces_posts.list_posts(space_id, **kw)
85
+
86
+
87
+ class CircleClient:
88
+ """Synchronous Circle SDK client composing all three APIs.
89
+
90
+ Provides access to the Headless Auth, Admin V2, and Headless Client V1
91
+ APIs through the ``auth``, ``admin``, and ``headless`` namespaces.
92
+
93
+ Args:
94
+ api_token: Circle API token. Falls back to the ``CIRCLE_API_TOKEN``
95
+ environment variable if not provided.
96
+ base_url: Base URL for the Circle API. Falls back to the
97
+ ``CIRCLE_BASE_URL`` environment variable, then the default
98
+ ``https://app.circle.so``.
99
+ community_url: Optional community-specific URL. Falls back to the
100
+ ``CIRCLE_COMMUNITY_URL`` environment variable.
101
+ rate_limit: Optional requests-per-second limit. When set, the client
102
+ proactively throttles to avoid 429 responses.
103
+
104
+ Raises:
105
+ ValueError: If no api_token is provided and ``CIRCLE_API_TOKEN`` is
106
+ not set.
107
+
108
+ Examples:
109
+ >>> client = CircleClient(api_token="YOUR_TOKEN")
110
+ >>> community = client.admin.get_community()
111
+
112
+ >>> # Using environment variables (CIRCLE_API_TOKEN must be set)
113
+ >>> client = CircleClient()
114
+
115
+ >>> # With rate limiting
116
+ >>> client = CircleClient(api_token="YOUR_TOKEN", rate_limit=10)
117
+ """
118
+
119
+ def __init__(
120
+ self,
121
+ api_token: Optional[str] = None,
122
+ base_url: Optional[str] = None,
123
+ community_url: Optional[str] = None,
124
+ rate_limit: Optional[float] = None,
125
+ ) -> None:
126
+ token = api_token or os.environ.get("CIRCLE_API_TOKEN")
127
+ if not token:
128
+ raise ValueError(
129
+ "api_token is required. Pass it directly or set the CIRCLE_API_TOKEN environment variable."
130
+ )
131
+ resolved_base = base_url or os.environ.get("CIRCLE_BASE_URL", DEFAULT_BASE_URL)
132
+ resolved_community = community_url or os.environ.get("CIRCLE_COMMUNITY_URL")
133
+ url = (resolved_community or resolved_base).rstrip("/")
134
+ self._admin_transport = SyncTransport(api_token=token, base_url=url, auth_scheme="Token", rate_limit=rate_limit)
135
+ self._bearer_transport = SyncTransport(api_token=token, base_url=url, auth_scheme="Bearer", rate_limit=rate_limit)
136
+ self.auth = HeadlessAuthClient(self._bearer_transport)
137
+ self.admin = _AdminNamespace(self._admin_transport)
138
+ self.headless = _HeadlessNamespace(self._bearer_transport)
139
+
140
+ def close(self) -> None:
141
+ self._admin_transport.close()
142
+ self._bearer_transport.close()
143
+
144
+ def __enter__(self) -> CircleClient:
145
+ return self
146
+
147
+ def __exit__(self, *args: object) -> None:
148
+ self.close()
149
+
150
+
151
+ class AsyncCircleClient:
152
+ """Asynchronous Circle SDK client composing all three APIs.
153
+
154
+ Args:
155
+ api_token: Circle API token. Falls back to ``CIRCLE_API_TOKEN`` env var.
156
+ base_url: Base URL. Falls back to ``CIRCLE_BASE_URL`` env var.
157
+ community_url: Community URL. Falls back to ``CIRCLE_COMMUNITY_URL`` env var.
158
+ rate_limit: Optional requests-per-second limit.
159
+
160
+ Raises:
161
+ ValueError: If no api_token is provided and ``CIRCLE_API_TOKEN`` is not set.
162
+
163
+ Examples:
164
+ >>> async with AsyncCircleClient(api_token="YOUR_TOKEN") as client:
165
+ ... members = await client.admin.list_community_members()
166
+ """
167
+
168
+ def __init__(
169
+ self,
170
+ api_token: Optional[str] = None,
171
+ base_url: Optional[str] = None,
172
+ community_url: Optional[str] = None,
173
+ rate_limit: Optional[float] = None,
174
+ ) -> None:
175
+ token = api_token or os.environ.get("CIRCLE_API_TOKEN")
176
+ if not token:
177
+ raise ValueError(
178
+ "api_token is required. Pass it directly or set the CIRCLE_API_TOKEN environment variable."
179
+ )
180
+ resolved_base = base_url or os.environ.get("CIRCLE_BASE_URL", DEFAULT_BASE_URL)
181
+ resolved_community = community_url or os.environ.get("CIRCLE_COMMUNITY_URL")
182
+ url = (resolved_community or resolved_base).rstrip("/")
183
+ self._admin_transport = AsyncTransport(api_token=token, base_url=url, auth_scheme="Token", rate_limit=rate_limit)
184
+ self._bearer_transport = AsyncTransport(api_token=token, base_url=url, auth_scheme="Bearer", rate_limit=rate_limit)
185
+ self.auth = AsyncHeadlessAuthClient(self._bearer_transport)
186
+ self.admin = _AsyncAdminNamespace(self._admin_transport)
187
+ self.headless = _AsyncHeadlessNamespace(self._bearer_transport)
188
+
189
+ async def close(self) -> None:
190
+ await self._admin_transport.close()
191
+ await self._bearer_transport.close()
192
+
193
+ async def __aenter__(self) -> AsyncCircleClient:
194
+ return self
195
+
196
+ async def __aexit__(self, *args: object) -> None:
197
+ await self.close()
circle/constants.py ADDED
@@ -0,0 +1,5 @@
1
+ """Centralized constants for Circle API endpoints."""
2
+
3
+ ADMIN_V2_PREFIX = "/api/admin/v2"
4
+ HEADLESS_V1_PREFIX = "/api/headless/v1"
5
+ HEADLESS_AUTH_PREFIX = "/api/v1/headless"
circle/exceptions.py ADDED
@@ -0,0 +1,34 @@
1
+ """Typed exceptions for Circle API errors."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ class CircleAPIError(Exception):
9
+ """Base exception for all Circle API errors."""
10
+
11
+ def __init__(self, message: str, status_code: int | None = None, error_details: Any = None) -> None:
12
+ self.status_code = status_code
13
+ self.error_details = error_details
14
+ super().__init__(message)
15
+
16
+
17
+ class AuthenticationError(CircleAPIError):
18
+ """401 Unauthorized."""
19
+
20
+
21
+ class ForbiddenError(CircleAPIError):
22
+ """403 Forbidden."""
23
+
24
+
25
+ class NotFoundError(CircleAPIError):
26
+ """404 Not Found."""
27
+
28
+
29
+ class ValidationError(CircleAPIError):
30
+ """422 Unprocessable Entity."""
31
+
32
+
33
+ class RateLimitError(CircleAPIError):
34
+ """429 Too Many Requests."""