Coverage for ghost/resources.py: 76%

62 statements  

« 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 

5 

6from .abs_resources import GhostAdminResource, GhostContentResource, GhostResource 

7 

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] 

23 

24 

25# Admin 

26 

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. 

29 

30 

31class PostResource(GhostAdminResource): 

32 # See: https://ghost.org/docs/admin-api/#the-post-object 

33 resource = "posts" 

34 

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, 

71 

72 

73class PageResource(GhostAdminResource): 

74 # See: https://ghost.org/docs/admin-api/#the-post-object 

75 resource = "pages" 

76 

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, 

113 

114 

115class TagResource(GhostAdminResource): 

116 # experimental 

117 resource = "tags" 

118 

119 

120class MemberResource(GhostAdminResource): 

121 resource = "members" 

122 

123 

124class UserResource(GhostAdminResource): 

125 resource = "users" 

126 

127 

128# Content 

129 

130 

131class AuthorResource(GhostContentResource): 

132 resource = "authors" 

133 

134 

135class SettingsResource(GhostContentResource): 

136 resource = "settings" 

137 

138 

139# Custom 

140 

141 

142class SiteResource(GhostResource): 

143 resource = "site" 

144 api = "admin" 

145 

146 def get(self, _=None, **__): 

147 """ 

148 The site resource simple returns info about the site 

149 """ 

150 return self.GET()["site"] 

151 

152 

153class ImageResource(GhostResource): 

154 resource = "images" 

155 api = "admin" 

156 

157 def _load(self, path: str): 

158 """ 

159 Load an image by path 

160 

161 Args: 

162 path (str): to the image 

163 

164 Returns: 

165 BytesIO: image bytes 

166 """ 

167 with open(path, "rb") as image: 

168 return BytesIO(image.read()) 

169 

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) 

175 

176 Returns: 

177 str: uploaded image URL 

178 """ 

179 # todo: support other filetypes than image/jpeg 

180 

181 if isinstance(image_obj_or_path, str): 

182 if not image_name: 

183 image_name = os.path.basename(image_obj_or_path) 

184 

185 image_obj_or_path = self._load(image_obj_or_path) 

186 

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) 

193 

194 return result["images"][0]["url"] 

195 

196 

197class ThemeResource(GhostResource): 

198 resource = "themes" 

199 api = "admin" 

200 

201 def _create_zip(self, folder: str): 

202 """ 

203 Create a zip of folder 

204 

205 Args: 

206 folder (str): what to zip 

207 

208 Returns: 

209 Path: to zip 

210 """ 

211 

212 archive = Path(f"{folder}.zip") 

213 shutil.make_archive(str(archive).replace(".zip", ""), "zip", folder) 

214 

215 return archive 

216 

217 def _upload_zip(self, zipfile: Path): 

218 """ 

219 POST a zipfile to Ghost. 

220 

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 ) 

229 

230 return resp["themes"][0]["name"] 

231 

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. 

236 

237 Args: 

238 file_or_folder (str): path to theme. 

239 

240 """ 

241 path = Path(file_or_folder) 

242 

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) 

248 

249 return self._upload_zip(file) 

250 else: 

251 raise FileNotFoundError(file_or_folder) 

252 

253 def activate(self, name: str): 

254 """ 

255 Enable a theme by name 

256 

257 Returns: 

258 str: the activated theme's name 

259 """ 

260 resp = self.PUT(name, "activate") 

261 

262 return resp["themes"][0]["name"] 

263 

264 

265# todo: (admin) tiers, offers, webhooks, ...? 

266# todo: (content): ...?