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,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)