Coverage for ghost/resources.py: 76%

1 import os.path

2 import shutil

3 from io import BytesIO

4 from pathlib import Path

5

6 from .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

31 class 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

73 class 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

115 class TagResource(GhostAdminResource):

116 # experimental

117 resource = "tags"

118

119

120 class MemberResource(GhostAdminResource):

121 resource = "members"

122

123

124 class UserResource(GhostAdminResource):

125 resource = "users"

126

127

128# Content

129

130

131 class AuthorResource(GhostContentResource):

132 resource = "authors"

133

134

135 class SettingsResource(GhostContentResource):

136 resource = "settings"

137

138

139# Custom

140

141

142 class 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

153 class 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

197 class 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): ...?