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 +46 -0
- pagekit/client.py +81 -0
- pagekit/errors.py +32 -0
- pagekit/http.py +241 -0
- pagekit/resources/__init__.py +20 -0
- pagekit/resources/authors.py +40 -0
- pagekit/resources/categories.py +40 -0
- pagekit/resources/media.py +46 -0
- pagekit/resources/posts.py +79 -0
- pagekit/resources/tags.py +30 -0
- pagekit/types.py +198 -0
- pagekit_core-0.1.0.dist-info/METADATA +97 -0
- pagekit_core-0.1.0.dist-info/RECORD +14 -0
- pagekit_core-0.1.0.dist-info/WHEEL +4 -0
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,,
|