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,131 @@
1
+ """Posts endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..models.post import Post, PostCreate, PostUpdate, PostStatus
8
+ from .base import CRUDEndpoint
9
+
10
+
11
+ class PostsEndpoint(CRUDEndpoint[Post]):
12
+ """Endpoint for managing WordPress posts."""
13
+
14
+ _path = "/wp/v2/posts"
15
+ _model_class = Post
16
+
17
+ def list(
18
+ self,
19
+ page: int = 1,
20
+ per_page: int = 10,
21
+ search: str | None = None,
22
+ order: str = "desc",
23
+ orderby: str = "date",
24
+ status: str | PostStatus | list[str] | None = None,
25
+ categories: list[int] | None = None,
26
+ categories_exclude: list[int] | None = None,
27
+ tags: list[int] | None = None,
28
+ tags_exclude: list[int] | None = None,
29
+ author: int | list[int] | None = None,
30
+ author_exclude: list[int] | None = None,
31
+ before: str | None = None,
32
+ after: str | None = None,
33
+ modified_before: str | None = None,
34
+ modified_after: str | None = None,
35
+ slug: str | list[str] | None = None,
36
+ sticky: bool | None = None,
37
+ **kwargs: Any,
38
+ ) -> list[Post]:
39
+ """List posts with filtering options.
40
+
41
+ Args:
42
+ page: Current page of the collection.
43
+ per_page: Maximum number of items to return.
44
+ search: Limit results to those matching a string.
45
+ order: Order sort attribute ascending or descending.
46
+ orderby: Sort collection by attribute.
47
+ status: Limit result set to posts with specific status(es).
48
+ categories: Limit result set to posts with specific categories.
49
+ categories_exclude: Exclude posts with specific categories.
50
+ tags: Limit result set to posts with specific tags.
51
+ tags_exclude: Exclude posts with specific tags.
52
+ author: Limit result set to posts by specific author(s).
53
+ author_exclude: Exclude posts by specific authors.
54
+ before: Limit to posts published before a given date.
55
+ after: Limit to posts published after a given date.
56
+ modified_before: Limit to posts modified before a given date.
57
+ modified_after: Limit to posts modified after a given date.
58
+ slug: Limit result set to posts with specific slug(s).
59
+ sticky: Limit result set to sticky or non-sticky posts.
60
+ """
61
+ params: dict[str, Any] = {
62
+ "page": page,
63
+ "per_page": per_page,
64
+ "order": order,
65
+ "orderby": orderby,
66
+ }
67
+
68
+ if search:
69
+ params["search"] = search
70
+ if status:
71
+ if isinstance(status, PostStatus):
72
+ params["status"] = status.value
73
+ elif isinstance(status, list):
74
+ params["status"] = ",".join(status)
75
+ else:
76
+ params["status"] = status
77
+ if categories:
78
+ params["categories"] = ",".join(map(str, categories))
79
+ if categories_exclude:
80
+ params["categories_exclude"] = ",".join(map(str, categories_exclude))
81
+ if tags:
82
+ params["tags"] = ",".join(map(str, tags))
83
+ if tags_exclude:
84
+ params["tags_exclude"] = ",".join(map(str, tags_exclude))
85
+ if author is not None:
86
+ if isinstance(author, list):
87
+ params["author"] = ",".join(map(str, author))
88
+ else:
89
+ params["author"] = author
90
+ if author_exclude:
91
+ params["author_exclude"] = ",".join(map(str, author_exclude))
92
+ if before:
93
+ params["before"] = before
94
+ if after:
95
+ params["after"] = after
96
+ if modified_before:
97
+ params["modified_before"] = modified_before
98
+ if modified_after:
99
+ params["modified_after"] = modified_after
100
+ if slug:
101
+ if isinstance(slug, list):
102
+ params["slug"] = ",".join(slug)
103
+ else:
104
+ params["slug"] = slug
105
+ if sticky is not None:
106
+ params["sticky"] = sticky
107
+
108
+ params.update(kwargs)
109
+ response = self._get(self._path, params=params)
110
+ return [Post.model_validate(item) for item in response]
111
+
112
+ def create(self, data: PostCreate | dict[str, Any]) -> Post:
113
+ """Create a new post."""
114
+ return super().create(data)
115
+
116
+ def update(self, id: int, data: PostUpdate | dict[str, Any]) -> Post:
117
+ """Update an existing post."""
118
+ return super().update(id, data)
119
+
120
+ def trash(self, id: int) -> Post:
121
+ """Move a post to trash."""
122
+ return self.update(id, PostUpdate(status=PostStatus.TRASH))
123
+
124
+ def restore(self, id: int) -> Post:
125
+ """Restore a post from trash."""
126
+ return self.update(id, PostUpdate(status=PostStatus.DRAFT))
127
+
128
+ def get_by_slug(self, slug: str) -> Post | None:
129
+ """Get a post by its slug."""
130
+ posts = self.list(slug=slug)
131
+ return posts[0] if posts else None
@@ -0,0 +1,121 @@
1
+ """Revisions 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, field_validator
9
+
10
+ from ..models.base import RenderedContent, parse_datetime
11
+ from .base import BaseEndpoint
12
+
13
+
14
+ class Revision(BaseModel):
15
+ """WordPress Revision object."""
16
+
17
+ id: int
18
+ author: int = 0
19
+ date: datetime | None = None
20
+ date_gmt: datetime | None = None
21
+ guid: RenderedContent | None = None
22
+ modified: datetime | None = None
23
+ modified_gmt: datetime | None = None
24
+ parent: int = 0
25
+ slug: str = ""
26
+ title: RenderedContent | None = None
27
+ content: RenderedContent | None = None
28
+ excerpt: RenderedContent | None = None
29
+
30
+ @field_validator("date", "date_gmt", "modified", "modified_gmt", mode="before")
31
+ @classmethod
32
+ def parse_dates(cls, v: Any) -> datetime | None:
33
+ return parse_datetime(v)
34
+
35
+
36
+ class RevisionsEndpoint(BaseEndpoint):
37
+ """Endpoint for managing post and page revisions."""
38
+
39
+ def list_post_revisions(
40
+ self,
41
+ post_id: int,
42
+ page: int = 1,
43
+ per_page: int = 10,
44
+ **kwargs: Any,
45
+ ) -> list[Revision]:
46
+ """List revisions for a post."""
47
+ params: dict[str, Any] = {
48
+ "page": page,
49
+ "per_page": per_page,
50
+ }
51
+ params.update(kwargs)
52
+
53
+ response = self._get(f"/wp/v2/posts/{post_id}/revisions", params=params)
54
+ return [Revision.model_validate(item) for item in response]
55
+
56
+ def get_post_revision(self, post_id: int, revision_id: int) -> Revision:
57
+ """Get a specific post revision."""
58
+ response = self._get(f"/wp/v2/posts/{post_id}/revisions/{revision_id}")
59
+ return Revision.model_validate(response)
60
+
61
+ def delete_post_revision(
62
+ self,
63
+ post_id: int,
64
+ revision_id: int,
65
+ force: bool = True,
66
+ ) -> dict[str, Any]:
67
+ """Delete a post revision."""
68
+ return self._delete(
69
+ f"/wp/v2/posts/{post_id}/revisions/{revision_id}",
70
+ params={"force": force},
71
+ )
72
+
73
+ def list_page_revisions(
74
+ self,
75
+ page_id: int,
76
+ page: int = 1,
77
+ per_page: int = 10,
78
+ **kwargs: Any,
79
+ ) -> list[Revision]:
80
+ """List revisions for a page."""
81
+ params: dict[str, Any] = {
82
+ "page": page,
83
+ "per_page": per_page,
84
+ }
85
+ params.update(kwargs)
86
+
87
+ response = self._get(f"/wp/v2/pages/{page_id}/revisions", params=params)
88
+ return [Revision.model_validate(item) for item in response]
89
+
90
+ def get_page_revision(self, page_id: int, revision_id: int) -> Revision:
91
+ """Get a specific page revision."""
92
+ response = self._get(f"/wp/v2/pages/{page_id}/revisions/{revision_id}")
93
+ return Revision.model_validate(response)
94
+
95
+ def delete_page_revision(
96
+ self,
97
+ page_id: int,
98
+ revision_id: int,
99
+ force: bool = True,
100
+ ) -> dict[str, Any]:
101
+ """Delete a page revision."""
102
+ return self._delete(
103
+ f"/wp/v2/pages/{page_id}/revisions/{revision_id}",
104
+ params={"force": force},
105
+ )
106
+
107
+ def get_latest_revision(self, post_id: int, post_type: str = "posts") -> Revision | None:
108
+ """Get the most recent revision."""
109
+ if post_type == "pages":
110
+ revisions = self.list_page_revisions(post_id, per_page=1)
111
+ else:
112
+ revisions = self.list_post_revisions(post_id, per_page=1)
113
+ return revisions[0] if revisions else None
114
+
115
+ def count_revisions(self, post_id: int, post_type: str = "posts") -> int:
116
+ """Count the number of revisions for a post or page."""
117
+ if post_type == "pages":
118
+ all_revisions = self.list_page_revisions(post_id, per_page=100)
119
+ else:
120
+ all_revisions = self.list_post_revisions(post_id, per_page=100)
121
+ return len(all_revisions)
@@ -0,0 +1,81 @@
1
+ """Search endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from .base import BaseEndpoint
10
+
11
+
12
+ class SearchResult(BaseModel):
13
+ """WordPress search result."""
14
+
15
+ id: int
16
+ title: str = ""
17
+ url: str = ""
18
+ type: str = ""
19
+ subtype: str = ""
20
+
21
+
22
+ class SearchEndpoint(BaseEndpoint):
23
+ """Endpoint for WordPress site-wide search."""
24
+
25
+ _path = "/wp/v2/search"
26
+
27
+ def search(
28
+ self,
29
+ search: str,
30
+ page: int = 1,
31
+ per_page: int = 10,
32
+ type: str | None = None,
33
+ subtype: str | list[str] | None = None,
34
+ **kwargs: Any,
35
+ ) -> list[SearchResult]:
36
+ """Search the site.
37
+
38
+ Args:
39
+ search: Search query string.
40
+ page: Current page of the collection.
41
+ per_page: Maximum number of items to return.
42
+ type: Limit results to a specific type (post, term, post-format).
43
+ subtype: Limit results to specific subtypes (post, page, category, etc.).
44
+ """
45
+ params: dict[str, Any] = {
46
+ "search": search,
47
+ "page": page,
48
+ "per_page": per_page,
49
+ }
50
+
51
+ if type:
52
+ params["type"] = type
53
+ if subtype:
54
+ if isinstance(subtype, list):
55
+ params["subtype"] = ",".join(subtype)
56
+ else:
57
+ params["subtype"] = subtype
58
+ params.update(kwargs)
59
+
60
+ response = self._get(self._path, params=params)
61
+ return [SearchResult.model_validate(item) for item in response]
62
+
63
+ def search_posts(self, query: str, **kwargs: Any) -> list[SearchResult]:
64
+ """Search only posts."""
65
+ return self.search(query, type="post", subtype="post", **kwargs)
66
+
67
+ def search_pages(self, query: str, **kwargs: Any) -> list[SearchResult]:
68
+ """Search only pages."""
69
+ return self.search(query, type="post", subtype="page", **kwargs)
70
+
71
+ def search_categories(self, query: str, **kwargs: Any) -> list[SearchResult]:
72
+ """Search only categories."""
73
+ return self.search(query, type="term", subtype="category", **kwargs)
74
+
75
+ def search_tags(self, query: str, **kwargs: Any) -> list[SearchResult]:
76
+ """Search only tags."""
77
+ return self.search(query, type="term", subtype="post_tag", **kwargs)
78
+
79
+ def search_all_content(self, query: str, **kwargs: Any) -> list[SearchResult]:
80
+ """Search all post types (posts, pages, custom post types)."""
81
+ return self.search(query, type="post", **kwargs)
@@ -0,0 +1,55 @@
1
+ """Settings endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..models.settings import Settings
8
+ from .base import BaseEndpoint
9
+
10
+
11
+ class SettingsEndpoint(BaseEndpoint):
12
+ """Endpoint for managing WordPress site settings."""
13
+
14
+ _path = "/wp/v2/settings"
15
+
16
+ def get(self) -> Settings:
17
+ """Get all site settings."""
18
+ response = self._get(self._path)
19
+ return Settings.model_validate(response)
20
+
21
+ def update(self, **settings: Any) -> Settings:
22
+ """Update site settings.
23
+
24
+ Args:
25
+ **settings: Key-value pairs of settings to update.
26
+ """
27
+ response = self._post(self._path, data=settings)
28
+ return Settings.model_validate(response)
29
+
30
+ def get_title(self) -> str:
31
+ """Get the site title."""
32
+ settings = self.get()
33
+ return settings.title
34
+
35
+ def set_title(self, title: str) -> Settings:
36
+ """Set the site title."""
37
+ return self.update(title=title)
38
+
39
+ def get_description(self) -> str:
40
+ """Get the site description (tagline)."""
41
+ settings = self.get()
42
+ return settings.description
43
+
44
+ def set_description(self, description: str) -> Settings:
45
+ """Set the site description (tagline)."""
46
+ return self.update(description=description)
47
+
48
+ def get_timezone(self) -> str:
49
+ """Get the site timezone."""
50
+ settings = self.get()
51
+ return settings.timezone
52
+
53
+ def set_timezone(self, timezone: str) -> Settings:
54
+ """Set the site timezone."""
55
+ return self.update(timezone=timezone)
@@ -0,0 +1,56 @@
1
+ """Post Statuses endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from .base import BaseEndpoint
10
+
11
+
12
+ class PostStatusInfo(BaseModel):
13
+ """WordPress Post Status object."""
14
+
15
+ name: str = ""
16
+ slug: str = ""
17
+ public: bool = True
18
+ queryable: bool = True
19
+ date_floating: bool = False
20
+
21
+
22
+ class StatusesEndpoint(BaseEndpoint):
23
+ """Endpoint for viewing WordPress post statuses."""
24
+
25
+ _path = "/wp/v2/statuses"
26
+
27
+ def list(self, **kwargs: Any) -> dict[str, PostStatusInfo]:
28
+ """List all registered post statuses."""
29
+ response = self._get(self._path, params=kwargs)
30
+ return {k: PostStatusInfo.model_validate(v) for k, v in response.items()}
31
+
32
+ def get(self, status: str, **kwargs: Any) -> PostStatusInfo:
33
+ """Get a specific post status by slug."""
34
+ response = self._get(f"{self._path}/{status}", params=kwargs)
35
+ return PostStatusInfo.model_validate(response)
36
+
37
+ def get_publish(self) -> PostStatusInfo:
38
+ """Get the 'publish' status."""
39
+ return self.get("publish")
40
+
41
+ def get_draft(self) -> PostStatusInfo:
42
+ """Get the 'draft' status."""
43
+ return self.get("draft")
44
+
45
+ def get_pending(self) -> PostStatusInfo:
46
+ """Get the 'pending' status."""
47
+ return self.get("pending")
48
+
49
+ def get_private(self) -> PostStatusInfo:
50
+ """Get the 'private' status."""
51
+ return self.get("private")
52
+
53
+ def list_public(self) -> dict[str, PostStatusInfo]:
54
+ """List only public post statuses."""
55
+ all_statuses = self.list()
56
+ return {k: v for k, v in all_statuses.items() if v.public}
@@ -0,0 +1,79 @@
1
+ """Tags endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..models.tag import Tag, TagCreate, TagUpdate
8
+ from .base import CRUDEndpoint
9
+
10
+
11
+ class TagsEndpoint(CRUDEndpoint[Tag]):
12
+ """Endpoint for managing WordPress tags."""
13
+
14
+ _path = "/wp/v2/tags"
15
+ _model_class = Tag
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
+ post: int | None = None,
26
+ slug: str | list[str] | None = None,
27
+ include: list[int] | None = None,
28
+ exclude: list[int] | None = None,
29
+ **kwargs: Any,
30
+ ) -> list[Tag]:
31
+ """List tags with filtering options."""
32
+ params: dict[str, Any] = {
33
+ "page": page,
34
+ "per_page": per_page,
35
+ "order": order,
36
+ "orderby": orderby,
37
+ "hide_empty": hide_empty,
38
+ }
39
+
40
+ if search:
41
+ params["search"] = search
42
+ if post is not None:
43
+ params["post"] = post
44
+ if slug:
45
+ if isinstance(slug, list):
46
+ params["slug"] = ",".join(slug)
47
+ else:
48
+ params["slug"] = slug
49
+ if include:
50
+ params["include"] = ",".join(map(str, include))
51
+ if exclude:
52
+ params["exclude"] = ",".join(map(str, exclude))
53
+
54
+ params.update(kwargs)
55
+ response = self._get(self._path, params=params)
56
+ return [Tag.model_validate(item) for item in response]
57
+
58
+ def create(self, data: TagCreate | dict[str, Any]) -> Tag:
59
+ """Create a new tag."""
60
+ return super().create(data)
61
+
62
+ def update(self, id: int, data: TagUpdate | dict[str, Any]) -> Tag:
63
+ """Update an existing tag."""
64
+ return super().update(id, data)
65
+
66
+ def get_by_slug(self, slug: str) -> Tag | None:
67
+ """Get a tag by its slug."""
68
+ tags = self.list(slug=slug)
69
+ return tags[0] if tags else None
70
+
71
+ def get_for_post(self, post_id: int) -> list[Tag]:
72
+ """Get tags assigned to a specific post."""
73
+ return self.list(post=post_id)
74
+
75
+ def get_popular(self, limit: int = 10) -> list[Tag]:
76
+ """Get the most popular tags by post count."""
77
+ all_tags = list(self.iterate_all(hide_empty=True))
78
+ sorted_tags = sorted(all_tags, key=lambda t: t.count, reverse=True)
79
+ return sorted_tags[:limit]
@@ -0,0 +1,41 @@
1
+ """Taxonomies endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..models.taxonomy import Taxonomy
8
+ from .base import BaseEndpoint
9
+
10
+
11
+ class TaxonomiesEndpoint(BaseEndpoint):
12
+ """Endpoint for viewing WordPress taxonomies."""
13
+
14
+ _path = "/wp/v2/taxonomies"
15
+
16
+ def list(self, type: str | None = None, **kwargs: Any) -> dict[str, Taxonomy]:
17
+ """List all taxonomies.
18
+
19
+ Args:
20
+ type: Limit results to taxonomies associated with a post type.
21
+ """
22
+ params: dict[str, Any] = {}
23
+ if type:
24
+ params["type"] = type
25
+ params.update(kwargs)
26
+
27
+ response = self._get(self._path, params=params)
28
+ return {k: Taxonomy.model_validate(v) for k, v in response.items()}
29
+
30
+ def get(self, taxonomy: str, **kwargs: Any) -> Taxonomy:
31
+ """Get a specific taxonomy by slug."""
32
+ response = self._get(f"{self._path}/{taxonomy}", params=kwargs)
33
+ return Taxonomy.model_validate(response)
34
+
35
+ def get_category(self) -> Taxonomy:
36
+ """Get the category taxonomy."""
37
+ return self.get("category")
38
+
39
+ def get_post_tag(self) -> Taxonomy:
40
+ """Get the post_tag taxonomy."""
41
+ return self.get("post_tag")
@@ -0,0 +1,51 @@
1
+ """Themes endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..models.theme import Theme
8
+ from .base import BaseEndpoint
9
+
10
+
11
+ class ThemesEndpoint(BaseEndpoint):
12
+ """Endpoint for managing WordPress themes."""
13
+
14
+ _path = "/wp/v2/themes"
15
+
16
+ def list(self, status: str | list[str] | None = None, **kwargs: Any) -> list[Theme]:
17
+ """List installed themes.
18
+
19
+ Args:
20
+ status: Filter by theme status (active, inactive).
21
+ """
22
+ params: dict[str, Any] = {}
23
+ if status:
24
+ if isinstance(status, list):
25
+ params["status"] = ",".join(status)
26
+ else:
27
+ params["status"] = status
28
+ params.update(kwargs)
29
+
30
+ response = self._get(self._path, params=params)
31
+ return [Theme.model_validate(item) for item in response]
32
+
33
+ def get(self, stylesheet: str, **kwargs: Any) -> Theme:
34
+ """Get a specific theme.
35
+
36
+ Args:
37
+ stylesheet: Theme stylesheet name.
38
+ """
39
+ response = self._get(f"{self._path}/{stylesheet}", params=kwargs)
40
+ return Theme.model_validate(response)
41
+
42
+ def get_active(self) -> Theme:
43
+ """Get the currently active theme."""
44
+ themes = self.list(status="active")
45
+ if themes:
46
+ return themes[0]
47
+ raise ValueError("No active theme found")
48
+
49
+ def list_inactive(self) -> list[Theme]:
50
+ """List all inactive themes."""
51
+ return self.list(status="inactive")