pagekit-core 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.
pagekit/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ from .client import AsyncPagekit, Pagekit
2
+ from .errors import PagekitError
3
+ from .http import DEFAULT_BASE_URL
4
+ from .types import (
5
+ Author,
6
+ AuthorListParams,
7
+ Category,
8
+ CategoryListParams,
9
+ Media,
10
+ MediaCreateInput,
11
+ MediaListParams,
12
+ Paginated,
13
+ Pagination,
14
+ Post,
15
+ PostCreateInput,
16
+ PostListParams,
17
+ PostStatus,
18
+ PostUpdateInput,
19
+ Seo,
20
+ Tag,
21
+ TagListParams,
22
+ )
23
+
24
+ __all__ = [
25
+ "AsyncPagekit",
26
+ "DEFAULT_BASE_URL",
27
+ "Pagekit",
28
+ "PagekitError",
29
+ "Author",
30
+ "AuthorListParams",
31
+ "Category",
32
+ "CategoryListParams",
33
+ "Media",
34
+ "MediaCreateInput",
35
+ "MediaListParams",
36
+ "Paginated",
37
+ "Pagination",
38
+ "Post",
39
+ "PostCreateInput",
40
+ "PostListParams",
41
+ "PostStatus",
42
+ "PostUpdateInput",
43
+ "Seo",
44
+ "Tag",
45
+ "TagListParams",
46
+ ]
pagekit/client.py ADDED
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from .errors import PagekitError
6
+ from .http import DEFAULT_BASE_URL, AsyncHttpClient, SyncHttpClient
7
+ from .resources.authors import AsyncAuthorsResource, SyncAuthorsResource
8
+ from .resources.categories import AsyncCategoriesResource, SyncCategoriesResource
9
+ from .resources.media import AsyncMediaResource, SyncMediaResource
10
+ from .resources.posts import AsyncPostsResource, SyncPostsResource
11
+ from .resources.tags import AsyncTagsResource, SyncTagsResource
12
+
13
+
14
+ class AsyncPagekit:
15
+ """Async Pagekit content API client."""
16
+
17
+ def __init__(
18
+ self,
19
+ *,
20
+ api_key: str,
21
+ base_url: str = DEFAULT_BASE_URL,
22
+ timeout: float = 30,
23
+ headers: dict[str, str] | None = None,
24
+ http_client: httpx.AsyncClient | None = None,
25
+ ) -> None:
26
+ self._http = AsyncHttpClient(
27
+ api_key=api_key,
28
+ base_url=base_url,
29
+ timeout=timeout,
30
+ headers=headers,
31
+ http_client=http_client,
32
+ )
33
+ self.posts = AsyncPostsResource(self._http)
34
+ self.authors = AsyncAuthorsResource(self._http)
35
+ self.categories = AsyncCategoriesResource(self._http)
36
+ self.tags = AsyncTagsResource(self._http)
37
+ self.media = AsyncMediaResource(self._http)
38
+
39
+ async def close(self) -> None:
40
+ await self._http._client.aclose()
41
+
42
+ async def __aenter__(self) -> AsyncPagekit:
43
+ return self
44
+
45
+ async def __aexit__(self, *args: object) -> None:
46
+ await self.close()
47
+
48
+
49
+ class Pagekit:
50
+ """Synchronous Pagekit content API client."""
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ api_key: str,
56
+ base_url: str = DEFAULT_BASE_URL,
57
+ timeout: float = 30,
58
+ headers: dict[str, str] | None = None,
59
+ http_client: httpx.Client | None = None,
60
+ ) -> None:
61
+ self._http = SyncHttpClient(
62
+ api_key=api_key,
63
+ base_url=base_url,
64
+ timeout=timeout,
65
+ headers=headers,
66
+ http_client=http_client,
67
+ )
68
+ self.posts = SyncPostsResource(self._http)
69
+ self.authors = SyncAuthorsResource(self._http)
70
+ self.categories = SyncCategoriesResource(self._http)
71
+ self.tags = SyncTagsResource(self._http)
72
+ self.media = SyncMediaResource(self._http)
73
+
74
+ def close(self) -> None:
75
+ self._http._client.close()
76
+
77
+ def __enter__(self) -> Pagekit:
78
+ return self
79
+
80
+ def __exit__(self, *args: object) -> None:
81
+ self.close()
pagekit/errors.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class PagekitError(Exception):
7
+ """Error returned by the Pagekit API."""
8
+
9
+ def __init__(
10
+ self,
11
+ message: str,
12
+ *,
13
+ status: int = 0,
14
+ code: str = "api_error",
15
+ details: Any = None,
16
+ ) -> None:
17
+ super().__init__(message)
18
+ self.status = status
19
+ self.code = code
20
+ self.details = details
21
+
22
+ @property
23
+ def is_auth_error(self) -> bool:
24
+ return self.status in (401, 403)
25
+
26
+ @property
27
+ def is_rate_limited(self) -> bool:
28
+ return self.status == 429
29
+
30
+ @property
31
+ def is_server_error(self) -> bool:
32
+ return self.status >= 500
pagekit/http.py ADDED
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol
4
+
5
+ import httpx
6
+
7
+ from .errors import PagekitError
8
+
9
+ DEFAULT_BASE_URL = "https://api.pagekit.app/v1"
10
+
11
+
12
+ class HttpClient(Protocol):
13
+ async def request(
14
+ self, path: str, *, method: str = "GET", query: dict[str, Any] | None = None, body: Any = None
15
+ ) -> Any: ...
16
+
17
+
18
+ def _to_camel(key: str) -> str:
19
+ parts = key.split("_")
20
+ return parts[0] + "".join(w.capitalize() for w in parts[1:])
21
+
22
+
23
+ def _serialize_params(obj: Any) -> Any:
24
+ if hasattr(obj, "__dataclass_fields__"):
25
+ result = {}
26
+ for k, v in obj.__dataclass_fields__.items(): # type: ignore[attr-defined]
27
+ val = getattr(obj, k)
28
+ if val is not None:
29
+ result[_to_camel(k)] = _serialize_params(val)
30
+ return result
31
+ if isinstance(obj, dict):
32
+ return {k: _serialize_params(v) for k, v in obj.items() if v is not None}
33
+ if isinstance(obj, list):
34
+ return [_serialize_params(v) for v in obj]
35
+ if hasattr(obj, "value"):
36
+ return obj.value
37
+ return obj
38
+
39
+
40
+ def _build_query(params: Any) -> dict[str, str]:
41
+ if params is None:
42
+ return {}
43
+ raw = _serialize_params(params) if hasattr(params, "__dataclass_fields__") else params or {}
44
+ return {k: str(v) for k, v in raw.items() if v is not None and v != ""}
45
+
46
+
47
+ def _unwrap_type(ft: Any) -> Any:
48
+ """Unwrap Optional/Union types to get the inner dataclass type."""
49
+ import typing
50
+
51
+ origin = getattr(ft, "__origin__", None)
52
+ if origin is typing.Union:
53
+ args = [a for a in ft.__args__ if a is not type(None)]
54
+ if len(args) == 1:
55
+ return args[0]
56
+ return ft
57
+
58
+
59
+ def _deserialize(data: dict[str, Any], cls: type) -> Any:
60
+ if not isinstance(data, dict):
61
+ return data
62
+ import dataclasses
63
+
64
+ if not dataclasses.is_dataclass(cls):
65
+ return data
66
+
67
+ field_map = {f.name: f for f in dataclasses.fields(cls)}
68
+ kwargs = {}
69
+ for f in dataclasses.fields(cls):
70
+ snake = f.name
71
+ camel = _to_camel(snake)
72
+ raw = data.get(camel, data.get(snake))
73
+ if raw is None:
74
+ continue
75
+ ft = f.type
76
+ if isinstance(ft, str):
77
+ import typing
78
+
79
+ ft = typing.get_type_hints(cls).get(snake, f.type)
80
+ inner = _unwrap_type(ft)
81
+ if dataclasses.is_dataclass(inner) and isinstance(raw, dict):
82
+ kwargs[snake] = _deserialize(raw, inner)
83
+ else:
84
+ kwargs[snake] = raw
85
+ return cls(**kwargs)
86
+
87
+
88
+ def _extract_message(body: Any) -> str:
89
+ if isinstance(body, dict):
90
+ err = body.get("error")
91
+ if isinstance(err, dict) and "message" in err:
92
+ return err["message"]
93
+ if "message" in body:
94
+ return body["message"]
95
+ return "Unknown error"
96
+
97
+
98
+ def _extract_code(body: Any) -> str:
99
+ if isinstance(body, dict):
100
+ err = body.get("error")
101
+ if isinstance(err, dict) and "code" in err:
102
+ return err["code"]
103
+ if "code" in body:
104
+ return body["code"]
105
+ return "api_error"
106
+
107
+
108
+ class AsyncHttpClient:
109
+ def __init__(
110
+ self,
111
+ *,
112
+ api_key: str,
113
+ base_url: str = DEFAULT_BASE_URL,
114
+ timeout: float = 30,
115
+ headers: dict[str, str] | None = None,
116
+ http_client: httpx.AsyncClient | None = None,
117
+ ) -> None:
118
+ if not api_key:
119
+ raise PagekitError("api_key is required", code="missing_api_key")
120
+ self.api_key = api_key
121
+ self.base_url = base_url.rstrip("/")
122
+ self._default_headers = {
123
+ "Authorization": f"Bearer {api_key}",
124
+ "Accept": "application/json",
125
+ **(headers or {}),
126
+ }
127
+ self._client = http_client or httpx.AsyncClient(
128
+ timeout=httpx.Timeout(timeout),
129
+ )
130
+
131
+ async def request(
132
+ self,
133
+ path: str,
134
+ *,
135
+ method: str = "GET",
136
+ query: dict[str, Any] | None = None,
137
+ body: Any = None,
138
+ ) -> Any:
139
+ url = f"{self.base_url}{path}"
140
+ headers = {**self._default_headers}
141
+ if body is not None:
142
+ headers["Content-Type"] = "application/json"
143
+
144
+ try:
145
+ resp = await self._client.request(
146
+ method,
147
+ url,
148
+ headers=headers,
149
+ params=_build_query(query) or None,
150
+ content=__import__("json").dumps(_serialize_params(body)) if body is not None else None,
151
+ )
152
+ except httpx.TimeoutException:
153
+ raise PagekitError("Request timed out", code="timeout")
154
+ except httpx.HTTPError as exc:
155
+ raise PagekitError(f"Network error: {exc}", code="network_error")
156
+
157
+ if resp.status_code == 204:
158
+ return None
159
+
160
+ try:
161
+ data = resp.json()
162
+ except Exception:
163
+ data = None
164
+
165
+ if not resp.is_success:
166
+ raise PagekitError(
167
+ _extract_message(data),
168
+ status=resp.status_code,
169
+ code=_extract_code(data),
170
+ details=data,
171
+ )
172
+
173
+ return data
174
+
175
+
176
+ class SyncHttpClient:
177
+ def __init__(
178
+ self,
179
+ *,
180
+ api_key: str,
181
+ base_url: str = DEFAULT_BASE_URL,
182
+ timeout: float = 30,
183
+ headers: dict[str, str] | None = None,
184
+ http_client: httpx.Client | None = None,
185
+ ) -> None:
186
+ if not api_key:
187
+ raise PagekitError("api_key is required", code="missing_api_key")
188
+ self.api_key = api_key
189
+ self.base_url = base_url.rstrip("/")
190
+ self._default_headers = {
191
+ "Authorization": f"Bearer {api_key}",
192
+ "Accept": "application/json",
193
+ **(headers or {}),
194
+ }
195
+ self._client = http_client or httpx.Client(
196
+ timeout=httpx.Timeout(timeout),
197
+ )
198
+
199
+ def request(
200
+ self,
201
+ path: str,
202
+ *,
203
+ method: str = "GET",
204
+ query: dict[str, Any] | None = None,
205
+ body: Any = None,
206
+ ) -> Any:
207
+ url = f"{self.base_url}{path}"
208
+ headers = {**self._default_headers}
209
+ if body is not None:
210
+ headers["Content-Type"] = "application/json"
211
+
212
+ try:
213
+ resp = self._client.request(
214
+ method,
215
+ url,
216
+ headers=headers,
217
+ params=_build_query(query) or None,
218
+ content=__import__("json").dumps(_serialize_params(body)) if body is not None else None,
219
+ )
220
+ except httpx.TimeoutException:
221
+ raise PagekitError("Request timed out", code="timeout")
222
+ except httpx.HTTPError as exc:
223
+ raise PagekitError(f"Network error: {exc}", code="network_error")
224
+
225
+ if resp.status_code == 204:
226
+ return None
227
+
228
+ try:
229
+ data = resp.json()
230
+ except Exception:
231
+ data = None
232
+
233
+ if not resp.is_success:
234
+ raise PagekitError(
235
+ _extract_message(data),
236
+ status=resp.status_code,
237
+ code=_extract_code(data),
238
+ details=data,
239
+ )
240
+
241
+ return data
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ from ..resources.authors import AsyncAuthorsResource, SyncAuthorsResource
4
+ from ..resources.categories import AsyncCategoriesResource, SyncCategoriesResource
5
+ from ..resources.media import AsyncMediaResource, SyncMediaResource
6
+ from ..resources.posts import AsyncPostsResource, SyncPostsResource
7
+ from ..resources.tags import AsyncTagsResource, SyncTagsResource
8
+
9
+ __all__ = [
10
+ "AsyncAuthorsResource",
11
+ "AsyncCategoriesResource",
12
+ "AsyncMediaResource",
13
+ "AsyncPostsResource",
14
+ "AsyncTagsResource",
15
+ "SyncAuthorsResource",
16
+ "SyncCategoriesResource",
17
+ "SyncMediaResource",
18
+ "SyncPostsResource",
19
+ "SyncTagsResource",
20
+ ]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import quote
4
+
5
+ from ..http import AsyncHttpClient, SyncHttpClient, _build_query, _deserialize
6
+ from ..types import Author, AuthorListParams, Paginated, Pagination
7
+
8
+
9
+ class AsyncAuthorsResource:
10
+ def __init__(self, http: AsyncHttpClient) -> None:
11
+ self._http = http
12
+
13
+ async def list(self, params: AuthorListParams | None = None) -> Paginated[Author]:
14
+ query = _build_query(params) if params else {}
15
+ raw = await self._http.request("/authors", query=query)
16
+ return Paginated(
17
+ data=[_deserialize(a, Author) for a in raw["data"]],
18
+ pagination=_deserialize(raw["pagination"], Pagination),
19
+ )
20
+
21
+ async def get(self, id: str) -> Author:
22
+ raw = await self._http.request(f"/authors/{quote(id, safe='')}")
23
+ return _deserialize(raw, Author)
24
+
25
+
26
+ class SyncAuthorsResource:
27
+ def __init__(self, http: SyncHttpClient) -> None:
28
+ self._http = http
29
+
30
+ def list(self, params: AuthorListParams | None = None) -> Paginated[Author]:
31
+ query = _build_query(params) if params else {}
32
+ raw = self._http.request("/authors", query=query)
33
+ return Paginated(
34
+ data=[_deserialize(a, Author) for a in raw["data"]],
35
+ pagination=_deserialize(raw["pagination"], Pagination),
36
+ )
37
+
38
+ def get(self, id: str) -> Author:
39
+ raw = self._http.request(f"/authors/{quote(id, safe='')}")
40
+ return _deserialize(raw, Author)
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import quote
4
+
5
+ from ..http import AsyncHttpClient, SyncHttpClient, _build_query, _deserialize
6
+ from ..types import Category, CategoryListParams, Paginated, Pagination
7
+
8
+
9
+ class AsyncCategoriesResource:
10
+ def __init__(self, http: AsyncHttpClient) -> None:
11
+ self._http = http
12
+
13
+ async def list(self, params: CategoryListParams | None = None) -> Paginated[Category]:
14
+ query = _build_query(params) if params else {}
15
+ raw = await self._http.request("/categories", query=query)
16
+ return Paginated(
17
+ data=[_deserialize(c, Category) for c in raw["data"]],
18
+ pagination=_deserialize(raw["pagination"], Pagination),
19
+ )
20
+
21
+ async def get_by_slug(self, slug: str) -> Category:
22
+ raw = await self._http.request(f"/categories/{quote(slug, safe='')}")
23
+ return _deserialize(raw, Category)
24
+
25
+
26
+ class SyncCategoriesResource:
27
+ def __init__(self, http: SyncHttpClient) -> None:
28
+ self._http = http
29
+
30
+ def list(self, params: CategoryListParams | None = None) -> Paginated[Category]:
31
+ query = _build_query(params) if params else {}
32
+ raw = self._http.request("/categories", query=query)
33
+ return Paginated(
34
+ data=[_deserialize(c, Category) for c in raw["data"]],
35
+ pagination=_deserialize(raw["pagination"], Pagination),
36
+ )
37
+
38
+ def get_by_slug(self, slug: str) -> Category:
39
+ raw = self._http.request(f"/categories/{quote(slug, safe='')}")
40
+ return _deserialize(raw, Category)
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import quote
4
+
5
+ from ..http import AsyncHttpClient, SyncHttpClient, _build_query, _deserialize
6
+ from ..types import Media, MediaCreateInput, MediaListParams, Paginated, Pagination
7
+
8
+
9
+ class AsyncMediaResource:
10
+ def __init__(self, http: AsyncHttpClient) -> None:
11
+ self._http = http
12
+
13
+ async def list(self, params: MediaListParams | None = None) -> Paginated[Media]:
14
+ query = _build_query(params) if params else {}
15
+ raw = await self._http.request("/media", query=query)
16
+ return Paginated(
17
+ data=[_deserialize(m, Media) for m in raw["data"]],
18
+ pagination=_deserialize(raw["pagination"], Pagination),
19
+ )
20
+
21
+ async def create(self, input: MediaCreateInput) -> Media:
22
+ raw = await self._http.request("/media", method="POST", body=input)
23
+ return _deserialize(raw, Media)
24
+
25
+ async def delete(self, id: str) -> None:
26
+ await self._http.request(f"/media/{quote(id, safe='')}", method="DELETE")
27
+
28
+
29
+ class SyncMediaResource:
30
+ def __init__(self, http: SyncHttpClient) -> None:
31
+ self._http = http
32
+
33
+ def list(self, params: MediaListParams | None = None) -> Paginated[Media]:
34
+ query = _build_query(params) if params else {}
35
+ raw = self._http.request("/media", query=query)
36
+ return Paginated(
37
+ data=[_deserialize(m, Media) for m in raw["data"]],
38
+ pagination=_deserialize(raw["pagination"], Pagination),
39
+ )
40
+
41
+ def create(self, input: MediaCreateInput) -> Media:
42
+ raw = self._http.request("/media", method="POST", body=input)
43
+ return _deserialize(raw, Media)
44
+
45
+ def delete(self, id: str) -> None:
46
+ self._http.request(f"/media/{quote(id, safe='')}", method="DELETE")
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import quote
4
+
5
+ from ..http import AsyncHttpClient, SyncHttpClient, _build_query, _deserialize
6
+ from ..types import (
7
+ Author,
8
+ AuthorListParams,
9
+ Paginated,
10
+ Pagination,
11
+ Post,
12
+ PostCreateInput,
13
+ PostListParams,
14
+ PostUpdateInput,
15
+ )
16
+
17
+
18
+ class AsyncPostsResource:
19
+ def __init__(self, http: AsyncHttpClient) -> None:
20
+ self._http = http
21
+
22
+ async def list(self, params: PostListParams | None = None) -> Paginated[Post]:
23
+ query = _build_query(params) if params else {}
24
+ raw = await self._http.request("/posts", query=query)
25
+ return Paginated(
26
+ data=[_deserialize(p, Post) for p in raw["data"]],
27
+ pagination=_deserialize(raw["pagination"], Pagination),
28
+ )
29
+
30
+ async def get(self, id: str) -> Post:
31
+ raw = await self._http.request(f"/posts/{quote(id, safe='')}")
32
+ return _deserialize(raw, Post)
33
+
34
+ async def get_by_slug(self, slug: str) -> Post:
35
+ raw = await self._http.request(f"/posts/slug/{quote(slug, safe='')}")
36
+ return _deserialize(raw, Post)
37
+
38
+ async def create(self, input: PostCreateInput) -> Post:
39
+ raw = await self._http.request("/posts", method="POST", body=input)
40
+ return _deserialize(raw, Post)
41
+
42
+ async def update(self, id: str, input: PostUpdateInput) -> Post:
43
+ raw = await self._http.request(f"/posts/{quote(id, safe='')}", method="PATCH", body=input)
44
+ return _deserialize(raw, Post)
45
+
46
+ async def delete(self, id: str) -> None:
47
+ await self._http.request(f"/posts/{quote(id, safe='')}", method="DELETE")
48
+
49
+
50
+ class SyncPostsResource:
51
+ def __init__(self, http: SyncHttpClient) -> None:
52
+ self._http = http
53
+
54
+ def list(self, params: PostListParams | None = None) -> Paginated[Post]:
55
+ query = _build_query(params) if params else {}
56
+ raw = self._http.request("/posts", query=query)
57
+ return Paginated(
58
+ data=[_deserialize(p, Post) for p in raw["data"]],
59
+ pagination=_deserialize(raw["pagination"], Pagination),
60
+ )
61
+
62
+ def get(self, id: str) -> Post:
63
+ raw = self._http.request(f"/posts/{quote(id, safe='')}")
64
+ return _deserialize(raw, Post)
65
+
66
+ def get_by_slug(self, slug: str) -> Post:
67
+ raw = self._http.request(f"/posts/slug/{quote(slug, safe='')}")
68
+ return _deserialize(raw, Post)
69
+
70
+ def create(self, input: PostCreateInput) -> Post:
71
+ raw = self._http.request("/posts", method="POST", body=input)
72
+ return _deserialize(raw, Post)
73
+
74
+ def update(self, id: str, input: PostUpdateInput) -> Post:
75
+ raw = self._http.request(f"/posts/{quote(id, safe='')}", method="PATCH", body=input)
76
+ return _deserialize(raw, Post)
77
+
78
+ def delete(self, id: str) -> None:
79
+ self._http.request(f"/posts/{quote(id, safe='')}", method="DELETE")
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from ..http import AsyncHttpClient, SyncHttpClient, _build_query, _deserialize
4
+ from ..types import Paginated, Pagination, Tag, TagListParams
5
+
6
+
7
+ class AsyncTagsResource:
8
+ def __init__(self, http: AsyncHttpClient) -> None:
9
+ self._http = http
10
+
11
+ async def list(self, params: TagListParams | None = None) -> Paginated[Tag]:
12
+ query = _build_query(params) if params else {}
13
+ raw = await self._http.request("/tags", query=query)
14
+ return Paginated(
15
+ data=[_deserialize(t, Tag) for t in raw["data"]],
16
+ pagination=_deserialize(raw["pagination"], Pagination),
17
+ )
18
+
19
+
20
+ class SyncTagsResource:
21
+ def __init__(self, http: SyncHttpClient) -> None:
22
+ self._http = http
23
+
24
+ def list(self, params: TagListParams | None = None) -> Paginated[Tag]:
25
+ query = _build_query(params) if params else {}
26
+ raw = self._http.request("/tags", query=query)
27
+ return Paginated(
28
+ data=[_deserialize(t, Tag) for t in raw["data"]],
29
+ pagination=_deserialize(raw["pagination"], Pagination),
30
+ )
pagekit/types.py ADDED
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Generic, Literal, TypeVar
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ class PostStatus(str, Enum):
11
+ DRAFT = "draft"
12
+ SCHEDULED = "scheduled"
13
+ PUBLISHED = "published"
14
+ ARCHIVED = "archived"
15
+
16
+
17
+ @dataclass
18
+ class Seo:
19
+ title: str | None = None
20
+ description: str | None = None
21
+ canonical_url: str | None = None
22
+ og_image: str | None = None
23
+
24
+
25
+ @dataclass
26
+ class Author:
27
+ id: str
28
+ name: str
29
+ email: str | None = None
30
+ bio: str | None = None
31
+ avatar_url: str | None = None
32
+ created_at: str | None = None
33
+ updated_at: str | None = None
34
+
35
+
36
+ @dataclass
37
+ class Category:
38
+ id: str
39
+ name: str
40
+ slug: str
41
+ description: str | None = None
42
+
43
+
44
+ @dataclass
45
+ class Tag:
46
+ id: str
47
+ name: str
48
+ slug: str
49
+
50
+
51
+ @dataclass
52
+ class Media:
53
+ id: str
54
+ url: str
55
+ filename: str
56
+ mime_type: str
57
+ size: int
58
+ width: int | None = None
59
+ height: int | None = None
60
+ alt: str | None = None
61
+ created_at: str | None = None
62
+
63
+
64
+ @dataclass
65
+ class AuthorRef:
66
+ id: str
67
+ name: str
68
+
69
+
70
+ @dataclass
71
+ class CategoryRef:
72
+ id: str
73
+ name: str
74
+ slug: str
75
+
76
+
77
+ @dataclass
78
+ class Post:
79
+ id: str
80
+ title: str
81
+ slug: str
82
+ content: str
83
+ excerpt: str | None = None
84
+ cover_image: str | None = None
85
+ status: PostStatus = PostStatus.DRAFT
86
+ published_at: str | None = None
87
+ scheduled_for: str | None = None
88
+ author: AuthorRef | None = None
89
+ category: CategoryRef | None = None
90
+ tags: list[str] = field(default_factory=list)
91
+ seo: Seo = field(default_factory=Seo)
92
+ created_at: str = ""
93
+ updated_at: str = ""
94
+
95
+
96
+ @dataclass
97
+ class Pagination:
98
+ page: int
99
+ limit: int
100
+ total: int
101
+ total_pages: int
102
+ has_next_page: bool
103
+ has_previous_page: bool
104
+
105
+
106
+ @dataclass
107
+ class Paginated(Generic[T]):
108
+ data: list[T]
109
+ pagination: Pagination
110
+
111
+
112
+ @dataclass
113
+ class PostCreateInput:
114
+ title: str
115
+ content: str | None = None
116
+ slug: str | None = None
117
+ excerpt: str | None = None
118
+ cover_image: str | None = None
119
+ status: PostStatus | None = None
120
+ author_id: str | None = None
121
+ category_id: str | None = None
122
+ tags: list[str] | None = None
123
+ seo_title: str | None = None
124
+ seo_description: str | None = None
125
+ canonical_url: str | None = None
126
+ og_image: str | None = None
127
+ published_at: str | None = None
128
+ scheduled_for: str | None = None
129
+
130
+
131
+ @dataclass
132
+ class PostUpdateInput:
133
+ title: str | None = None
134
+ content: str | None = None
135
+ slug: str | None = None
136
+ excerpt: str | None = None
137
+ cover_image: str | None = None
138
+ status: PostStatus | None = None
139
+ author_id: str | None = None
140
+ category_id: str | None = None
141
+ tags: list[str] | None = None
142
+ seo_title: str | None = None
143
+ seo_description: str | None = None
144
+ canonical_url: str | None = None
145
+ og_image: str | None = None
146
+ published_at: str | None = None
147
+ scheduled_for: str | None = None
148
+
149
+
150
+ @dataclass
151
+ class PostListParams:
152
+ status: PostStatus | None = None
153
+ category: str | None = None
154
+ tag: str | None = None
155
+ author: str | None = None
156
+ search: str | None = None
157
+ page: int | None = None
158
+ limit: int | None = None
159
+ sort: str | None = None
160
+
161
+
162
+ @dataclass
163
+ class CategoryListParams:
164
+ page: int | None = None
165
+ limit: int | None = None
166
+ sort: str | None = None
167
+
168
+
169
+ @dataclass
170
+ class TagListParams:
171
+ page: int | None = None
172
+ limit: int | None = None
173
+ sort: str | None = None
174
+
175
+
176
+ @dataclass
177
+ class AuthorListParams:
178
+ page: int | None = None
179
+ limit: int | None = None
180
+ sort: str | None = None
181
+
182
+
183
+ @dataclass
184
+ class MediaListParams:
185
+ page: int | None = None
186
+ limit: int | None = None
187
+ sort: str | None = None
188
+
189
+
190
+ @dataclass
191
+ class MediaCreateInput:
192
+ url: str
193
+ filename: str
194
+ mime_type: str
195
+ size: int
196
+ width: int | None = None
197
+ height: int | None = None
198
+ alt: str | None = None
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.5
2
+ Name: pagekit-core
3
+ Version: 0.1.0
4
+ Summary: Python client for the Pagekit content API
5
+ Project-URL: Homepage, https://github.com/arovi-labs/pagekit
6
+ Project-URL: Repository, https://github.com/arovi-labs/pagekit
7
+ Project-URL: Issues, https://github.com/arovi-labs/pagekit/issues
8
+ Author: Arovi Labs
9
+ License-Expression: MIT
10
+ Keywords: blog,cms,content-api,headless,pagekit
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx<1,>=0.27
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Requires-Dist: respx>=0.22; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # pagekit
29
+
30
+ Python client for the [Pagekit](https://pagekit.app) content API.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install pagekit-core
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ ```python
41
+ from pagekit import Pagekit
42
+
43
+ client = Pagekit(api_key="pk_live_...")
44
+
45
+ # List posts
46
+ page = client.posts.list(status="published", limit=10)
47
+ for post in page.data:
48
+ print(post.title)
49
+
50
+ # Create a post
51
+ post = client.posts.create(title="Hello world", content="<p>First post</p>")
52
+
53
+ # Get by slug
54
+ post = client.posts.get_by_slug("hello-world")
55
+ ```
56
+
57
+ ## Async usage
58
+
59
+ ```python
60
+ from pagekit import AsyncPagekit
61
+
62
+ client = AsyncPagekit(api_key="pk_live_...")
63
+
64
+ posts = await client.posts.list()
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ ```python
70
+ client = Pagekit(
71
+ api_key="pk_live_...",
72
+ base_url="https://api.pagekit.app/v1", # default
73
+ timeout=30, # seconds
74
+ headers={"X-Custom": "value"}, # extra headers
75
+ )
76
+ ```
77
+
78
+ ## Error handling
79
+
80
+ ```python
81
+ from pagekit import Pagekit, PagekitError
82
+
83
+ client = Pagekit(api_key="pk_live_...")
84
+
85
+ try:
86
+ post = client.posts.get("nonexistent")
87
+ except PagekitError as e:
88
+ print(e.status) # 404
89
+ print(e.code) # "not_found"
90
+ print(e.is_auth_error) # False
91
+ print(e.is_rate_limited) # False
92
+ print(e.is_server_error) # False
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,14 @@
1
+ pagekit/__init__.py,sha256=jE05i4cI0oryMWbFs298JaY972zRC4YgUbaeLk4qS-4,825
2
+ pagekit/client.py,sha256=y73g0cBOIXnlKU8FNIBNBSrjIU4c8fHmVXNKANqbzTE,2502
3
+ pagekit/errors.py,sha256=Nqe6lyD_pM8sjoE9_w0yBzBhohhOXRIMyln6_mag80U,693
4
+ pagekit/http.py,sha256=io5rREnmoveMBUlYoM1F9efu1lDzZajzwT5-lNZQjCo,7086
5
+ pagekit/types.py,sha256=Pt9jxCmsFfl6CtWjTrJLsIbU4f8pP3GBGwHjRugz2ts,3919
6
+ pagekit/resources/__init__.py,sha256=ehZvtw7SqfiZATwsV_18n6kGRhkKPfVH_0hY-is2ugc,676
7
+ pagekit/resources/authors.py,sha256=yuts2yoPfHAVGB04b54WuVOTIItpYA7BCURS3QPyXDs,1481
8
+ pagekit/resources/categories.py,sha256=ZVa4y8ku_8GCd4QCTk-HDeYiX2DCiuWxyi7uhIt6gxA,1547
9
+ pagekit/resources/media.py,sha256=LKRgXpH3qw_EiAyQtDAenIBzdvYdM5yZ-l4mjoXJQLs,1767
10
+ pagekit/resources/posts.py,sha256=HompTUpIqCp8U72OxmIZnaxhS-MStm8P1ea1_H1CWTA,2858
11
+ pagekit/resources/tags.py,sha256=nrGFnsvicHBbTUkfBs2-CvV6fqOGv-CpJvnzJMxxg5I,1107
12
+ pagekit_core-0.1.0.dist-info/METADATA,sha256=7IIsZzTpDeOAsjqkUyRp2EotPq1o3aOQzg0QzjQEKWc,2349
13
+ pagekit_core-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
14
+ pagekit_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any