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.
Files changed (42) hide show
  1. src/wordpress_api/__init__.py +31 -0
  2. src/wordpress_api/auth.py +174 -0
  3. src/wordpress_api/client.py +293 -0
  4. src/wordpress_api/endpoints/__init__.py +43 -0
  5. src/wordpress_api/endpoints/application_passwords.py +235 -0
  6. src/wordpress_api/endpoints/autosaves.py +106 -0
  7. src/wordpress_api/endpoints/base.py +117 -0
  8. src/wordpress_api/endpoints/blocks.py +107 -0
  9. src/wordpress_api/endpoints/categories.py +91 -0
  10. src/wordpress_api/endpoints/comments.py +127 -0
  11. src/wordpress_api/endpoints/media.py +164 -0
  12. src/wordpress_api/endpoints/menus.py +120 -0
  13. src/wordpress_api/endpoints/pages.py +109 -0
  14. src/wordpress_api/endpoints/plugins.py +89 -0
  15. src/wordpress_api/endpoints/post_types.py +61 -0
  16. src/wordpress_api/endpoints/posts.py +131 -0
  17. src/wordpress_api/endpoints/revisions.py +121 -0
  18. src/wordpress_api/endpoints/search.py +81 -0
  19. src/wordpress_api/endpoints/settings.py +55 -0
  20. src/wordpress_api/endpoints/statuses.py +56 -0
  21. src/wordpress_api/endpoints/tags.py +79 -0
  22. src/wordpress_api/endpoints/taxonomies.py +41 -0
  23. src/wordpress_api/endpoints/themes.py +51 -0
  24. src/wordpress_api/endpoints/users.py +129 -0
  25. src/wordpress_api/exceptions.py +79 -0
  26. src/wordpress_api/models/__init__.py +49 -0
  27. src/wordpress_api/models/base.py +65 -0
  28. src/wordpress_api/models/category.py +41 -0
  29. src/wordpress_api/models/comment.py +75 -0
  30. src/wordpress_api/models/media.py +108 -0
  31. src/wordpress_api/models/menu.py +83 -0
  32. src/wordpress_api/models/page.py +80 -0
  33. src/wordpress_api/models/plugin.py +36 -0
  34. src/wordpress_api/models/post.py +112 -0
  35. src/wordpress_api/models/settings.py +32 -0
  36. src/wordpress_api/models/tag.py +38 -0
  37. src/wordpress_api/models/taxonomy.py +49 -0
  38. src/wordpress_api/models/theme.py +50 -0
  39. src/wordpress_api/models/user.py +82 -0
  40. wp_python-0.1.0.dist-info/METADATA +12 -0
  41. wp_python-0.1.0.dist-info/RECORD +42 -0
  42. wp_python-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,31 @@
1
+ """
2
+ WordPress REST API Python Client
3
+
4
+ A full-featured Python 3.12 interface to the WordPress REST API.
5
+ """
6
+
7
+ from .client import WordPressClient
8
+ from .auth import ApplicationPasswordAuth, BasicAuth, JWTAuth, OAuth2Auth
9
+ from .exceptions import (
10
+ WordPressError,
11
+ AuthenticationError,
12
+ NotFoundError,
13
+ ValidationError,
14
+ RateLimitError,
15
+ ServerError,
16
+ )
17
+
18
+ __version__ = "1.0.0"
19
+ __all__ = [
20
+ "WordPressClient",
21
+ "ApplicationPasswordAuth",
22
+ "BasicAuth",
23
+ "JWTAuth",
24
+ "OAuth2Auth",
25
+ "WordPressError",
26
+ "AuthenticationError",
27
+ "NotFoundError",
28
+ "ValidationError",
29
+ "RateLimitError",
30
+ "ServerError",
31
+ ]
@@ -0,0 +1,174 @@
1
+ """Authentication handlers for WordPress REST API."""
2
+
3
+ import base64
4
+ from abc import ABC, abstractmethod
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+
10
+ class AuthHandler(ABC):
11
+ """Abstract base class for authentication handlers."""
12
+
13
+ @abstractmethod
14
+ def get_headers(self) -> dict[str, str]:
15
+ """Return headers required for authentication."""
16
+ pass
17
+
18
+ @abstractmethod
19
+ def authenticate(self, request: httpx.Request) -> httpx.Request:
20
+ """Authenticate the request."""
21
+ pass
22
+
23
+
24
+ class BasicAuth(AuthHandler):
25
+ """HTTP Basic Authentication.
26
+
27
+ Note: Basic auth should only be used over HTTPS.
28
+ """
29
+
30
+ def __init__(self, username: str, password: str) -> None:
31
+ self.username = username
32
+ self.password = password
33
+ self._credentials = base64.b64encode(
34
+ f"{username}:{password}".encode()
35
+ ).decode()
36
+
37
+ def get_headers(self) -> dict[str, str]:
38
+ return {"Authorization": f"Basic {self._credentials}"}
39
+
40
+ def authenticate(self, request: httpx.Request) -> httpx.Request:
41
+ request.headers.update(self.get_headers())
42
+ return request
43
+
44
+
45
+ class ApplicationPasswordAuth(AuthHandler):
46
+ """WordPress Application Passwords authentication.
47
+
48
+ Application Passwords were introduced in WordPress 5.6 and provide
49
+ a secure way to authenticate REST API requests without exposing
50
+ the user's main password.
51
+ """
52
+
53
+ def __init__(self, username: str, application_password: str) -> None:
54
+ self.username = username
55
+ self.application_password = application_password.replace(" ", "")
56
+ self._credentials = base64.b64encode(
57
+ f"{username}:{self.application_password}".encode()
58
+ ).decode()
59
+
60
+ def get_headers(self) -> dict[str, str]:
61
+ return {"Authorization": f"Basic {self._credentials}"}
62
+
63
+ def authenticate(self, request: httpx.Request) -> httpx.Request:
64
+ request.headers.update(self.get_headers())
65
+ return request
66
+
67
+
68
+ class JWTAuth(AuthHandler):
69
+ """JWT (JSON Web Token) Authentication.
70
+
71
+ Requires a JWT authentication plugin on the WordPress site.
72
+ """
73
+
74
+ def __init__(self, token: str | None = None) -> None:
75
+ self.token = token
76
+ self._base_url: str | None = None
77
+
78
+ def set_base_url(self, url: str) -> None:
79
+ """Set the base URL for token requests."""
80
+ self._base_url = url
81
+
82
+ def get_headers(self) -> dict[str, str]:
83
+ if not self.token:
84
+ return {}
85
+ return {"Authorization": f"Bearer {self.token}"}
86
+
87
+ def authenticate(self, request: httpx.Request) -> httpx.Request:
88
+ if self.token:
89
+ request.headers.update(self.get_headers())
90
+ return request
91
+
92
+ def obtain_token(
93
+ self,
94
+ client: httpx.Client,
95
+ username: str,
96
+ password: str,
97
+ endpoint: str = "/wp-json/jwt-auth/v1/token",
98
+ ) -> str:
99
+ """Obtain a JWT token from the WordPress site."""
100
+ response = client.post(
101
+ endpoint,
102
+ json={"username": username, "password": password},
103
+ )
104
+ response.raise_for_status()
105
+ data = response.json()
106
+ self.token = data.get("token", data.get("data", {}).get("token"))
107
+ return self.token
108
+
109
+ def validate_token(
110
+ self,
111
+ client: httpx.Client,
112
+ endpoint: str = "/wp-json/jwt-auth/v1/token/validate",
113
+ ) -> bool:
114
+ """Validate the current JWT token."""
115
+ if not self.token:
116
+ return False
117
+ response = client.post(endpoint, headers=self.get_headers())
118
+ return response.status_code == 200
119
+
120
+
121
+ class OAuth2Auth(AuthHandler):
122
+ """OAuth 2.0 Authentication.
123
+
124
+ Requires OAuth 2.0 plugin on the WordPress site.
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ access_token: str | None = None,
130
+ refresh_token: str | None = None,
131
+ client_id: str | None = None,
132
+ client_secret: str | None = None,
133
+ ) -> None:
134
+ self.access_token = access_token
135
+ self.refresh_token = refresh_token
136
+ self.client_id = client_id
137
+ self.client_secret = client_secret
138
+
139
+ def get_headers(self) -> dict[str, str]:
140
+ if not self.access_token:
141
+ return {}
142
+ return {"Authorization": f"Bearer {self.access_token}"}
143
+
144
+ def authenticate(self, request: httpx.Request) -> httpx.Request:
145
+ if self.access_token:
146
+ request.headers.update(self.get_headers())
147
+ return request
148
+
149
+ def refresh_access_token(
150
+ self,
151
+ client: httpx.Client,
152
+ token_endpoint: str,
153
+ ) -> str:
154
+ """Refresh the access token using the refresh token."""
155
+ if not self.refresh_token or not self.client_id:
156
+ raise ValueError("Refresh token and client ID required")
157
+
158
+ data: dict[str, Any] = {
159
+ "grant_type": "refresh_token",
160
+ "refresh_token": self.refresh_token,
161
+ "client_id": self.client_id,
162
+ }
163
+ if self.client_secret:
164
+ data["client_secret"] = self.client_secret
165
+
166
+ response = client.post(token_endpoint, data=data)
167
+ response.raise_for_status()
168
+ token_data = response.json()
169
+
170
+ self.access_token = token_data["access_token"]
171
+ if "refresh_token" in token_data:
172
+ self.refresh_token = token_data["refresh_token"]
173
+
174
+ return self.access_token
@@ -0,0 +1,293 @@
1
+ """Main WordPress REST API Client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from urllib.parse import urljoin # noqa: F401
7
+
8
+ import httpx
9
+
10
+ from .auth import ApplicationPasswordAuth, AuthHandler, BasicAuth, JWTAuth # noqa: F401
11
+ from .endpoints.application_passwords import ApplicationPasswordsEndpoint
12
+ from .endpoints.autosaves import AutosavesEndpoint
13
+ from .endpoints.blocks import BlocksEndpoint
14
+ from .endpoints.categories import CategoriesEndpoint
15
+ from .endpoints.comments import CommentsEndpoint
16
+ from .endpoints.media import MediaEndpoint
17
+ from .endpoints.menus import MenusEndpoint
18
+ from .endpoints.pages import PagesEndpoint
19
+ from .endpoints.plugins import PluginsEndpoint
20
+ from .endpoints.post_types import PostTypesEndpoint
21
+ from .endpoints.posts import PostsEndpoint
22
+ from .endpoints.revisions import RevisionsEndpoint
23
+ from .endpoints.search import SearchEndpoint
24
+ from .endpoints.settings import SettingsEndpoint
25
+ from .endpoints.statuses import StatusesEndpoint
26
+ from .endpoints.tags import TagsEndpoint
27
+ from .endpoints.taxonomies import TaxonomiesEndpoint
28
+ from .endpoints.themes import ThemesEndpoint
29
+ from .endpoints.users import UsersEndpoint
30
+ from .exceptions import WordPressError, raise_for_status # noqa: F401
31
+
32
+
33
+ class WordPressClient:
34
+ """WordPress REST API Client.
35
+
36
+ A full-featured Python interface to the WordPress REST API.
37
+
38
+ Example:
39
+ ```python
40
+ from wordpress_api import WordPressClient, ApplicationPasswordAuth
41
+
42
+ auth = ApplicationPasswordAuth("username", "xxxx xxxx xxxx xxxx xxxx xxxx")
43
+ client = WordPressClient("https://example.com", auth=auth)
44
+
45
+ # List posts
46
+ posts = client.posts.list()
47
+
48
+ # Create a new post
49
+ new_post = client.posts.create({
50
+ "title": "Hello World",
51
+ "content": "This is my first post!",
52
+ "status": "publish"
53
+ })
54
+
55
+ # Get current user
56
+ me = client.users.me()
57
+ ```
58
+
59
+ Args:
60
+ base_url: The WordPress site URL (e.g., "https://example.com").
61
+ auth: Authentication handler (ApplicationPasswordAuth, BasicAuth, or JWTAuth).
62
+ timeout: Request timeout in seconds (default 30).
63
+ verify_ssl: Whether to verify SSL certificates (default True).
64
+ user_agent: Custom User-Agent string.
65
+ api_prefix: REST API prefix (default "/wp-json").
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ base_url: str,
71
+ auth: AuthHandler | None = None,
72
+ timeout: float = 30.0,
73
+ verify_ssl: bool = True,
74
+ user_agent: str | None = None,
75
+ api_prefix: str = "/wp-json",
76
+ ) -> None:
77
+ self.base_url = base_url.rstrip("/")
78
+ self.api_prefix = api_prefix
79
+ self.auth = auth
80
+ self.timeout = timeout
81
+ self.verify_ssl = verify_ssl
82
+
83
+ headers = {
84
+ "Accept": "application/json",
85
+ "Content-Type": "application/json",
86
+ }
87
+ if user_agent:
88
+ headers["User-Agent"] = user_agent
89
+ else:
90
+ headers["User-Agent"] = "WordPress-Python-Client/1.0"
91
+
92
+ if auth:
93
+ headers.update(auth.get_headers())
94
+
95
+ self._client = httpx.Client(
96
+ base_url=f"{self.base_url}{self.api_prefix}",
97
+ headers=headers,
98
+ timeout=timeout,
99
+ verify=verify_ssl,
100
+ )
101
+
102
+ self._last_response: httpx.Response | None = None
103
+ self._total_pages: int | None = None
104
+ self._total_items: int | None = None
105
+
106
+ self._init_endpoints()
107
+
108
+ def _init_endpoints(self) -> None:
109
+ """Initialize all API endpoints."""
110
+ self.posts = PostsEndpoint(self)
111
+ self.pages = PagesEndpoint(self)
112
+ self.media = MediaEndpoint(self)
113
+ self.users = UsersEndpoint(self)
114
+ self.comments = CommentsEndpoint(self)
115
+ self.categories = CategoriesEndpoint(self)
116
+ self.tags = TagsEndpoint(self)
117
+ self.taxonomies = TaxonomiesEndpoint(self)
118
+ self.settings = SettingsEndpoint(self)
119
+ self.plugins = PluginsEndpoint(self)
120
+ self.themes = ThemesEndpoint(self)
121
+ self.menus = MenusEndpoint(self)
122
+ self.search = SearchEndpoint(self)
123
+ self.blocks = BlocksEndpoint(self)
124
+ self.revisions = RevisionsEndpoint(self)
125
+ self.autosaves = AutosavesEndpoint(self)
126
+ self.post_types = PostTypesEndpoint(self)
127
+ self.statuses = StatusesEndpoint(self)
128
+ self.application_passwords = ApplicationPasswordsEndpoint(self)
129
+
130
+ def _request(
131
+ self,
132
+ method: str,
133
+ path: str,
134
+ params: dict[str, Any] | None = None,
135
+ json: dict[str, Any] | None = None,
136
+ files: dict[str, Any] | None = None,
137
+ content: bytes | None = None,
138
+ headers: dict[str, str] | None = None,
139
+ ) -> Any:
140
+ """Make an HTTP request to the WordPress REST API."""
141
+ if params:
142
+ params = {k: v for k, v in params.items() if v is not None}
143
+
144
+ request_headers = {}
145
+ if headers:
146
+ request_headers.update(headers)
147
+
148
+ if content is not None:
149
+ response = self._client.request(
150
+ method,
151
+ path,
152
+ params=params,
153
+ content=content,
154
+ headers=request_headers,
155
+ )
156
+ elif files:
157
+ response = self._client.request(
158
+ method,
159
+ path,
160
+ params=params,
161
+ files=files,
162
+ headers=request_headers,
163
+ )
164
+ else:
165
+ response = self._client.request(
166
+ method,
167
+ path,
168
+ params=params,
169
+ json=json,
170
+ headers=request_headers,
171
+ )
172
+
173
+ self._last_response = response
174
+ self._total_pages = int(response.headers.get("X-WP-TotalPages", 0))
175
+ self._total_items = int(response.headers.get("X-WP-Total", 0))
176
+
177
+ if response.status_code >= 400:
178
+ try:
179
+ error_data = response.json()
180
+ except Exception:
181
+ error_data = {"message": response.text}
182
+ raise_for_status(response.status_code, error_data)
183
+
184
+ if response.status_code == 204:
185
+ return {}
186
+
187
+ try:
188
+ return response.json()
189
+ except Exception:
190
+ return {"raw": response.text}
191
+
192
+ def discover(self) -> dict[str, Any]:
193
+ """Discover the WordPress REST API.
194
+
195
+ Returns information about available routes and authentication.
196
+ """
197
+ return self._request("GET", "/")
198
+
199
+ def get_namespaces(self) -> list[str]:
200
+ """Get available API namespaces."""
201
+ discovery = self.discover()
202
+ return discovery.get("namespaces", [])
203
+
204
+ def get_routes(self) -> dict[str, Any]:
205
+ """Get all available API routes."""
206
+ discovery = self.discover()
207
+ return discovery.get("routes", {})
208
+
209
+ def get_authentication_status(self) -> dict[str, Any]:
210
+ """Check if the current authentication is valid."""
211
+ discovery = self.discover()
212
+ return discovery.get("authentication", {})
213
+
214
+ @property
215
+ def total_pages(self) -> int | None:
216
+ """Total pages from the last list request."""
217
+ return self._total_pages
218
+
219
+ @property
220
+ def total_items(self) -> int | None:
221
+ """Total items from the last list request."""
222
+ return self._total_items
223
+
224
+ @property
225
+ def last_response(self) -> httpx.Response | None:
226
+ """The last HTTP response received."""
227
+ return self._last_response
228
+
229
+ def close(self) -> None:
230
+ """Close the HTTP client."""
231
+ self._client.close()
232
+
233
+ def __enter__(self) -> "WordPressClient":
234
+ return self
235
+
236
+ def __exit__(self, *args: Any) -> None:
237
+ self.close()
238
+
239
+ def custom_post_type(self, post_type: str) -> "CustomPostTypeEndpoint":
240
+ """Access a custom post type endpoint.
241
+
242
+ Args:
243
+ post_type: The custom post type slug (e.g., "products", "portfolio").
244
+
245
+ Returns:
246
+ A CustomPostTypeEndpoint instance for the specified post type.
247
+ """
248
+ return CustomPostTypeEndpoint(self, post_type)
249
+
250
+
251
+ class CustomPostTypeEndpoint:
252
+ """Endpoint for custom post types."""
253
+
254
+ def __init__(self, client: WordPressClient, post_type: str) -> None:
255
+ self._client = client
256
+ self.post_type = post_type
257
+ self._path = f"/wp/v2/{post_type}"
258
+
259
+ def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
260
+ return self._client._request("GET", path, params=params)
261
+
262
+ def _post(self, path: str, data: dict[str, Any] | None = None) -> Any:
263
+ return self._client._request("POST", path, json=data)
264
+
265
+ def _delete(self, path: str, params: dict[str, Any] | None = None) -> Any:
266
+ return self._client._request("DELETE", path, params=params)
267
+
268
+ def list(
269
+ self,
270
+ page: int = 1,
271
+ per_page: int = 10,
272
+ **kwargs: Any,
273
+ ) -> list[dict[str, Any]]:
274
+ """List items of this custom post type."""
275
+ params = {"page": page, "per_page": per_page, **kwargs}
276
+ return self._get(self._path, params=params)
277
+
278
+ def get(self, id: int, **kwargs: Any) -> dict[str, Any]:
279
+ """Get a single item by ID."""
280
+ return self._get(f"{self._path}/{id}", params=kwargs)
281
+
282
+ def create(self, data: dict[str, Any]) -> dict[str, Any]:
283
+ """Create a new item."""
284
+ return self._post(self._path, data=data)
285
+
286
+ def update(self, id: int, data: dict[str, Any]) -> dict[str, Any]:
287
+ """Update an existing item."""
288
+ return self._post(f"{self._path}/{id}", data=data)
289
+
290
+ def delete(self, id: int, force: bool = False, **kwargs: Any) -> dict[str, Any]:
291
+ """Delete an item."""
292
+ params = {"force": force, **kwargs}
293
+ return self._delete(f"{self._path}/{id}", params=params)
@@ -0,0 +1,43 @@
1
+ """WordPress REST API Endpoints."""
2
+
3
+ from .posts import PostsEndpoint
4
+ from .pages import PagesEndpoint
5
+ from .media import MediaEndpoint
6
+ from .users import UsersEndpoint
7
+ from .comments import CommentsEndpoint
8
+ from .categories import CategoriesEndpoint
9
+ from .tags import TagsEndpoint
10
+ from .taxonomies import TaxonomiesEndpoint
11
+ from .settings import SettingsEndpoint
12
+ from .plugins import PluginsEndpoint
13
+ from .themes import ThemesEndpoint
14
+ from .menus import MenusEndpoint
15
+ from .search import SearchEndpoint
16
+ from .blocks import BlocksEndpoint
17
+ from .revisions import RevisionsEndpoint
18
+ from .autosaves import AutosavesEndpoint
19
+ from .post_types import PostTypesEndpoint
20
+ from .statuses import StatusesEndpoint
21
+ from .application_passwords import ApplicationPasswordsEndpoint
22
+
23
+ __all__ = [
24
+ "PostsEndpoint",
25
+ "PagesEndpoint",
26
+ "MediaEndpoint",
27
+ "UsersEndpoint",
28
+ "CommentsEndpoint",
29
+ "CategoriesEndpoint",
30
+ "TagsEndpoint",
31
+ "TaxonomiesEndpoint",
32
+ "SettingsEndpoint",
33
+ "PluginsEndpoint",
34
+ "ThemesEndpoint",
35
+ "MenusEndpoint",
36
+ "SearchEndpoint",
37
+ "BlocksEndpoint",
38
+ "RevisionsEndpoint",
39
+ "AutosavesEndpoint",
40
+ "PostTypesEndpoint",
41
+ "StatusesEndpoint",
42
+ "ApplicationPasswordsEndpoint",
43
+ ]