wordpress-markdown-blog-loader 1.6.3__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.
- wordpress_markdown_blog_loader/__init__.py +0 -0
- wordpress_markdown_blog_loader/__main__.py +32 -0
- wordpress_markdown_blog_loader/api.py +658 -0
- wordpress_markdown_blog_loader/blog.py +621 -0
- wordpress_markdown_blog_loader/check_links.py +77 -0
- wordpress_markdown_blog_loader/download.py +67 -0
- wordpress_markdown_blog_loader/name_to_email.py +22 -0
- wordpress_markdown_blog_loader/new.py +158 -0
- wordpress_markdown_blog_loader/remove_newlines.py +22 -0
- wordpress_markdown_blog_loader/upload.py +112 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/METADATA +219 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/RECORD +16 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/WHEEL +5 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/entry_points.txt +2 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/licenses/LICENSE +202 -0
- wordpress_markdown_blog_loader-1.6.3.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import click
|
|
4
|
+
from wordpress_markdown_blog_loader import upload, download, new, check_links
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def main():
|
|
9
|
+
"""
|
|
10
|
+
Wordpress CLI
|
|
11
|
+
"""
|
|
12
|
+
logging.basicConfig(
|
|
13
|
+
level=os.getenv("LOG_LEVEL", "INFO"), format="%(levelname)s: %(message)s"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@main.group()
|
|
18
|
+
def posts():
|
|
19
|
+
"""
|
|
20
|
+
Wordpress posts up- and download
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
posts.add_command(upload.command)
|
|
25
|
+
posts.add_command(download.command)
|
|
26
|
+
posts.add_command(new.command)
|
|
27
|
+
posts.add_command(new.update_banner_command)
|
|
28
|
+
posts.add_command(check_links.command)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from functools import cache
|
|
10
|
+
from os.path import expanduser
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List, Dict, Iterator
|
|
13
|
+
from typing import Optional, Union
|
|
14
|
+
from urllib.parse import urlparse, ParseResult
|
|
15
|
+
|
|
16
|
+
import pytz
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_default_host() -> Optional[str]:
|
|
21
|
+
"""
|
|
22
|
+
returns the default api host from .wordpress.ini and/or ~/.wordpress.ini
|
|
23
|
+
"""
|
|
24
|
+
config = configparser.ConfigParser()
|
|
25
|
+
config.read([".wordpress.ini", expanduser("~/.wordpress.ini")])
|
|
26
|
+
return config.defaults().get("host")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_password(host: str) -> Optional[str]:
|
|
30
|
+
"""
|
|
31
|
+
Returns a password from the environment variable for the specified host.
|
|
32
|
+
|
|
33
|
+
The default environment variable is WP_APP_PASSWORD. A password for a specific host can be
|
|
34
|
+
specified by setting an environment variable WP_APP_PASSWORD_<host> where
|
|
35
|
+
the host name is all uppercase and non characters and digits replaced with _.
|
|
36
|
+
|
|
37
|
+
If not set, None is returned.
|
|
38
|
+
|
|
39
|
+
>>> os.environ["WP_APP_PASSWORD_XEBIA_COM"] = "host_password"
|
|
40
|
+
>>> get_password('xebia.com')
|
|
41
|
+
'host_password'
|
|
42
|
+
>>> get_password('binx.io') is None
|
|
43
|
+
True
|
|
44
|
+
>>> os.environ["WP_APP_PASSWORD"] = "default_password"
|
|
45
|
+
>>> get_password('binx.io')
|
|
46
|
+
'default_password'
|
|
47
|
+
"""
|
|
48
|
+
h = re.sub(r"[^a-zA-Z-0-9]", "_", host).upper()
|
|
49
|
+
if result := os.getenv(f"WP_APP_PASSWORD_{h}"):
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
return os.getenv("WP_APP_PASSWORD")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class WordpressEndpoint:
|
|
56
|
+
def __init__(self, **kwargs):
|
|
57
|
+
self.host = None
|
|
58
|
+
self.api_host = None
|
|
59
|
+
self.url = None
|
|
60
|
+
self.username = None
|
|
61
|
+
self.password = None
|
|
62
|
+
for key in self.__dict__.keys():
|
|
63
|
+
self.__setattr__(key, kwargs[key] if key in kwargs else None)
|
|
64
|
+
|
|
65
|
+
def read_from_config(self, host: str):
|
|
66
|
+
config = configparser.ConfigParser()
|
|
67
|
+
config.read([".wordpress.ini", expanduser("~/.wordpress.ini")])
|
|
68
|
+
|
|
69
|
+
if not host:
|
|
70
|
+
host = config.defaults().get("host")
|
|
71
|
+
assert host, "no host specified and no default host found"
|
|
72
|
+
|
|
73
|
+
self.host = host
|
|
74
|
+
self.api_host = config.get(host, "api_host", fallback=self.host)
|
|
75
|
+
self.url = f"https://{self.api_host}/wp-json/wp/v2"
|
|
76
|
+
|
|
77
|
+
self.username = config.get(host, "username")
|
|
78
|
+
self.password = get_password(host)
|
|
79
|
+
if not self.password:
|
|
80
|
+
logging.warning(
|
|
81
|
+
"taking plain text app password from configuration file, use WP_APP_PASSWORD environment variable instead!"
|
|
82
|
+
)
|
|
83
|
+
self.password = config.get(host, "password")
|
|
84
|
+
|
|
85
|
+
def is_host_for(self, url: Union[str, ParseResult]) -> bool:
|
|
86
|
+
result = url if isinstance(url, ParseResult) else urlparse(url)
|
|
87
|
+
return result.hostname in [self.host, self.api_host]
|
|
88
|
+
|
|
89
|
+
def normalize_url(self, url: str) -> str:
|
|
90
|
+
"""
|
|
91
|
+
>>> WordpressEndpoint(host="binx.io", api_host="binx.wpengine.com").normalize_url("https://binx.io/bla")
|
|
92
|
+
'https://binx.wpengine.com/bla'
|
|
93
|
+
>>> WordpressEndpoint(host="binx.io", api_host="binx.wpengine.com").normalize_url("https://xebia.com/bla")
|
|
94
|
+
'https://xebia.com/bla'
|
|
95
|
+
>>> WordpressEndpoint(host="binx.io", api_host="binx.wpengine.com").normalize_url("https://binx.io:443/bla")
|
|
96
|
+
'https://binx.wpengine.com:443/bla'
|
|
97
|
+
"""
|
|
98
|
+
if self.host == self.api_host:
|
|
99
|
+
return url
|
|
100
|
+
|
|
101
|
+
parsed_url = urlparse(url)
|
|
102
|
+
if parsed_url.hostname == self.host:
|
|
103
|
+
return parsed_url._replace(
|
|
104
|
+
netloc=parsed_url.netloc.replace(self.host, self.api_host)
|
|
105
|
+
).geturl()
|
|
106
|
+
|
|
107
|
+
return url
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def load(host: str = None) -> "WordpressEndpoint":
|
|
111
|
+
result = WordpressEndpoint()
|
|
112
|
+
result.read_from_config(host)
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class User(dict):
|
|
117
|
+
def __init__(self, u):
|
|
118
|
+
self.update(u)
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def name(self):
|
|
122
|
+
return self.get("name")
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def id(self):
|
|
126
|
+
return self.get("id")
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def email(self):
|
|
130
|
+
return self.get("email")
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def slug(self):
|
|
134
|
+
return self.get("slug")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Medium(dict):
|
|
138
|
+
def __init__(self, properties: dict):
|
|
139
|
+
self.update(properties)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def medium_id(self):
|
|
143
|
+
return int(self["id"])
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def url(self) -> str:
|
|
147
|
+
return self.get("guid").get("rendered")
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def link(self) -> str:
|
|
151
|
+
return self["link"]
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def slug(self) -> str:
|
|
155
|
+
return self.get("slug")
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def title(self) -> Optional[str]:
|
|
159
|
+
return self.get("title", {}).get("rendered")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Post(dict):
|
|
163
|
+
def __init__(self, p):
|
|
164
|
+
self.update(p)
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def slug(self):
|
|
168
|
+
return self["slug"] if self["slug"] else self.get("generated_slug")
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def post_id(self) -> id:
|
|
172
|
+
return int(self["id"])
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def link(self):
|
|
176
|
+
return self["link"]
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def guid(self) -> str:
|
|
180
|
+
return self["_links"]["self"][0]["href"]
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def title(self):
|
|
184
|
+
r = self["title"]["rendered"]
|
|
185
|
+
return r.replace("–", "-")
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def featured_media(self) -> Optional[int]:
|
|
189
|
+
return self.get("featured_media")
|
|
190
|
+
|
|
191
|
+
@featured_media.setter
|
|
192
|
+
def featured_media(self, medium_id: int):
|
|
193
|
+
self["featured_media"] = medium_id
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def content(self) -> Optional[str]:
|
|
197
|
+
return self.get("content", {}).get("rendered")
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def raw_content(self) -> Optional[str]:
|
|
201
|
+
return self.get("content", {}).get("raw")
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def categories(self) -> list[id]:
|
|
205
|
+
return self.get("categories", [])
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def industries_taxonomy(self) -> list[id]:
|
|
209
|
+
return self.get("industries_taxonomy", [])
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def partners_taxonomy(self) -> list[id]:
|
|
213
|
+
return self.get("partners_taxonomy", [])
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def capabilities(self) -> list[id]:
|
|
217
|
+
return self.get("capabilities", [])
|
|
218
|
+
|
|
219
|
+
@property
|
|
220
|
+
def tags(self) -> list[int]:
|
|
221
|
+
return self.get("tags", [])
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def author(self) -> int:
|
|
225
|
+
return self["author"]
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def excerpt(self) -> Optional[str]:
|
|
229
|
+
return self.get("excerpt", {}).get("rendered")
|
|
230
|
+
|
|
231
|
+
@excerpt.setter
|
|
232
|
+
def excerpt(self, exerpt):
|
|
233
|
+
self["excerpt"] = exerpt
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def date(self) -> datetime:
|
|
237
|
+
return (
|
|
238
|
+
pytz.timezone("utc")
|
|
239
|
+
.localize(datetime.fromisoformat(self.get("date_gmt")))
|
|
240
|
+
.astimezone()
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@date.setter
|
|
244
|
+
def date(self, new_date: datetime):
|
|
245
|
+
self["date"] = new_date
|
|
246
|
+
self["date_gmt"] = new_date.astimezone(pytz.timezone("utc"))
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def status(self):
|
|
250
|
+
return self["status"]
|
|
251
|
+
|
|
252
|
+
@status.setter
|
|
253
|
+
def status(self, new_status):
|
|
254
|
+
self["status"] = new_status
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def og_images(self) -> list[ParseResult]:
|
|
258
|
+
"""
|
|
259
|
+
returns urls to the og:image links
|
|
260
|
+
"""
|
|
261
|
+
result = []
|
|
262
|
+
for name in ["rank_math_twitter_image", "rank_math_facebook_image"]:
|
|
263
|
+
if image := self.get("meta", {}).get(name):
|
|
264
|
+
result.append(urlparse(image))
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def og_description(self) -> Optional[str]:
|
|
269
|
+
return self.get("meta", {}).get("rank_math_facebook_description")
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def permalink_template(self) -> Optional[str]:
|
|
273
|
+
return self.get("permalink_template")
|
|
274
|
+
|
|
275
|
+
@permalink_template.setter
|
|
276
|
+
def permalink_template(self, template: str):
|
|
277
|
+
self["permalink_template"] = template
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class PermissionDenied(Exception):
|
|
281
|
+
def __init__(self, msg):
|
|
282
|
+
super().__init__(msg)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class Wordpress(object):
|
|
286
|
+
def __init__(self, host: Optional[str] = None):
|
|
287
|
+
self.endpoint = WordpressEndpoint.load(host)
|
|
288
|
+
|
|
289
|
+
self._media: List[Medium] = {}
|
|
290
|
+
self.headers = {
|
|
291
|
+
"accept": "application/json",
|
|
292
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
|
|
293
|
+
}
|
|
294
|
+
self.session = requests.Session()
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def auth(self) -> (str, str):
|
|
298
|
+
return (self.endpoint.username, self.endpoint.password)
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def url(self) -> str:
|
|
302
|
+
return self.endpoint.url
|
|
303
|
+
|
|
304
|
+
def normalize_url(self, url: str) -> str:
|
|
305
|
+
return self.endpoint.normalize_url(url)
|
|
306
|
+
|
|
307
|
+
def is_host_for(self, url: Union[str, ParseResult]) -> bool:
|
|
308
|
+
return self.endpoint.is_host_for(url)
|
|
309
|
+
|
|
310
|
+
def get_all(self, resource: str, query: dict = None) -> Iterator[dict]:
|
|
311
|
+
params = query.copy() if query else {}
|
|
312
|
+
params["per_page"] = "100"
|
|
313
|
+
page = 1
|
|
314
|
+
total_pages = 1
|
|
315
|
+
while page <= total_pages:
|
|
316
|
+
params["page"] = str(page)
|
|
317
|
+
response = self.session.get(
|
|
318
|
+
f"{self.url}/{resource}",
|
|
319
|
+
auth=self.auth,
|
|
320
|
+
params=params,
|
|
321
|
+
headers=self.headers,
|
|
322
|
+
)
|
|
323
|
+
if response.status_code == 200:
|
|
324
|
+
for object in response.json():
|
|
325
|
+
yield object
|
|
326
|
+
total_pages = int(response.headers["X-WP-TotalPages"])
|
|
327
|
+
page = page + 1
|
|
328
|
+
else:
|
|
329
|
+
msg = f"failed to get all {resource}: {response.status_code}, {response.text}"
|
|
330
|
+
if response.status_code in [401, 403]:
|
|
331
|
+
raise PermissionDenied(msg)
|
|
332
|
+
else:
|
|
333
|
+
print(msg)
|
|
334
|
+
exit(1)
|
|
335
|
+
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
def users(self, query: dict = None) -> List["User"]:
|
|
339
|
+
return list(map(lambda u: User(u), self.get_all("users", query)))
|
|
340
|
+
|
|
341
|
+
def get_user_by_id(self, resource_id: int) -> "User":
|
|
342
|
+
return User(self.get("users", resource_id))
|
|
343
|
+
|
|
344
|
+
def get_unique_user_by_name(
|
|
345
|
+
self, name: str, email: Optional[str], author_id: Optional[str]
|
|
346
|
+
) -> "User":
|
|
347
|
+
|
|
348
|
+
user = self.get_user_by_id("me")
|
|
349
|
+
if user and user.name == name:
|
|
350
|
+
return user
|
|
351
|
+
|
|
352
|
+
users = []
|
|
353
|
+
try:
|
|
354
|
+
# The email field is only returned when the context edit is passed.
|
|
355
|
+
# See: https://developer.wordpress.org/rest-api/reference/users/
|
|
356
|
+
users = self.users({"search": name, "context": "edit"})
|
|
357
|
+
except PermissionDenied as error:
|
|
358
|
+
logging.warning("Permission denied to read user email addresses")
|
|
359
|
+
users = self.users({"search": name})
|
|
360
|
+
|
|
361
|
+
if len(users) == 0:
|
|
362
|
+
raise ValueError(f"author '{name}' not found on {self.endpoint.host}")
|
|
363
|
+
elif len(users) == 1:
|
|
364
|
+
return users[0]
|
|
365
|
+
|
|
366
|
+
if user := next(
|
|
367
|
+
filter(
|
|
368
|
+
lambda u: (author_id and u.slug == author_id)
|
|
369
|
+
or (
|
|
370
|
+
not author_id
|
|
371
|
+
and email
|
|
372
|
+
and u.email
|
|
373
|
+
and u.email.lower() == email.lower()
|
|
374
|
+
),
|
|
375
|
+
users,
|
|
376
|
+
),
|
|
377
|
+
None,
|
|
378
|
+
):
|
|
379
|
+
return user
|
|
380
|
+
|
|
381
|
+
candidates = ", ".join(["{} / {}".format(u.slug, u.email) for u in users])
|
|
382
|
+
if author_id:
|
|
383
|
+
raise ValueError(
|
|
384
|
+
f"Multiple authors named '{name}' found, none with author id {author_id} (possible: {candidates})."
|
|
385
|
+
)
|
|
386
|
+
elif email:
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"Multiple authors named '{name}' found, but none with email {email}. (possible: {candidates})."
|
|
389
|
+
)
|
|
390
|
+
else:
|
|
391
|
+
raise ValueError(
|
|
392
|
+
f"Multiple authors named '{name}' found. (possible: {candidates})."
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
def posts(self, query: dict = None) -> Iterator["Post"]:
|
|
396
|
+
for p in self.get_all("posts", query):
|
|
397
|
+
yield Post(p)
|
|
398
|
+
|
|
399
|
+
def get_post_by_slug(self, slug: str) -> Optional["Post"]:
|
|
400
|
+
return next(
|
|
401
|
+
filter(
|
|
402
|
+
lambda p: p.slug == slug,
|
|
403
|
+
(
|
|
404
|
+
map(
|
|
405
|
+
lambda u: Post(u),
|
|
406
|
+
self.get_all(
|
|
407
|
+
"posts", {"status": "draft,publish,pending", "slug": slug}
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
),
|
|
411
|
+
),
|
|
412
|
+
None,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def media(self) -> List[Medium]:
|
|
416
|
+
if not self._media:
|
|
417
|
+
self._media = [Medium(m) for m in self.get_all("media")]
|
|
418
|
+
return self._media
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
@cache
|
|
422
|
+
def categories(self) -> Dict[str, int]:
|
|
423
|
+
return {c["slug"]: c["id"] for c in self.get_all("categories")}
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
@cache
|
|
427
|
+
def categories_by_id(self) -> Dict[int, str]:
|
|
428
|
+
return {id: slug for slug, id in self.categories.items()}
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
@cache
|
|
432
|
+
def industries_taxonomy(self) -> Dict[str, int]:
|
|
433
|
+
return {c["slug"]: c["id"] for c in self.get_all("industries_taxonomy")}
|
|
434
|
+
|
|
435
|
+
@property
|
|
436
|
+
@cache
|
|
437
|
+
def industries_taxonomy_by_id(self) -> Dict[str, int]:
|
|
438
|
+
return {id: slug for slug, id in self.industries_taxonomy.items()}
|
|
439
|
+
|
|
440
|
+
@property
|
|
441
|
+
@cache
|
|
442
|
+
def partners_taxonomy(self) -> Dict[str, int]:
|
|
443
|
+
return {c["slug"]: c["id"] for c in self.get_all("partners_taxonomy")}
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
@cache
|
|
447
|
+
def partners_taxonomy_by_id(self) -> Dict[str, int]:
|
|
448
|
+
return {id: slug for slug, id in self.partners_taxonomy.items()}
|
|
449
|
+
|
|
450
|
+
@property
|
|
451
|
+
@cache
|
|
452
|
+
def capabilities(self) -> Dict[str, int]:
|
|
453
|
+
return {c["slug"]: c["id"] for c in self.get_all("capabilities")}
|
|
454
|
+
|
|
455
|
+
@property
|
|
456
|
+
@cache
|
|
457
|
+
def capabilities_by_id(self) -> Dict[str, int]:
|
|
458
|
+
return {id: slug for slug, id in self.capabilities.items()}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
@cache
|
|
463
|
+
def tags(self) -> Dict[str, int]:
|
|
464
|
+
return {c["slug"]: c["id"] for c in self.get_all("tags")}
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
@cache
|
|
468
|
+
def tags_by_id(self) -> Dict[int, str]:
|
|
469
|
+
return {id: slug for slug, id in self.tags.items()}
|
|
470
|
+
|
|
471
|
+
def search_for_image_by_slug(self, slug) -> Optional[Medium]:
|
|
472
|
+
response = self.session.get(
|
|
473
|
+
f"{self.url}/media",
|
|
474
|
+
auth=self.auth,
|
|
475
|
+
headers=self.headers,
|
|
476
|
+
params={"search": slug},
|
|
477
|
+
)
|
|
478
|
+
if response.status_code == 200:
|
|
479
|
+
matches = map(lambda i: Medium(i), response.json())
|
|
480
|
+
return next(filter(lambda i: slug in [i.slug, i.title], matches), None)
|
|
481
|
+
else:
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
def get_media(self, url: str) -> bytes:
|
|
485
|
+
response = self.session.get(
|
|
486
|
+
url, headers=self.headers, stream=True, auth=self.auth
|
|
487
|
+
)
|
|
488
|
+
assert response.status_code == 200, f"status code {response.status_code}"
|
|
489
|
+
return response.content
|
|
490
|
+
|
|
491
|
+
def upload_media(self, slug: str, path: Path) -> Medium:
|
|
492
|
+
old_content = []
|
|
493
|
+
|
|
494
|
+
with open(path, "rb") as file:
|
|
495
|
+
content = file.read()
|
|
496
|
+
|
|
497
|
+
stored_image = self.search_for_image_by_slug(slug)
|
|
498
|
+
if stored_image:
|
|
499
|
+
response = self.session.get(
|
|
500
|
+
stored_image.url,
|
|
501
|
+
headers={"User-Agent": self.headers["User-Agent"]},
|
|
502
|
+
stream=True,
|
|
503
|
+
auth=self.auth,
|
|
504
|
+
)
|
|
505
|
+
assert response.status_code == 200
|
|
506
|
+
old_content = response.content
|
|
507
|
+
|
|
508
|
+
if old_content != content:
|
|
509
|
+
if stored_image:
|
|
510
|
+
logging.info(
|
|
511
|
+
"force delete existing image under slug %s, id %s",
|
|
512
|
+
slug,
|
|
513
|
+
stored_image.medium_id,
|
|
514
|
+
)
|
|
515
|
+
delete_response = self.session.delete(
|
|
516
|
+
f"{self.url}/media/{stored_image.medium_id}",
|
|
517
|
+
auth=self.auth,
|
|
518
|
+
headers=self.headers,
|
|
519
|
+
params={"force": 1},
|
|
520
|
+
)
|
|
521
|
+
if delete_response.status_code not in [200, 201, 202]:
|
|
522
|
+
raise Exception(delete_response.text)
|
|
523
|
+
|
|
524
|
+
filename = f"{slug}{path.suffix}"
|
|
525
|
+
print(f"INFO: uploading image as {filename}")
|
|
526
|
+
|
|
527
|
+
headers = self.headers | {
|
|
528
|
+
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
529
|
+
"Content-Type": mimetypes.guess_type(path)[0],
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
response = self.session.post(
|
|
533
|
+
f"{self.url}/media/",
|
|
534
|
+
data=content,
|
|
535
|
+
auth=self.auth,
|
|
536
|
+
headers=headers,
|
|
537
|
+
params={
|
|
538
|
+
"slug": slug,
|
|
539
|
+
"title": slug,
|
|
540
|
+
},
|
|
541
|
+
)
|
|
542
|
+
if response.status_code not in [200, 201]:
|
|
543
|
+
raise Exception(response.text)
|
|
544
|
+
|
|
545
|
+
stored_image = Medium(response.json())
|
|
546
|
+
self.session.patch(
|
|
547
|
+
f"{self.url}/media/{stored_image.medium_id}",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
self._media[slug] = stored_image
|
|
551
|
+
return self._media[slug]
|
|
552
|
+
|
|
553
|
+
def find_image_by_link(self, link: Union[str, ParseResult]) -> Optional[Medium]:
|
|
554
|
+
url = link if isinstance(link, ParseResult) else urlparse(link)
|
|
555
|
+
stem = Path(url.path).stem
|
|
556
|
+
|
|
557
|
+
if not (self.is_host_for(link) and url.path.startswith("/wp-content/uploads/")):
|
|
558
|
+
return None
|
|
559
|
+
|
|
560
|
+
return next(
|
|
561
|
+
filter(
|
|
562
|
+
lambda m: url.geturl() == m.url,
|
|
563
|
+
map(lambda m: Medium(m), self.get_all("media", {"search": stem})),
|
|
564
|
+
),
|
|
565
|
+
None,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
def update_post(self, guid: str, properties: dict):
|
|
569
|
+
response = self.session.patch(
|
|
570
|
+
self.normalize_url(guid),
|
|
571
|
+
json=properties,
|
|
572
|
+
auth=self.auth,
|
|
573
|
+
headers=self.headers,
|
|
574
|
+
)
|
|
575
|
+
if response.status_code not in [200, 201]:
|
|
576
|
+
raise Exception(response.text)
|
|
577
|
+
return Post(response.json())
|
|
578
|
+
|
|
579
|
+
def create_post(self, properties: dict) -> "Post":
|
|
580
|
+
response = self.session.post(
|
|
581
|
+
f"{self.url}/posts",
|
|
582
|
+
json=properties,
|
|
583
|
+
auth=self.auth,
|
|
584
|
+
headers=self.headers,
|
|
585
|
+
)
|
|
586
|
+
if response.status_code not in [200, 201]:
|
|
587
|
+
raise Exception(response.text)
|
|
588
|
+
return Post(response.json())
|
|
589
|
+
|
|
590
|
+
def get_resource_by_url(self, url: str, params: dict = {}) -> Optional[dict]:
|
|
591
|
+
response = self.session.get(
|
|
592
|
+
self.normalize_url(url), auth=self.auth, headers=self.headers, params=params
|
|
593
|
+
)
|
|
594
|
+
if response.status_code == 200:
|
|
595
|
+
return response.json()
|
|
596
|
+
elif response.status_code == 404:
|
|
597
|
+
return None
|
|
598
|
+
else:
|
|
599
|
+
raise Exception(response.text)
|
|
600
|
+
|
|
601
|
+
def get(self, resource, resource_id, params: dict = {}) -> Optional[dict]:
|
|
602
|
+
return self.get_resource_by_url(f"{self.url}/{resource}/{resource_id}", params)
|
|
603
|
+
|
|
604
|
+
def connect(self):
|
|
605
|
+
global categories
|
|
606
|
+
|
|
607
|
+
categories = {c["slug"]: c["id"] for c in self.get_all("categories")}
|
|
608
|
+
|
|
609
|
+
def get_category_id_by_name(self, category: str) -> str:
|
|
610
|
+
if category in self.categories:
|
|
611
|
+
return self.categories[category]
|
|
612
|
+
|
|
613
|
+
raise ValueError(
|
|
614
|
+
"invalid category '{}' try one of\n {}".format(
|
|
615
|
+
category, ",\n ".join(self.categories.keys())
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
def get_industry_by_name(self, slug: str) -> str:
|
|
620
|
+
if slug in self.industries_taxonomy:
|
|
621
|
+
return self.industries_taxonomy[slug]
|
|
622
|
+
|
|
623
|
+
raise ValueError(
|
|
624
|
+
"invalid industry '{}' try one of\n {}".format(
|
|
625
|
+
slug, ",\n ".join(self.industries_taxonomy.keys())
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def get_partner_by_name(self, slug: str) -> str:
|
|
630
|
+
if slug in self.partners_taxonomy:
|
|
631
|
+
return self.partners_taxonomy[slug]
|
|
632
|
+
|
|
633
|
+
raise ValueError(
|
|
634
|
+
"invalid partner '{}' try one of\n {}".format(
|
|
635
|
+
slug, ",\n ".join(self.partners_taxonomy.keys())
|
|
636
|
+
)
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
def get_capabilities_by_name(self, slug: str) -> str:
|
|
640
|
+
if slug in self.capabilities:
|
|
641
|
+
return self.capabilities[slug]
|
|
642
|
+
|
|
643
|
+
raise ValueError(
|
|
644
|
+
"invalid capability '{}' try one of\n {}".format(
|
|
645
|
+
slug, ",\n ".join(self.capabilities.keys())
|
|
646
|
+
)
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def get_tag_id_by_name(self, tag: str) -> str:
|
|
651
|
+
if tag in self.tags:
|
|
652
|
+
return self.tags[tag]
|
|
653
|
+
|
|
654
|
+
raise ValueError(
|
|
655
|
+
"invalid tag '{}' try one of\n {}".format(
|
|
656
|
+
tag, ",\n ".join(self.tags.keys())
|
|
657
|
+
)
|
|
658
|
+
)
|