Coverage for ghost/resources.py: 76%
62 statements
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-17 17:19 +0200
« prev ^ index » next coverage.py v7.5.3, created at 2024-06-17 17:19 +0200
1import os.path
2import shutil
3from io import BytesIO
4from pathlib import Path
6from .abs_resources import GhostAdminResource, GhostContentResource, GhostResource
8__all__ = [
9 "GhostResource",
10 "GhostAdminResource",
11 "GhostContentResource",
12 "PostResource",
13 "PageResource",
14 "TagResource",
15 "MemberResource",
16 "UserResource",
17 "AuthorResource",
18 "SettingsResource",
19 "SiteResource",
20 "ImageResource",
21 "ThemeResource",
22]
25# Admin
27# todo: inherit create/update/get for some Resources with all values allowed for that specific resource
28# -> useful as documentation instead of going to the (sometimes) confusing Ghost Docs.
31class PostResource(GhostAdminResource):
32 # See: https://ghost.org/docs/admin-api/#the-post-object
33 resource = "posts"
35 # slug,
36 # id,
37 # uuid,
38 # title,
39 # mobiledoc,
40 # html,
41 # comment_id,
42 # feature_image,
43 # feature_image_alt,
44 # feature_image_caption,
45 # featured,
46 # status = "dr,
47 # visibility = "pub,
48 # created_at,
49 # updated_at,
50 # published_at,
51 # custom_excerpt,
52 # codeinjection_head,
53 # codeinjection_foot,
54 # custom_template,
55 # canonical_url,
56 # tags,
57 # authors,
58 # primary_author,
59 # primary_tag,
60 # url,
61 # excerpt,
62 # og_image,
63 # og_title,
64 # og_description,
65 # twitter_image,
66 # twitter_title,
67 # twitter_description,
68 # meta_title,
69 # meta_description,
70 # email_only,
73class PageResource(GhostAdminResource):
74 # See: https://ghost.org/docs/admin-api/#the-post-object
75 resource = "pages"
77 # slug,
78 # id,
79 # uuid,
80 # title,
81 # mobiledoc,
82 # html,
83 # comment_id,
84 # feature_image,
85 # feature_image_alt,
86 # feature_image_caption,
87 # featured,
88 # status = "dr,
89 # visibility = "pub,
90 # created_at,
91 # updated_at,
92 # published_at,
93 # custom_excerpt,
94 # codeinjection_head,
95 # codeinjection_foot,
96 # custom_template,
97 # canonical_url,
98 # tags,
99 # authors,
100 # primary_author,
101 # primary_tag,
102 # url,
103 # excerpt,
104 # og_image,
105 # og_title,
106 # og_description,
107 # twitter_image,
108 # twitter_title,
109 # twitter_description,
110 # meta_title,
111 # meta_description,
112 # email_only,
115class TagResource(GhostAdminResource):
116 # experimental
117 resource = "tags"
120class MemberResource(GhostAdminResource):
121 resource = "members"
124class UserResource(GhostAdminResource):
125 resource = "users"
128# Content
131class AuthorResource(GhostContentResource):
132 resource = "authors"
135class SettingsResource(GhostContentResource):
136 resource = "settings"
139# Custom
142class SiteResource(GhostResource):
143 resource = "site"
144 api = "admin"
146 def get(self, _=None, **__):
147 """
148 The site resource simple returns info about the site
149 """
150 return self.GET()["site"]
153class ImageResource(GhostResource):
154 resource = "images"
155 api = "admin"
157 def _load(self, path: str):
158 """
159 Load an image by path
161 Args:
162 path (str): to the image
164 Returns:
165 BytesIO: image bytes
166 """
167 with open(path, "rb") as image:
168 return BytesIO(image.read())
170 def upload(self, image_obj_or_path, image_name: str = None):
171 """
172 Args:
173 image_obj_or_path (BytesIO | str): either a path to the image or its bytes
174 image_name (str): optional image title (can also be used from path)
176 Returns:
177 str: uploaded image URL
178 """
179 # todo: support other filetypes than image/jpeg
181 if isinstance(image_obj_or_path, str):
182 if not image_name:
183 image_name = os.path.basename(image_obj_or_path)
185 image_obj_or_path = self._load(image_obj_or_path)
187 files = {"file": (image_name, image_obj_or_path, "image/jpeg")}
188 params = {
189 "purpose": "image",
190 "ref": image_name,
191 } # 'image', 'profile_image', 'icon'
192 result = self.POST("upload", files=files, params=params)
194 return result["images"][0]["url"]
197class ThemeResource(GhostResource):
198 resource = "themes"
199 api = "admin"
201 def _create_zip(self, folder: str):
202 """
203 Create a zip of folder
205 Args:
206 folder (str): what to zip
208 Returns:
209 Path: to zip
210 """
212 archive = Path(f"{folder}.zip")
213 shutil.make_archive(str(archive).replace(".zip", ""), "zip", folder)
215 return archive
217 def _upload_zip(self, zipfile: Path):
218 """
219 POST a zipfile to Ghost.
221 Returns:
222 str: uploaded filename
223 """
224 with zipfile.open("rb") as file:
225 resp = self.POST(
226 "upload",
227 files={"file": (zipfile.name, file, "application/zip")},
228 )
230 return resp["themes"][0]["name"]
232 def upload(self, file_or_folder: str):
233 """
234 Upload a zipfile or folder as a Ghost theme.
235 If a folder is selected it will be zipped before upload.
237 Args:
238 file_or_folder (str): path to theme.
240 """
241 path = Path(file_or_folder)
243 if path.is_file():
244 return self._upload_zip(path)
245 elif path.exists():
246 # -> is folder
247 file = self._create_zip(file_or_folder)
249 return self._upload_zip(file)
250 else:
251 raise FileNotFoundError(file_or_folder)
253 def activate(self, name: str):
254 """
255 Enable a theme by name
257 Returns:
258 str: the activated theme's name
259 """
260 resp = self.PUT(name, "activate")
262 return resp["themes"][0]["name"]
265# todo: (admin) tiers, offers, webhooks, ...?
266# todo: (content): ...?