wp_python 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.
- src/wordpress_api/__init__.py +31 -0
- src/wordpress_api/auth.py +174 -0
- src/wordpress_api/client.py +293 -0
- src/wordpress_api/endpoints/__init__.py +43 -0
- src/wordpress_api/endpoints/application_passwords.py +235 -0
- src/wordpress_api/endpoints/autosaves.py +106 -0
- src/wordpress_api/endpoints/base.py +117 -0
- src/wordpress_api/endpoints/blocks.py +107 -0
- src/wordpress_api/endpoints/categories.py +91 -0
- src/wordpress_api/endpoints/comments.py +127 -0
- src/wordpress_api/endpoints/media.py +164 -0
- src/wordpress_api/endpoints/menus.py +120 -0
- src/wordpress_api/endpoints/pages.py +109 -0
- src/wordpress_api/endpoints/plugins.py +89 -0
- src/wordpress_api/endpoints/post_types.py +61 -0
- src/wordpress_api/endpoints/posts.py +131 -0
- src/wordpress_api/endpoints/revisions.py +121 -0
- src/wordpress_api/endpoints/search.py +81 -0
- src/wordpress_api/endpoints/settings.py +55 -0
- src/wordpress_api/endpoints/statuses.py +56 -0
- src/wordpress_api/endpoints/tags.py +79 -0
- src/wordpress_api/endpoints/taxonomies.py +41 -0
- src/wordpress_api/endpoints/themes.py +51 -0
- src/wordpress_api/endpoints/users.py +129 -0
- src/wordpress_api/exceptions.py +79 -0
- src/wordpress_api/models/__init__.py +49 -0
- src/wordpress_api/models/base.py +65 -0
- src/wordpress_api/models/category.py +41 -0
- src/wordpress_api/models/comment.py +75 -0
- src/wordpress_api/models/media.py +108 -0
- src/wordpress_api/models/menu.py +83 -0
- src/wordpress_api/models/page.py +80 -0
- src/wordpress_api/models/plugin.py +36 -0
- src/wordpress_api/models/post.py +112 -0
- src/wordpress_api/models/settings.py +32 -0
- src/wordpress_api/models/tag.py +38 -0
- src/wordpress_api/models/taxonomy.py +49 -0
- src/wordpress_api/models/theme.py +50 -0
- src/wordpress_api/models/user.py +82 -0
- wp_python-0.1.0.dist-info/METADATA +12 -0
- wp_python-0.1.0.dist-info/RECORD +42 -0
- wp_python-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Application Passwords endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
|
|
11
|
+
from ..models.base import parse_datetime
|
|
12
|
+
from .base import BaseEndpoint
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApplicationPassword(BaseModel):
|
|
16
|
+
"""WordPress Application Password object."""
|
|
17
|
+
|
|
18
|
+
uuid: str = ""
|
|
19
|
+
app_id: str = ""
|
|
20
|
+
name: str = ""
|
|
21
|
+
created: datetime | None = None
|
|
22
|
+
last_used: datetime | None = None
|
|
23
|
+
last_ip: str | None = None
|
|
24
|
+
|
|
25
|
+
@field_validator("created", "last_used", mode="before")
|
|
26
|
+
@classmethod
|
|
27
|
+
def parse_dates(cls, v: Any) -> datetime | None:
|
|
28
|
+
return parse_datetime(v)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ApplicationPasswordCreated(BaseModel):
|
|
32
|
+
"""Response when creating an Application Password (includes the password)."""
|
|
33
|
+
|
|
34
|
+
uuid: str = ""
|
|
35
|
+
app_id: str = ""
|
|
36
|
+
name: str = ""
|
|
37
|
+
created: datetime | None = None
|
|
38
|
+
password: str = ""
|
|
39
|
+
|
|
40
|
+
@field_validator("created", mode="before")
|
|
41
|
+
@classmethod
|
|
42
|
+
def parse_dates(cls, v: Any) -> datetime | None:
|
|
43
|
+
return parse_datetime(v)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ApplicationPasswordsEndpoint(BaseEndpoint):
|
|
47
|
+
"""Endpoint for managing WordPress Application Passwords.
|
|
48
|
+
|
|
49
|
+
Application Passwords were introduced in WordPress 5.6 and provide
|
|
50
|
+
a secure way to authenticate REST API requests.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def _get_path(self, user_id: int | str) -> str:
|
|
54
|
+
"""Get the endpoint path for a user's application passwords."""
|
|
55
|
+
return f"/wp/v2/users/{user_id}/application-passwords"
|
|
56
|
+
|
|
57
|
+
def list(self, user_id: int | str = "me") -> list[ApplicationPassword]:
|
|
58
|
+
"""List all application passwords for a user.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
user_id: User ID or "me" for current user.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of application passwords (without the actual password values).
|
|
65
|
+
"""
|
|
66
|
+
response = self._get(self._get_path(user_id))
|
|
67
|
+
return [ApplicationPassword.model_validate(item) for item in response]
|
|
68
|
+
|
|
69
|
+
def get(self, uuid: str, user_id: int | str = "me") -> ApplicationPassword:
|
|
70
|
+
"""Get a specific application password by UUID.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
uuid: The UUID of the application password.
|
|
74
|
+
user_id: User ID or "me" for current user.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The application password details.
|
|
78
|
+
"""
|
|
79
|
+
response = self._get(f"{self._get_path(user_id)}/{uuid}")
|
|
80
|
+
return ApplicationPassword.model_validate(response)
|
|
81
|
+
|
|
82
|
+
def create(
|
|
83
|
+
self,
|
|
84
|
+
name: str,
|
|
85
|
+
user_id: int | str = "me",
|
|
86
|
+
app_id: str | None = None,
|
|
87
|
+
) -> ApplicationPasswordCreated:
|
|
88
|
+
"""Create a new application password.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
name: A human-readable name for the application password.
|
|
92
|
+
user_id: User ID or "me" for current user.
|
|
93
|
+
app_id: Optional application identifier (UUID format).
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The created application password including the password value.
|
|
97
|
+
Note: The password is only returned once at creation time!
|
|
98
|
+
"""
|
|
99
|
+
data: dict[str, Any] = {"name": name}
|
|
100
|
+
if app_id:
|
|
101
|
+
data["app_id"] = app_id
|
|
102
|
+
|
|
103
|
+
response = self._post(self._get_path(user_id), data=data)
|
|
104
|
+
return ApplicationPasswordCreated.model_validate(response)
|
|
105
|
+
|
|
106
|
+
def update(
|
|
107
|
+
self,
|
|
108
|
+
uuid: str,
|
|
109
|
+
name: str,
|
|
110
|
+
user_id: int | str = "me",
|
|
111
|
+
) -> ApplicationPassword:
|
|
112
|
+
"""Update an application password's name.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
uuid: The UUID of the application password.
|
|
116
|
+
name: The new name for the application password.
|
|
117
|
+
user_id: User ID or "me" for current user.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The updated application password.
|
|
121
|
+
"""
|
|
122
|
+
response = self._post(
|
|
123
|
+
f"{self._get_path(user_id)}/{uuid}",
|
|
124
|
+
data={"name": name},
|
|
125
|
+
)
|
|
126
|
+
return ApplicationPassword.model_validate(response)
|
|
127
|
+
|
|
128
|
+
def delete(
|
|
129
|
+
self,
|
|
130
|
+
uuid: str,
|
|
131
|
+
user_id: int | str = "me",
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""Delete an application password.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
uuid: The UUID of the application password.
|
|
137
|
+
user_id: User ID or "me" for current user.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Deletion confirmation.
|
|
141
|
+
"""
|
|
142
|
+
return self._delete(f"{self._get_path(user_id)}/{uuid}")
|
|
143
|
+
|
|
144
|
+
def delete_all(self, user_id: int | str = "me") -> dict[str, Any]:
|
|
145
|
+
"""Delete all application passwords for a user.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
user_id: User ID or "me" for current user.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Deletion confirmation.
|
|
152
|
+
"""
|
|
153
|
+
return self._delete(self._get_path(user_id))
|
|
154
|
+
|
|
155
|
+
def get_or_create(
|
|
156
|
+
self,
|
|
157
|
+
name: str,
|
|
158
|
+
user_id: int | str = "me",
|
|
159
|
+
app_id: str | None = None,
|
|
160
|
+
) -> ApplicationPasswordCreated | ApplicationPassword:
|
|
161
|
+
"""Get an existing application password by name, or create one if none exist.
|
|
162
|
+
|
|
163
|
+
This method checks if the user has any application passwords with the
|
|
164
|
+
given name. If found, it returns the existing one. If not found (or if
|
|
165
|
+
no application passwords exist), it creates a new one.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
name: The name to search for or use when creating.
|
|
169
|
+
user_id: User ID or "me" for current user.
|
|
170
|
+
app_id: Optional application identifier for new passwords.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Either an existing ApplicationPassword or a newly created
|
|
174
|
+
ApplicationPasswordCreated (which includes the password value).
|
|
175
|
+
|
|
176
|
+
Note:
|
|
177
|
+
If an existing password is returned, the actual password value
|
|
178
|
+
is NOT included (WordPress doesn't store or return it after creation).
|
|
179
|
+
Only newly created passwords include the password value.
|
|
180
|
+
"""
|
|
181
|
+
existing = self.list(user_id)
|
|
182
|
+
|
|
183
|
+
for app_pass in existing:
|
|
184
|
+
if app_pass.name == name:
|
|
185
|
+
return app_pass
|
|
186
|
+
|
|
187
|
+
return self.create(name=name, user_id=user_id, app_id=app_id)
|
|
188
|
+
|
|
189
|
+
def ensure_exists(
|
|
190
|
+
self,
|
|
191
|
+
name: str,
|
|
192
|
+
user_id: int | str = "me",
|
|
193
|
+
app_id: str | None = None,
|
|
194
|
+
) -> tuple[ApplicationPasswordCreated | ApplicationPassword, bool]:
|
|
195
|
+
"""Ensure an application password exists, creating if necessary.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
name: The name for the application password.
|
|
199
|
+
user_id: User ID or "me" for current user.
|
|
200
|
+
app_id: Optional application identifier for new passwords.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
A tuple of (application_password, was_created).
|
|
204
|
+
If was_created is True, the password value is available.
|
|
205
|
+
"""
|
|
206
|
+
existing = self.list(user_id)
|
|
207
|
+
|
|
208
|
+
for app_pass in existing:
|
|
209
|
+
if app_pass.name == name:
|
|
210
|
+
return app_pass, False
|
|
211
|
+
|
|
212
|
+
new_password = self.create(name=name, user_id=user_id, app_id=app_id)
|
|
213
|
+
return new_password, True
|
|
214
|
+
|
|
215
|
+
def has_any(self, user_id: int | str = "me") -> bool:
|
|
216
|
+
"""Check if the user has any application passwords.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
user_id: User ID or "me" for current user.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
True if the user has at least one application password.
|
|
223
|
+
"""
|
|
224
|
+
return len(self.list(user_id)) > 0
|
|
225
|
+
|
|
226
|
+
def count(self, user_id: int | str = "me") -> int:
|
|
227
|
+
"""Count the number of application passwords for a user.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
user_id: User ID or "me" for current user.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Number of application passwords.
|
|
234
|
+
"""
|
|
235
|
+
return len(self.list(user_id))
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Autosaves endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, field_validator
|
|
9
|
+
|
|
10
|
+
from ..models.base import RenderedContent, parse_datetime
|
|
11
|
+
from .base import BaseEndpoint
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Autosave(BaseModel):
|
|
15
|
+
"""WordPress Autosave object."""
|
|
16
|
+
|
|
17
|
+
id: int
|
|
18
|
+
author: int = 0
|
|
19
|
+
date: datetime | None = None
|
|
20
|
+
date_gmt: datetime | None = None
|
|
21
|
+
modified: datetime | None = None
|
|
22
|
+
modified_gmt: datetime | None = None
|
|
23
|
+
parent: int = 0
|
|
24
|
+
slug: str = ""
|
|
25
|
+
title: RenderedContent | None = None
|
|
26
|
+
content: RenderedContent | None = None
|
|
27
|
+
excerpt: RenderedContent | None = None
|
|
28
|
+
|
|
29
|
+
@field_validator("date", "date_gmt", "modified", "modified_gmt", mode="before")
|
|
30
|
+
@classmethod
|
|
31
|
+
def parse_dates(cls, v: Any) -> datetime | None:
|
|
32
|
+
return parse_datetime(v)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AutosavesEndpoint(BaseEndpoint):
|
|
36
|
+
"""Endpoint for managing post and page autosaves."""
|
|
37
|
+
|
|
38
|
+
def list_post_autosaves(self, post_id: int, **kwargs: Any) -> list[Autosave]:
|
|
39
|
+
"""List autosaves for a post."""
|
|
40
|
+
response = self._get(f"/wp/v2/posts/{post_id}/autosaves", params=kwargs)
|
|
41
|
+
return [Autosave.model_validate(item) for item in response]
|
|
42
|
+
|
|
43
|
+
def get_post_autosave(self, post_id: int, autosave_id: int) -> Autosave:
|
|
44
|
+
"""Get a specific post autosave."""
|
|
45
|
+
response = self._get(f"/wp/v2/posts/{post_id}/autosaves/{autosave_id}")
|
|
46
|
+
return Autosave.model_validate(response)
|
|
47
|
+
|
|
48
|
+
def create_post_autosave(
|
|
49
|
+
self,
|
|
50
|
+
post_id: int,
|
|
51
|
+
title: str | None = None,
|
|
52
|
+
content: str | None = None,
|
|
53
|
+
excerpt: str | None = None,
|
|
54
|
+
**kwargs: Any,
|
|
55
|
+
) -> Autosave:
|
|
56
|
+
"""Create an autosave for a post."""
|
|
57
|
+
data: dict[str, Any] = {}
|
|
58
|
+
if title:
|
|
59
|
+
data["title"] = title
|
|
60
|
+
if content:
|
|
61
|
+
data["content"] = content
|
|
62
|
+
if excerpt:
|
|
63
|
+
data["excerpt"] = excerpt
|
|
64
|
+
data.update(kwargs)
|
|
65
|
+
|
|
66
|
+
response = self._post(f"/wp/v2/posts/{post_id}/autosaves", data=data)
|
|
67
|
+
return Autosave.model_validate(response)
|
|
68
|
+
|
|
69
|
+
def list_page_autosaves(self, page_id: int, **kwargs: Any) -> list[Autosave]:
|
|
70
|
+
"""List autosaves for a page."""
|
|
71
|
+
response = self._get(f"/wp/v2/pages/{page_id}/autosaves", params=kwargs)
|
|
72
|
+
return [Autosave.model_validate(item) for item in response]
|
|
73
|
+
|
|
74
|
+
def get_page_autosave(self, page_id: int, autosave_id: int) -> Autosave:
|
|
75
|
+
"""Get a specific page autosave."""
|
|
76
|
+
response = self._get(f"/wp/v2/pages/{page_id}/autosaves/{autosave_id}")
|
|
77
|
+
return Autosave.model_validate(response)
|
|
78
|
+
|
|
79
|
+
def create_page_autosave(
|
|
80
|
+
self,
|
|
81
|
+
page_id: int,
|
|
82
|
+
title: str | None = None,
|
|
83
|
+
content: str | None = None,
|
|
84
|
+
excerpt: str | None = None,
|
|
85
|
+
**kwargs: Any,
|
|
86
|
+
) -> Autosave:
|
|
87
|
+
"""Create an autosave for a page."""
|
|
88
|
+
data: dict[str, Any] = {}
|
|
89
|
+
if title:
|
|
90
|
+
data["title"] = title
|
|
91
|
+
if content:
|
|
92
|
+
data["content"] = content
|
|
93
|
+
if excerpt:
|
|
94
|
+
data["excerpt"] = excerpt
|
|
95
|
+
data.update(kwargs)
|
|
96
|
+
|
|
97
|
+
response = self._post(f"/wp/v2/pages/{page_id}/autosaves", data=data)
|
|
98
|
+
return Autosave.model_validate(response)
|
|
99
|
+
|
|
100
|
+
def get_latest_autosave(self, post_id: int, post_type: str = "posts") -> Autosave | None:
|
|
101
|
+
"""Get the most recent autosave."""
|
|
102
|
+
if post_type == "pages":
|
|
103
|
+
autosaves = self.list_page_autosaves(post_id)
|
|
104
|
+
else:
|
|
105
|
+
autosaves = self.list_post_autosaves(post_id)
|
|
106
|
+
return autosaves[0] if autosaves else None
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Base endpoint class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TypeVar, Generic, TYPE_CHECKING
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T", bound=BaseModel)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseEndpoint:
|
|
14
|
+
"""Base class for all API endpoints."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, client: Any) -> None:
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
20
|
+
"""Make GET request."""
|
|
21
|
+
return self._client._request("GET", path, params=params)
|
|
22
|
+
|
|
23
|
+
def _post(
|
|
24
|
+
self,
|
|
25
|
+
path: str,
|
|
26
|
+
data: dict[str, Any] | None = None,
|
|
27
|
+
files: dict[str, Any] | None = None,
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""Make POST request."""
|
|
30
|
+
return self._client._request("POST", path, json=data, files=files)
|
|
31
|
+
|
|
32
|
+
def _put(self, path: str, data: dict[str, Any] | None = None) -> Any:
|
|
33
|
+
"""Make PUT request."""
|
|
34
|
+
return self._client._request("PUT", path, json=data)
|
|
35
|
+
|
|
36
|
+
def _patch(self, path: str, data: dict[str, Any] | None = None) -> Any:
|
|
37
|
+
"""Make PATCH request."""
|
|
38
|
+
return self._client._request("PATCH", path, json=data)
|
|
39
|
+
|
|
40
|
+
def _delete(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
41
|
+
"""Make DELETE request."""
|
|
42
|
+
return self._client._request("DELETE", path, params=params)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CRUDEndpoint(BaseEndpoint, Generic[T]):
|
|
46
|
+
"""Base class for CRUD endpoints."""
|
|
47
|
+
|
|
48
|
+
_path: str = ""
|
|
49
|
+
_model_class: type[T]
|
|
50
|
+
|
|
51
|
+
def list(
|
|
52
|
+
self,
|
|
53
|
+
page: int = 1,
|
|
54
|
+
per_page: int = 10,
|
|
55
|
+
search: str | None = None,
|
|
56
|
+
order: str = "desc",
|
|
57
|
+
orderby: str = "date",
|
|
58
|
+
**kwargs: Any,
|
|
59
|
+
) -> list[T]:
|
|
60
|
+
"""List resources with pagination."""
|
|
61
|
+
params: dict[str, Any] = {
|
|
62
|
+
"page": page,
|
|
63
|
+
"per_page": per_page,
|
|
64
|
+
"order": order,
|
|
65
|
+
"orderby": orderby,
|
|
66
|
+
}
|
|
67
|
+
if search:
|
|
68
|
+
params["search"] = search
|
|
69
|
+
params.update(kwargs)
|
|
70
|
+
|
|
71
|
+
response = self._get(self._path, params=params)
|
|
72
|
+
return [self._model_class.model_validate(item) for item in response]
|
|
73
|
+
|
|
74
|
+
def get(self, id: int, context: str = "view", **kwargs: Any) -> T:
|
|
75
|
+
"""Get a single resource by ID."""
|
|
76
|
+
params = {"context": context, **kwargs}
|
|
77
|
+
response = self._get(f"{self._path}/{id}", params=params)
|
|
78
|
+
return self._model_class.model_validate(response)
|
|
79
|
+
|
|
80
|
+
def create(self, data: BaseModel | dict[str, Any]) -> T:
|
|
81
|
+
"""Create a new resource."""
|
|
82
|
+
if isinstance(data, BaseModel):
|
|
83
|
+
payload = data.model_dump(exclude_none=True)
|
|
84
|
+
else:
|
|
85
|
+
payload = data
|
|
86
|
+
response = self._post(self._path, data=payload)
|
|
87
|
+
return self._model_class.model_validate(response)
|
|
88
|
+
|
|
89
|
+
def update(self, id: int, data: BaseModel | dict[str, Any]) -> T:
|
|
90
|
+
"""Update an existing resource."""
|
|
91
|
+
if isinstance(data, BaseModel):
|
|
92
|
+
payload = data.model_dump(exclude_none=True)
|
|
93
|
+
else:
|
|
94
|
+
payload = data
|
|
95
|
+
response = self._post(f"{self._path}/{id}", data=payload)
|
|
96
|
+
return self._model_class.model_validate(response)
|
|
97
|
+
|
|
98
|
+
def delete(self, id: int, force: bool = False, **kwargs: Any) -> dict[str, Any]:
|
|
99
|
+
"""Delete a resource."""
|
|
100
|
+
params = {"force": force, **kwargs}
|
|
101
|
+
return self._delete(f"{self._path}/{id}", params=params)
|
|
102
|
+
|
|
103
|
+
def iterate_all(
|
|
104
|
+
self,
|
|
105
|
+
per_page: int = 100,
|
|
106
|
+
**kwargs: Any,
|
|
107
|
+
) -> Iterator[T]:
|
|
108
|
+
"""Iterate through all resources with automatic pagination."""
|
|
109
|
+
page = 1
|
|
110
|
+
while True:
|
|
111
|
+
items = self.list(page=page, per_page=per_page, **kwargs)
|
|
112
|
+
if not items:
|
|
113
|
+
break
|
|
114
|
+
yield from items
|
|
115
|
+
if len(items) < per_page:
|
|
116
|
+
break
|
|
117
|
+
page += 1
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Block Types and Patterns endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from .base import BaseEndpoint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BlockType(BaseModel):
|
|
13
|
+
"""WordPress Block Type."""
|
|
14
|
+
|
|
15
|
+
name: str = ""
|
|
16
|
+
title: str = ""
|
|
17
|
+
description: str = ""
|
|
18
|
+
icon: str | None = None
|
|
19
|
+
category: str = ""
|
|
20
|
+
keywords: list[str] = Field(default_factory=list)
|
|
21
|
+
parent: list[str] | None = None
|
|
22
|
+
supports: dict[str, Any] = Field(default_factory=dict)
|
|
23
|
+
styles: list[dict[str, Any]] = Field(default_factory=list)
|
|
24
|
+
textdomain: str = ""
|
|
25
|
+
example: dict[str, Any] | None = None
|
|
26
|
+
attributes: dict[str, Any] = Field(default_factory=dict)
|
|
27
|
+
provides_context: dict[str, str] = Field(default_factory=dict)
|
|
28
|
+
uses_context: list[str] = Field(default_factory=list)
|
|
29
|
+
editor_script: str | None = None
|
|
30
|
+
script: str | None = None
|
|
31
|
+
editor_style: str | None = None
|
|
32
|
+
style: str | None = None
|
|
33
|
+
is_dynamic: bool = False
|
|
34
|
+
api_version: int = 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class BlockPattern(BaseModel):
|
|
38
|
+
"""WordPress Block Pattern."""
|
|
39
|
+
|
|
40
|
+
name: str = ""
|
|
41
|
+
title: str = ""
|
|
42
|
+
description: str = ""
|
|
43
|
+
content: str = ""
|
|
44
|
+
categories: list[str] = Field(default_factory=list)
|
|
45
|
+
keywords: list[str] = Field(default_factory=list)
|
|
46
|
+
viewport_width: int | None = None
|
|
47
|
+
block_types: list[str] = Field(default_factory=list)
|
|
48
|
+
inserter: bool = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BlockPatternCategory(BaseModel):
|
|
52
|
+
"""WordPress Block Pattern Category."""
|
|
53
|
+
|
|
54
|
+
name: str = ""
|
|
55
|
+
label: str = ""
|
|
56
|
+
description: str = ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BlocksEndpoint(BaseEndpoint):
|
|
60
|
+
"""Endpoint for WordPress block types and patterns."""
|
|
61
|
+
|
|
62
|
+
_types_path = "/wp/v2/block-types"
|
|
63
|
+
_patterns_path = "/wp/v2/block-patterns/patterns"
|
|
64
|
+
_pattern_categories_path = "/wp/v2/block-patterns/categories"
|
|
65
|
+
|
|
66
|
+
def list_types(self, namespace: str | None = None, **kwargs: Any) -> list[BlockType]:
|
|
67
|
+
"""List registered block types.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
namespace: Filter by block namespace (e.g., 'core').
|
|
71
|
+
"""
|
|
72
|
+
params: dict[str, Any] = {}
|
|
73
|
+
if namespace:
|
|
74
|
+
params["namespace"] = namespace
|
|
75
|
+
params.update(kwargs)
|
|
76
|
+
|
|
77
|
+
response = self._get(self._types_path, params=params)
|
|
78
|
+
return [BlockType.model_validate(item) for item in response]
|
|
79
|
+
|
|
80
|
+
def get_type(self, namespace: str, name: str, **kwargs: Any) -> BlockType:
|
|
81
|
+
"""Get a specific block type.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
namespace: Block namespace (e.g., 'core').
|
|
85
|
+
name: Block name (e.g., 'paragraph').
|
|
86
|
+
"""
|
|
87
|
+
response = self._get(f"{self._types_path}/{namespace}/{name}", params=kwargs)
|
|
88
|
+
return BlockType.model_validate(response)
|
|
89
|
+
|
|
90
|
+
def list_core_blocks(self) -> list[BlockType]:
|
|
91
|
+
"""List all core WordPress blocks."""
|
|
92
|
+
return self.list_types(namespace="core")
|
|
93
|
+
|
|
94
|
+
def list_patterns(self, **kwargs: Any) -> list[BlockPattern]:
|
|
95
|
+
"""List registered block patterns."""
|
|
96
|
+
response = self._get(self._patterns_path, params=kwargs)
|
|
97
|
+
return [BlockPattern.model_validate(item) for item in response]
|
|
98
|
+
|
|
99
|
+
def list_pattern_categories(self, **kwargs: Any) -> list[BlockPatternCategory]:
|
|
100
|
+
"""List block pattern categories."""
|
|
101
|
+
response = self._get(self._pattern_categories_path, params=kwargs)
|
|
102
|
+
return [BlockPatternCategory.model_validate(item) for item in response]
|
|
103
|
+
|
|
104
|
+
def get_patterns_by_category(self, category: str) -> list[BlockPattern]:
|
|
105
|
+
"""Get patterns filtered by category."""
|
|
106
|
+
patterns = self.list_patterns()
|
|
107
|
+
return [p for p in patterns if category in p.categories]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Categories endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..models.category import Category, CategoryCreate, CategoryUpdate
|
|
8
|
+
from .base import CRUDEndpoint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CategoriesEndpoint(CRUDEndpoint[Category]):
|
|
12
|
+
"""Endpoint for managing WordPress categories."""
|
|
13
|
+
|
|
14
|
+
_path = "/wp/v2/categories"
|
|
15
|
+
_model_class = Category
|
|
16
|
+
|
|
17
|
+
def list(
|
|
18
|
+
self,
|
|
19
|
+
page: int = 1,
|
|
20
|
+
per_page: int = 10,
|
|
21
|
+
search: str | None = None,
|
|
22
|
+
order: str = "asc",
|
|
23
|
+
orderby: str = "name",
|
|
24
|
+
hide_empty: bool = False,
|
|
25
|
+
parent: int | None = None,
|
|
26
|
+
post: int | None = None,
|
|
27
|
+
slug: str | list[str] | None = None,
|
|
28
|
+
include: list[int] | None = None,
|
|
29
|
+
exclude: list[int] | None = None,
|
|
30
|
+
**kwargs: Any,
|
|
31
|
+
) -> list[Category]:
|
|
32
|
+
"""List categories with filtering options."""
|
|
33
|
+
params: dict[str, Any] = {
|
|
34
|
+
"page": page,
|
|
35
|
+
"per_page": per_page,
|
|
36
|
+
"order": order,
|
|
37
|
+
"orderby": orderby,
|
|
38
|
+
"hide_empty": hide_empty,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if search:
|
|
42
|
+
params["search"] = search
|
|
43
|
+
if parent is not None:
|
|
44
|
+
params["parent"] = parent
|
|
45
|
+
if post is not None:
|
|
46
|
+
params["post"] = post
|
|
47
|
+
if slug:
|
|
48
|
+
if isinstance(slug, list):
|
|
49
|
+
params["slug"] = ",".join(slug)
|
|
50
|
+
else:
|
|
51
|
+
params["slug"] = slug
|
|
52
|
+
if include:
|
|
53
|
+
params["include"] = ",".join(map(str, include))
|
|
54
|
+
if exclude:
|
|
55
|
+
params["exclude"] = ",".join(map(str, exclude))
|
|
56
|
+
|
|
57
|
+
params.update(kwargs)
|
|
58
|
+
response = self._get(self._path, params=params)
|
|
59
|
+
return [Category.model_validate(item) for item in response]
|
|
60
|
+
|
|
61
|
+
def create(self, data: CategoryCreate | dict[str, Any]) -> Category:
|
|
62
|
+
"""Create a new category."""
|
|
63
|
+
return super().create(data)
|
|
64
|
+
|
|
65
|
+
def update(self, id: int, data: CategoryUpdate | dict[str, Any]) -> Category:
|
|
66
|
+
"""Update an existing category."""
|
|
67
|
+
return super().update(id, data)
|
|
68
|
+
|
|
69
|
+
def get_by_slug(self, slug: str) -> Category | None:
|
|
70
|
+
"""Get a category by its slug."""
|
|
71
|
+
categories = self.list(slug=slug)
|
|
72
|
+
return categories[0] if categories else None
|
|
73
|
+
|
|
74
|
+
def get_children(self, parent_id: int, **kwargs: Any) -> list[Category]:
|
|
75
|
+
"""Get child categories of a parent."""
|
|
76
|
+
return self.list(parent=parent_id, **kwargs)
|
|
77
|
+
|
|
78
|
+
def get_hierarchy(self) -> dict[int, list[Category]]:
|
|
79
|
+
"""Get all categories organized by parent ID."""
|
|
80
|
+
all_categories = list(self.iterate_all())
|
|
81
|
+
hierarchy: dict[int, list[Category]] = {}
|
|
82
|
+
for category in all_categories:
|
|
83
|
+
parent = category.parent
|
|
84
|
+
if parent not in hierarchy:
|
|
85
|
+
hierarchy[parent] = []
|
|
86
|
+
hierarchy[parent].append(category)
|
|
87
|
+
return hierarchy
|
|
88
|
+
|
|
89
|
+
def get_for_post(self, post_id: int) -> list[Category]:
|
|
90
|
+
"""Get categories assigned to a specific post."""
|
|
91
|
+
return self.list(post=post_id)
|