opengraph-writer 1.0.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.
- opengraph_writer/__init__.py +867 -0
- opengraph_writer/py.typed +0 -0
- opengraph_writer/pyramid_helpers.py +25 -0
- opengraph_writer-1.0.0.dist-info/METADATA +121 -0
- opengraph_writer-1.0.0.dist-info/RECORD +8 -0
- opengraph_writer-1.0.0.dist-info/WHEEL +5 -0
- opengraph_writer-1.0.0.dist-info/licenses/LICENSE.txt +21 -0
- opengraph_writer-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
"""
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
The followig information about Open Graph 1.0 is copied verbatim from Facebook, and remains their copyright:
|
|
5
|
+
|
|
6
|
+
https://developers.facebook.com/docs/opengraphprotocol/
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
|
|
10
|
+
<meta property="og:title" content="The Rock"/>
|
|
11
|
+
|
|
12
|
+
The Open Graph protocol defines four required properties:
|
|
13
|
+
|
|
14
|
+
og:title - The title of your object as it should appear within the graph, e.g., "The Rock".
|
|
15
|
+
og:type - The type of your object, e.g., "movie". See the complete list of supported types.
|
|
16
|
+
og:image - An image URL which should represent your object within the graph. The image must be at least 50px by 50px and have a maximum aspect ratio of 3:1. We support PNG, JPEG and GIF formats. You may include multiple og:image tags to associate multiple images with your page.
|
|
17
|
+
og:url - The canonical URL of your object that will be used as its permanent ID in the graph, e.g., http://www.imdb.com/title/tt0117500/.
|
|
18
|
+
|
|
19
|
+
In addition, we've extended the basic meta data to add a required field to connect your webpage with Facebook:
|
|
20
|
+
|
|
21
|
+
fb:app_id - A Facebook Platform application ID that administers this page.
|
|
22
|
+
|
|
23
|
+
It's also recommended that you include the following properties as well as these multi-part properties.
|
|
24
|
+
|
|
25
|
+
og:site_name - A human-readable name for your site, e.g., "IMDb".
|
|
26
|
+
og:description - A one to two sentence description of your page.
|
|
27
|
+
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
To associate the page with your Facebook account, add the additional property fb:admins to your page with a comma-separated list of the user IDs or usernames of the Facebook accounts who own the page, e.g.:
|
|
31
|
+
|
|
32
|
+
<meta property="fb:admins" content="USER_ID1,USER_ID2"/>
|
|
33
|
+
|
|
34
|
+
The following is taken from the 2.0 spec -- http://ogp.me/, and remains their copyright
|
|
35
|
+
|
|
36
|
+
Arrays
|
|
37
|
+
If a tag can have multiple values, just put multiple versions of the same <meta> tag on your page. The first tag (from top to bottom) is given preference during conflicts.
|
|
38
|
+
|
|
39
|
+
<meta property="og:image" content="http://example.com/rock.jpg" />
|
|
40
|
+
<meta property="og:image" content="http://example.com/rock2.jpg" />
|
|
41
|
+
|
|
42
|
+
Put structured properties after you declare their root tag. Whenever another root element is parsed, that structured property is considered to be done and another one is started.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
__VERSION__ = "1.0.0"
|
|
46
|
+
|
|
47
|
+
# stdlib
|
|
48
|
+
import datetime
|
|
49
|
+
import re
|
|
50
|
+
import typing
|
|
51
|
+
|
|
52
|
+
# pypi
|
|
53
|
+
from metadata_utils import html_attribute_escape
|
|
54
|
+
|
|
55
|
+
# typing
|
|
56
|
+
_OG_KV = typing.Tuple[str, typing.Any]
|
|
57
|
+
_OG_KV_MANY = typing.Iterable[_OG_KV]
|
|
58
|
+
_OG_DATA = typing.Dict[str, typing.Any]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# http://en.wikipedia.org/wiki/ISO_8601
|
|
62
|
+
regex_dates = {
|
|
63
|
+
# Date 2012-02-02
|
|
64
|
+
"date": re.compile("^([0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])$"),
|
|
65
|
+
# Ordinal date: 2012-033
|
|
66
|
+
"ordinal_date": re.compile(
|
|
67
|
+
"^([0-9]{4})-(36[0-6]|3[0-5][0-9]|[12][0-9]{2}|0[1-9][0-9]|00[1-9])$"
|
|
68
|
+
),
|
|
69
|
+
# Date with week number: 2012-W05-4
|
|
70
|
+
"week_number": re.compile("^([0-9]{4})-?W(5[0-3]|[1-4][0-9]|0[1-9])-?([1-7])$"),
|
|
71
|
+
# Separate date and time in UTC: 2012-02-02 15:29Z
|
|
72
|
+
"datetime, UTC": re.compile(
|
|
73
|
+
"^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9])T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(.[0-9]+)?(Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$"
|
|
74
|
+
),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# basically just testing that the string starts with http/https, and has some resemeblence of a domain on it. after an optional trailing slash, i don't need to make this super accurate
|
|
79
|
+
regex_url = re.compile(r"""^http[s]?:\/\/[a-z0-9.\-]+[.][a-z]{2,4}\/?""")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
OG_PROPERTIES: typing.Dict[str, dict] = {
|
|
83
|
+
"og:title": {
|
|
84
|
+
"required": True,
|
|
85
|
+
"description": 'The title of your object as it should appear within the graph, e.g., "The Rock".',
|
|
86
|
+
"type": "string",
|
|
87
|
+
},
|
|
88
|
+
"og:type": {
|
|
89
|
+
"required": True,
|
|
90
|
+
"description": 'The type of your object, e.g., "movie". See the complete list of supported types.',
|
|
91
|
+
"type": "string",
|
|
92
|
+
"valid_types-1": {
|
|
93
|
+
"activity": {"grouping": "Activities"},
|
|
94
|
+
"sport": {"grouping": "Activities"},
|
|
95
|
+
"bar": {"grouping": "Businesses"},
|
|
96
|
+
"company": {"grouping": "Businesses"},
|
|
97
|
+
"cafe": {"grouping": "Businesses"},
|
|
98
|
+
"hotel": {"grouping": "Businesses"},
|
|
99
|
+
"restaurant": {"grouping": "Businesses"},
|
|
100
|
+
"cause": {"grouping": "Groups"},
|
|
101
|
+
"sports_league": {"grouping": "Groups"},
|
|
102
|
+
"sports_team": {"grouping": "Groups"},
|
|
103
|
+
"band": {"grouping": "Organizations"},
|
|
104
|
+
"government": {"grouping": "Organizations"},
|
|
105
|
+
"non_profit": {"grouping": "Organizations"},
|
|
106
|
+
"school": {"grouping": "Organizations"},
|
|
107
|
+
"university": {"grouping": "Organizations"},
|
|
108
|
+
"actor": {"grouping": "People"},
|
|
109
|
+
"athlete": {"grouping": "People"},
|
|
110
|
+
"author": {"grouping": "People "},
|
|
111
|
+
"director": {"grouping": "People"},
|
|
112
|
+
"musician": {"grouping": "People"},
|
|
113
|
+
"politician": {"grouping": "People"},
|
|
114
|
+
"public_figure": {"grouping": "People"},
|
|
115
|
+
"city": {"grouping": "Places"},
|
|
116
|
+
"country": {"grouping": "Places"},
|
|
117
|
+
"landmark": {"grouping": "Places"},
|
|
118
|
+
"state_province": {"grouping": "Places"},
|
|
119
|
+
"album": {"grouping": "Products and Entertainment"},
|
|
120
|
+
"book": {"grouping": "Products and Entertainment"},
|
|
121
|
+
"drink": {"grouping": "Products and Entertainment"},
|
|
122
|
+
"food": {"grouping": "Products and Entertainment"},
|
|
123
|
+
"game": {"grouping": "Products and Entertainment"},
|
|
124
|
+
"product": {"grouping": "Products and Entertainment"},
|
|
125
|
+
"song": {"grouping": "Products and Entertainment"},
|
|
126
|
+
"movie": {"grouping": "Products and Entertainment"},
|
|
127
|
+
"tv_show": {"grouping": "Products and Entertainment"},
|
|
128
|
+
"blog": {"grouping": "Websites "},
|
|
129
|
+
"website": {"grouping": "Websites"},
|
|
130
|
+
"article": {"grouping": "Websites"},
|
|
131
|
+
"game.achievement": {
|
|
132
|
+
"grouping": "Game",
|
|
133
|
+
"properties": {
|
|
134
|
+
"game:points": {"description": "POINTS_FOR_ACHIEVEMENT"}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
"valid_types-2": {
|
|
139
|
+
"website": {"namespace": "http://ogp.me/ns/website#", "properties": {}},
|
|
140
|
+
"article": {
|
|
141
|
+
"namespace": "http://ogp.me/ns/article#",
|
|
142
|
+
"properties": {
|
|
143
|
+
"article:published_time": {
|
|
144
|
+
"type": "datetime",
|
|
145
|
+
"description": "When the article was first published.",
|
|
146
|
+
},
|
|
147
|
+
"article:modified_time": {
|
|
148
|
+
"type": "datetime",
|
|
149
|
+
"description": "When the article was last changed.",
|
|
150
|
+
},
|
|
151
|
+
"article:expiration_time": {
|
|
152
|
+
"type": "datetime",
|
|
153
|
+
"description": "When the article is out of date after.",
|
|
154
|
+
},
|
|
155
|
+
"article:author": {
|
|
156
|
+
"type": "profile",
|
|
157
|
+
"description": "Writers of the article.",
|
|
158
|
+
"array_allowed": True,
|
|
159
|
+
},
|
|
160
|
+
"article:section": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"description": "A high-level section name. E.g. Technology",
|
|
163
|
+
},
|
|
164
|
+
"article:tag": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"description": "Tag words associated with this article.",
|
|
167
|
+
"array_allowed": True,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
"book": {
|
|
172
|
+
"namespace": "http://ogp.me/ns/book#",
|
|
173
|
+
"properties": {
|
|
174
|
+
"book:author": {
|
|
175
|
+
"type": "profile",
|
|
176
|
+
"description": "Who wrote this book.",
|
|
177
|
+
"array_allowed": True,
|
|
178
|
+
},
|
|
179
|
+
"book:isbn": {"type": "string", "description": "The ISBN"},
|
|
180
|
+
"book:release_date": {
|
|
181
|
+
"type": "datetime",
|
|
182
|
+
"description": "The date the book was released.",
|
|
183
|
+
},
|
|
184
|
+
"book:tag": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "Tag words associated with this book.",
|
|
187
|
+
"array_allowed": True,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
"profile": {
|
|
192
|
+
"namespace": "http://ogp.me/ns/profile#",
|
|
193
|
+
"properties": {
|
|
194
|
+
"profile:first_name": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"description": "first name",
|
|
197
|
+
},
|
|
198
|
+
"profile:last_name": {"type": "string", "description": "last name"},
|
|
199
|
+
"profile:username": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "A short unique string to identify them.",
|
|
202
|
+
},
|
|
203
|
+
"profile:gender": {
|
|
204
|
+
"type": "enum",
|
|
205
|
+
"enums": ["male", "female"],
|
|
206
|
+
"description": "Their gender",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
"video.movie": {
|
|
211
|
+
"namespace": "http://ogp.me/ns/video#",
|
|
212
|
+
"properties": {
|
|
213
|
+
"video:actor": {
|
|
214
|
+
"type": "profile",
|
|
215
|
+
"description": "actors in the movie",
|
|
216
|
+
"array_allowed": True,
|
|
217
|
+
},
|
|
218
|
+
"video:actor:role": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"description": "the role they played",
|
|
221
|
+
},
|
|
222
|
+
"video:director": {
|
|
223
|
+
"type": "profile",
|
|
224
|
+
"description": "directors of the movie",
|
|
225
|
+
"array_allowed": True,
|
|
226
|
+
},
|
|
227
|
+
"video:writer": {
|
|
228
|
+
"type": "profile",
|
|
229
|
+
"description": "writers of the movie",
|
|
230
|
+
"array_allowed": True,
|
|
231
|
+
},
|
|
232
|
+
"video:duration": {
|
|
233
|
+
"type": "integer",
|
|
234
|
+
"description": "The movie's length in seconds",
|
|
235
|
+
},
|
|
236
|
+
"video:release_date": {
|
|
237
|
+
"type": "datetime",
|
|
238
|
+
"description": "The date the movie was released",
|
|
239
|
+
},
|
|
240
|
+
"video:tag": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "Tag words associated with this video.",
|
|
243
|
+
"array_allowed": True,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
"video.episode": {
|
|
248
|
+
"namespace": "http://ogp.me/ns/video#",
|
|
249
|
+
"properties": {
|
|
250
|
+
"video:actor": {
|
|
251
|
+
"type": "profile",
|
|
252
|
+
"description": "actors in the movie",
|
|
253
|
+
"array_allowed": True,
|
|
254
|
+
},
|
|
255
|
+
"video:actor:role": {
|
|
256
|
+
"type": "string",
|
|
257
|
+
"description": "the role they played",
|
|
258
|
+
},
|
|
259
|
+
"video:director": {
|
|
260
|
+
"type": "profile",
|
|
261
|
+
"description": "directors of the movie",
|
|
262
|
+
"array_allowed": True,
|
|
263
|
+
},
|
|
264
|
+
"video:writer": {
|
|
265
|
+
"type": "profile",
|
|
266
|
+
"description": "writers of the movie",
|
|
267
|
+
"array_allowed": True,
|
|
268
|
+
},
|
|
269
|
+
"video:duration": {
|
|
270
|
+
"type": "integer",
|
|
271
|
+
"description": "The movie's length in seconds",
|
|
272
|
+
},
|
|
273
|
+
"video:release_date": {
|
|
274
|
+
"type": "datetime",
|
|
275
|
+
"description": "The date the movie was released",
|
|
276
|
+
},
|
|
277
|
+
"video:tag": {
|
|
278
|
+
"type": "string",
|
|
279
|
+
"description": "Tag words associated with this video.",
|
|
280
|
+
"array_allowed": True,
|
|
281
|
+
},
|
|
282
|
+
"video:series": {
|
|
283
|
+
"type": "video.tv_show",
|
|
284
|
+
"description": "Which series this episode belongs to.",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
"video.tv_show": {
|
|
289
|
+
"namespace": "http://ogp.me/ns/video#",
|
|
290
|
+
"properties": {
|
|
291
|
+
"video:actor": {
|
|
292
|
+
"type": "profile",
|
|
293
|
+
"description": "actors in the movie",
|
|
294
|
+
"array_allowed": True,
|
|
295
|
+
},
|
|
296
|
+
"video:actor:role": {
|
|
297
|
+
"type": "string",
|
|
298
|
+
"description": "the role they played",
|
|
299
|
+
},
|
|
300
|
+
"video:director": {
|
|
301
|
+
"type": "profile",
|
|
302
|
+
"description": "directors of the movie",
|
|
303
|
+
"array_allowed": True,
|
|
304
|
+
},
|
|
305
|
+
"video:writer": {
|
|
306
|
+
"type": "profile",
|
|
307
|
+
"description": "writers of the movie",
|
|
308
|
+
"array_allowed": True,
|
|
309
|
+
},
|
|
310
|
+
"video:duration": {
|
|
311
|
+
"type": "integer",
|
|
312
|
+
"description": "The movie's length in seconds",
|
|
313
|
+
},
|
|
314
|
+
"video:release_date": {
|
|
315
|
+
"type": "datetime",
|
|
316
|
+
"description": "The date the movie was released",
|
|
317
|
+
},
|
|
318
|
+
"video:tag": {
|
|
319
|
+
"type": "string",
|
|
320
|
+
"description": "Tag words associated with this video.",
|
|
321
|
+
"array_allowed": True,
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
"video.other": {
|
|
326
|
+
"namespace": "http://ogp.me/ns/video#",
|
|
327
|
+
"properties": {
|
|
328
|
+
"video:actor": {
|
|
329
|
+
"type": "profile",
|
|
330
|
+
"description": "actors in the movie",
|
|
331
|
+
"array_allowed": True,
|
|
332
|
+
},
|
|
333
|
+
"video:actor:role": {
|
|
334
|
+
"type": "string",
|
|
335
|
+
"description": "the role they played",
|
|
336
|
+
},
|
|
337
|
+
"video:director": {
|
|
338
|
+
"type": "profile",
|
|
339
|
+
"description": "directors of the movie",
|
|
340
|
+
"array_allowed": True,
|
|
341
|
+
},
|
|
342
|
+
"video:writer": {
|
|
343
|
+
"type": "profile",
|
|
344
|
+
"description": "writers of the movie",
|
|
345
|
+
"array_allowed": True,
|
|
346
|
+
},
|
|
347
|
+
"video:duration": {
|
|
348
|
+
"type": "integer",
|
|
349
|
+
"description": "The movie's length in seconds",
|
|
350
|
+
},
|
|
351
|
+
"video:release_date": {
|
|
352
|
+
"type": "datetime",
|
|
353
|
+
"description": "The date the movie was released",
|
|
354
|
+
},
|
|
355
|
+
"video:tag": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"description": "Tag words associated with this video.",
|
|
358
|
+
"array_allowed": True,
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
"music.song": {
|
|
363
|
+
"namespace": "http://ogp.me/ns/music#",
|
|
364
|
+
"properties": {
|
|
365
|
+
"music:duration": {
|
|
366
|
+
"type": "integer",
|
|
367
|
+
"description": "The song's length in seconds",
|
|
368
|
+
},
|
|
369
|
+
"music:album": {
|
|
370
|
+
"type": "music.album",
|
|
371
|
+
"description": "The album this song is from.",
|
|
372
|
+
"array_allowed": True,
|
|
373
|
+
},
|
|
374
|
+
"music:album:disc": {
|
|
375
|
+
"type": "integer",
|
|
376
|
+
"description": "Which disc of the album this song is on",
|
|
377
|
+
},
|
|
378
|
+
"music:album:track": {
|
|
379
|
+
"type": "integer",
|
|
380
|
+
"description": "Which track this song is",
|
|
381
|
+
},
|
|
382
|
+
"music:musician": {
|
|
383
|
+
"type": "profile",
|
|
384
|
+
"description": "The musician that made this song",
|
|
385
|
+
"array_allowed": True,
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
"music.album": {
|
|
390
|
+
"namespace": "http://ogp.me/ns/music#",
|
|
391
|
+
"properties": {
|
|
392
|
+
"music:song": {
|
|
393
|
+
"type": "music.song",
|
|
394
|
+
"description": "The song on this album.",
|
|
395
|
+
},
|
|
396
|
+
"music:song:disc": {
|
|
397
|
+
"type": "integer",
|
|
398
|
+
"description": "the disc the song is on for the album",
|
|
399
|
+
},
|
|
400
|
+
"music:song:track": {
|
|
401
|
+
"type": "integer",
|
|
402
|
+
"description": "the track number the song is on for the album",
|
|
403
|
+
},
|
|
404
|
+
"music:musician": {
|
|
405
|
+
"type": "profile",
|
|
406
|
+
"description": "The musician that made this song",
|
|
407
|
+
},
|
|
408
|
+
"music:release_date": {
|
|
409
|
+
"type": "datetime",
|
|
410
|
+
"description": "The date the album was released.",
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
"music.playlist": {
|
|
415
|
+
"namespace": "http://ogp.me/ns/music#",
|
|
416
|
+
"properties": {
|
|
417
|
+
"music:song": {"type": "music.song", "description": "The song"},
|
|
418
|
+
"music:song:disc": {
|
|
419
|
+
"type": "integer",
|
|
420
|
+
"description": "the disc the song is on for the album",
|
|
421
|
+
},
|
|
422
|
+
"music:song:track": {
|
|
423
|
+
"type": "integer",
|
|
424
|
+
"description": "the track number the song is on for the album",
|
|
425
|
+
},
|
|
426
|
+
"music:creator": {
|
|
427
|
+
"type": "profile",
|
|
428
|
+
"description": "The creator of this playlist.",
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
"music.radio_station": {
|
|
433
|
+
"namespace": "http://ogp.me/ns/music#",
|
|
434
|
+
"properties": {
|
|
435
|
+
"music:creator": {
|
|
436
|
+
"type": "profile",
|
|
437
|
+
"description": "The creator of this station.",
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
"og:image": {
|
|
444
|
+
"required": True,
|
|
445
|
+
"type": "url",
|
|
446
|
+
"description": "An image URL which should represent your object within the graph. The image must be at least 50px by 50px and have a maximum aspect ratio of 3:1. We support PNG, JPEG and GIF formats. You may include multiple og:image tags to associate multiple images with your page.",
|
|
447
|
+
"properties": {
|
|
448
|
+
"og:image:url": {"description": "Identical to og:image.", "type": "url"},
|
|
449
|
+
"og:image:secure_url": {
|
|
450
|
+
"description": " An alternate url to use if the webpage requires HTTPS.",
|
|
451
|
+
"type": "url",
|
|
452
|
+
},
|
|
453
|
+
"og:image:type": {
|
|
454
|
+
"description": "A MIME type for this image.",
|
|
455
|
+
"type": "string",
|
|
456
|
+
},
|
|
457
|
+
"og:image:width": {
|
|
458
|
+
"description": "The number of pixels wide.",
|
|
459
|
+
"type": "integer",
|
|
460
|
+
},
|
|
461
|
+
"og:image:height": {
|
|
462
|
+
"description": "The number of pixels high.",
|
|
463
|
+
"type": "integer",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
"og:url": {
|
|
468
|
+
"required": True,
|
|
469
|
+
"description": "The canonical URL of your object that will be used as its permanent ID in the graph, e.g., http://www.imdb.com/title/tt0117500/",
|
|
470
|
+
"type": "url",
|
|
471
|
+
},
|
|
472
|
+
"og:site_name": {
|
|
473
|
+
"required": False,
|
|
474
|
+
"description": 'A human-readable name for your site, e.g., "IMDb".',
|
|
475
|
+
"type": "string",
|
|
476
|
+
},
|
|
477
|
+
"og:description": {
|
|
478
|
+
"required": False,
|
|
479
|
+
"description": "A one to two sentence description of your page.",
|
|
480
|
+
"type": "string",
|
|
481
|
+
},
|
|
482
|
+
"og:isbn": {
|
|
483
|
+
"required": False,
|
|
484
|
+
"description": "For products which have a UPC code or ISBN number, you can specify them using the og:upc and og:isbn properties. These properties help uniquely identify products.",
|
|
485
|
+
"type": "string",
|
|
486
|
+
},
|
|
487
|
+
"og:upc": {
|
|
488
|
+
"required": False,
|
|
489
|
+
"description": "For products which have a UPC code or ISBN number, you can specify them using the og:upc and og:isbn properties. These properties help uniquely identify products.",
|
|
490
|
+
"type": "string",
|
|
491
|
+
},
|
|
492
|
+
"og:audio": {
|
|
493
|
+
"required": False,
|
|
494
|
+
"description": "A URL to an audio file to accompany this object.",
|
|
495
|
+
"type": "url",
|
|
496
|
+
"properties": {
|
|
497
|
+
"og:audio:secure_url": {
|
|
498
|
+
"description": " An alternate url to use if the webpage requires HTTPS.",
|
|
499
|
+
"type": "url",
|
|
500
|
+
},
|
|
501
|
+
"og:audio:type": {
|
|
502
|
+
"description": "A MIME type for this audio.",
|
|
503
|
+
"type": "string",
|
|
504
|
+
},
|
|
505
|
+
"og:audio:title": {
|
|
506
|
+
"description": "NOT IN 2.0 SPEC -- song title",
|
|
507
|
+
"type": "string",
|
|
508
|
+
},
|
|
509
|
+
"og:audio:artist": {
|
|
510
|
+
"description": "NOT IN 2.0 SPEC -- song artist",
|
|
511
|
+
"type": "string",
|
|
512
|
+
},
|
|
513
|
+
"og:audio:album": {
|
|
514
|
+
"description": "NOT IN 2.0 SPEC -- song album",
|
|
515
|
+
"type": "string",
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
"og:determiner": {
|
|
520
|
+
"required": False,
|
|
521
|
+
"description": """The word that appears before this object's title in a sentence. An enum of (a, an, the, "", auto). If auto is chosen, the consumer of your data should chose between "a" or "an". Default is "" (blank).""",
|
|
522
|
+
"type": "enum",
|
|
523
|
+
"enums": ("a", "an", "the", "", "auto"),
|
|
524
|
+
},
|
|
525
|
+
"og:locale": {
|
|
526
|
+
"required": False,
|
|
527
|
+
"description": """The locale these tags are marked up in. Of the format language_TERRITORY. Default is en_US.""",
|
|
528
|
+
"type": "string",
|
|
529
|
+
},
|
|
530
|
+
"og:locale:alternate": {
|
|
531
|
+
"required": False,
|
|
532
|
+
"description": """An array of other locales this page is available in..""",
|
|
533
|
+
"type": "string",
|
|
534
|
+
"array_allowed": True,
|
|
535
|
+
},
|
|
536
|
+
"og:video": {
|
|
537
|
+
"required": False,
|
|
538
|
+
"description": "A URL to a video file that complements this object. set content to url of video file. You may specify more than one og:video. If you specify more than one og:video, then og:video:type is required for each video. You must include a valid og:image for your video to be displayed in the news feed.",
|
|
539
|
+
"properties": {
|
|
540
|
+
"og:video:secure_url": {
|
|
541
|
+
"description": " An alternate url to use if the webpage requires HTTPS.",
|
|
542
|
+
"type": "url",
|
|
543
|
+
},
|
|
544
|
+
"og:video:type": {
|
|
545
|
+
"description": "A MIME type for this video.",
|
|
546
|
+
"type": "string",
|
|
547
|
+
},
|
|
548
|
+
"og:video:width": {
|
|
549
|
+
"description": "The number of pixels wide.",
|
|
550
|
+
"type": "integer",
|
|
551
|
+
},
|
|
552
|
+
"og:video:height": {
|
|
553
|
+
"description": "The number of pixels high.",
|
|
554
|
+
"type": "integer",
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
},
|
|
558
|
+
}
|
|
559
|
+
facebook_extensions = {
|
|
560
|
+
"fb:admins": {
|
|
561
|
+
"required": False,
|
|
562
|
+
"description": 'To associate the page with your Facebook account, add the additional property fb:admins to your page with a comma-separated list of the user IDs or usernames of the Facebook accounts who own the page, e.g.: <meta property="fb:admins" content="USER_ID1,USER_ID2"/>',
|
|
563
|
+
},
|
|
564
|
+
"fb:app_id": {
|
|
565
|
+
"required": False,
|
|
566
|
+
"description": "A Facebook Platform application ID that administers this page.",
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
# deprecated
|
|
571
|
+
og_properties = OG_PROPERTIES
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class OGErrors(dict):
|
|
575
|
+
def __init__(self) -> None:
|
|
576
|
+
"""Initialize the validation result buckets."""
|
|
577
|
+
self["critical"] = {}
|
|
578
|
+
self["recommended"] = {}
|
|
579
|
+
self["not_validated"] = []
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def validate_item(
|
|
583
|
+
info_dict: typing.Dict[str, typing.Any],
|
|
584
|
+
value: typing.Any,
|
|
585
|
+
) -> bool:
|
|
586
|
+
"""Check one metadata value against its declared type."""
|
|
587
|
+
if info_dict["type"] == "string":
|
|
588
|
+
if isinstance(value, str):
|
|
589
|
+
return True
|
|
590
|
+
return False
|
|
591
|
+
|
|
592
|
+
elif info_dict["type"] == "boolean":
|
|
593
|
+
try:
|
|
594
|
+
_success = (0, 1, "true", "false").index(value) # noqa: F841
|
|
595
|
+
return True
|
|
596
|
+
except ValueError:
|
|
597
|
+
return False
|
|
598
|
+
|
|
599
|
+
elif info_dict["type"] == "enum":
|
|
600
|
+
try:
|
|
601
|
+
_success = info_dict["enums"].index(value) # noqa: F841
|
|
602
|
+
return True
|
|
603
|
+
except ValueError:
|
|
604
|
+
return False
|
|
605
|
+
|
|
606
|
+
elif info_dict["type"] == "integer":
|
|
607
|
+
try:
|
|
608
|
+
if isinstance(value, int):
|
|
609
|
+
return True
|
|
610
|
+
else:
|
|
611
|
+
# coercing an int(value) into a string should catch utf8&string types, and fail on floats
|
|
612
|
+
if value == "%s" % int(value):
|
|
613
|
+
return True
|
|
614
|
+
except (TypeError, ValueError, OverflowError):
|
|
615
|
+
return False
|
|
616
|
+
|
|
617
|
+
elif info_dict["type"] == "datetime":
|
|
618
|
+
if isinstance(value, (datetime.date, datetime.datetime)):
|
|
619
|
+
return True
|
|
620
|
+
if isinstance(value, str):
|
|
621
|
+
for test in regex_dates:
|
|
622
|
+
if re.match(regex_dates[test], value):
|
|
623
|
+
return True
|
|
624
|
+
return False
|
|
625
|
+
|
|
626
|
+
elif info_dict["type"] == "url":
|
|
627
|
+
return isinstance(value, str) and regex_url.match(value) is not None
|
|
628
|
+
|
|
629
|
+
elif info_dict["type"] == "profile":
|
|
630
|
+
# TODO
|
|
631
|
+
# this involves looking for other fields.
|
|
632
|
+
return True
|
|
633
|
+
|
|
634
|
+
return False
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _validate_values(info: typing.Dict[str, typing.Any], value: typing.Any) -> bool:
|
|
638
|
+
"""Validate every entry in a repeated metadata property."""
|
|
639
|
+
if isinstance(value, list):
|
|
640
|
+
return bool(value) and all(validate_item(info, entry) for entry in value)
|
|
641
|
+
return validate_item(info, value)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def stringify(value: typing.Any) -> str:
|
|
645
|
+
"""turns a value into a string if needed"""
|
|
646
|
+
if isinstance(value, bool):
|
|
647
|
+
if value:
|
|
648
|
+
return "true"
|
|
649
|
+
return "false"
|
|
650
|
+
elif isinstance(value, datetime.datetime):
|
|
651
|
+
return value.isoformat()
|
|
652
|
+
elif isinstance(value, datetime.date):
|
|
653
|
+
return value.isoformat()
|
|
654
|
+
return str(value)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
class OpenGraphItem(object):
|
|
658
|
+
_data: _OG_DATA = {}
|
|
659
|
+
_errors: typing.Optional[OGErrors] = None
|
|
660
|
+
|
|
661
|
+
def __init__(
|
|
662
|
+
self,
|
|
663
|
+
sets: typing.Optional[_OG_KV_MANY] = None,
|
|
664
|
+
) -> None:
|
|
665
|
+
self._data = {}
|
|
666
|
+
if sets:
|
|
667
|
+
self.set_many(sets)
|
|
668
|
+
|
|
669
|
+
def set_many(
|
|
670
|
+
self,
|
|
671
|
+
pairs: _OG_KV_MANY,
|
|
672
|
+
) -> None:
|
|
673
|
+
for pair in pairs:
|
|
674
|
+
f, v = pair
|
|
675
|
+
self.set(f, v)
|
|
676
|
+
|
|
677
|
+
def set(
|
|
678
|
+
self,
|
|
679
|
+
field: str,
|
|
680
|
+
value: typing.Any,
|
|
681
|
+
append: bool = False,
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Replace a property value or append another value in order."""
|
|
684
|
+
if not append:
|
|
685
|
+
self._data[field] = value
|
|
686
|
+
else:
|
|
687
|
+
if field not in self._data:
|
|
688
|
+
self._data[field] = []
|
|
689
|
+
else:
|
|
690
|
+
if not isinstance(self._data[field], list):
|
|
691
|
+
self._data[field] = [self._data[field]]
|
|
692
|
+
self._data[field].append(value)
|
|
693
|
+
|
|
694
|
+
def validate(
|
|
695
|
+
self,
|
|
696
|
+
facebook: bool = False,
|
|
697
|
+
schema1: bool = False,
|
|
698
|
+
schema2: bool = True,
|
|
699
|
+
) -> bool:
|
|
700
|
+
"""
|
|
701
|
+
Validate the object
|
|
702
|
+
|
|
703
|
+
:param facebook: validate against the facebook extensions?
|
|
704
|
+
:type resp: bool
|
|
705
|
+
:param schema1: validate against schema1? Default: `False`.
|
|
706
|
+
This is likely the schema before opengraph was open sourced.
|
|
707
|
+
It is very basic, and only checks for a supported "og:type"
|
|
708
|
+
:type resp: bool
|
|
709
|
+
:param schema2: validate against schema2? Default: `True`
|
|
710
|
+
This appears to be the current opengraph protocol.
|
|
711
|
+
This will process both the og:type and the subtypes.
|
|
712
|
+
:type resp: bool
|
|
713
|
+
|
|
714
|
+
:rtype: bool
|
|
715
|
+
"""
|
|
716
|
+
if facebook is True:
|
|
717
|
+
raise ValueError("Support for Facebook Extensions is not built yet")
|
|
718
|
+
if schema1 and schema2:
|
|
719
|
+
raise ValueError("Validate against either schema1 or schema2")
|
|
720
|
+
|
|
721
|
+
errors = OGErrors()
|
|
722
|
+
|
|
723
|
+
def _set_error(
|
|
724
|
+
level: str,
|
|
725
|
+
item: str,
|
|
726
|
+
message: str,
|
|
727
|
+
) -> None:
|
|
728
|
+
# if level not in errors:
|
|
729
|
+
# errors[level]= {}
|
|
730
|
+
errors[level][item] = message
|
|
731
|
+
|
|
732
|
+
validate_fields = set(self._data.keys())
|
|
733
|
+
|
|
734
|
+
# make sure that everything in OG_PROPERTIES:
|
|
735
|
+
# 1) is present if required
|
|
736
|
+
# 2) is valid if not required
|
|
737
|
+
# 3) is valid if not required
|
|
738
|
+
for i in OG_PROPERTIES:
|
|
739
|
+
if OG_PROPERTIES[i]["required"]:
|
|
740
|
+
if i not in self._data:
|
|
741
|
+
_set_error("critical", i, "Missing Required Element")
|
|
742
|
+
else:
|
|
743
|
+
# required; check validity
|
|
744
|
+
validate_fields.remove(i)
|
|
745
|
+
if not (validate_item if i == "og:type" else _validate_values)(
|
|
746
|
+
OG_PROPERTIES[i], self._data[i]
|
|
747
|
+
):
|
|
748
|
+
_set_error("critical", i, "Required Element does not validate")
|
|
749
|
+
else:
|
|
750
|
+
# not required; check validity
|
|
751
|
+
if i in self._data:
|
|
752
|
+
validate_fields.remove(i)
|
|
753
|
+
if not _validate_values(OG_PROPERTIES[i], self._data[i]):
|
|
754
|
+
_set_error(
|
|
755
|
+
"recommended", i, "non-required Element does not validate"
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
og_type = self._data.get("og:type")
|
|
759
|
+
|
|
760
|
+
if schema1:
|
|
761
|
+
# schema1 only checks for validity of the valid type
|
|
762
|
+
if not og_type:
|
|
763
|
+
_set_error("critical", "og:type", "Missing og:type")
|
|
764
|
+
else:
|
|
765
|
+
if (
|
|
766
|
+
not isinstance(og_type, str)
|
|
767
|
+
or og_type not in OG_PROPERTIES["og:type"]["valid_types-1"]
|
|
768
|
+
):
|
|
769
|
+
_set_error("critical", "og:type", "Invalid og:type")
|
|
770
|
+
|
|
771
|
+
if schema2:
|
|
772
|
+
if not og_type:
|
|
773
|
+
_set_error("critical", "og:type", "Missing og:type")
|
|
774
|
+
else:
|
|
775
|
+
if (
|
|
776
|
+
not isinstance(og_type, str)
|
|
777
|
+
or og_type not in OG_PROPERTIES["og:type"]["valid_types-2"]
|
|
778
|
+
):
|
|
779
|
+
_set_error("critical", "og:type", "Invalid og:type")
|
|
780
|
+
else:
|
|
781
|
+
# validate all the likely subtypes
|
|
782
|
+
og_type_dict = OG_PROPERTIES["og:type"]["valid_types-2"][og_type]
|
|
783
|
+
for subtype in OG_PROPERTIES["og:type"]["valid_types-2"][og_type][
|
|
784
|
+
"properties"
|
|
785
|
+
]:
|
|
786
|
+
subtype_dict = og_type_dict["properties"][subtype]
|
|
787
|
+
value = None
|
|
788
|
+
if subtype in self._data:
|
|
789
|
+
value = self._data[subtype]
|
|
790
|
+
if "required" in og_type_dict and og_type_dict["required"]:
|
|
791
|
+
if subtype not in self._data:
|
|
792
|
+
_set_error(
|
|
793
|
+
"critical",
|
|
794
|
+
"%s" % subtype,
|
|
795
|
+
"Missing required subtype",
|
|
796
|
+
)
|
|
797
|
+
else:
|
|
798
|
+
validate_fields.discard(subtype)
|
|
799
|
+
if not _validate_values(subtype_dict, value):
|
|
800
|
+
_set_error(
|
|
801
|
+
"critical",
|
|
802
|
+
"%s" % subtype,
|
|
803
|
+
"Required subtype does not validate correctly",
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
if subtype not in self._data:
|
|
807
|
+
_set_error(
|
|
808
|
+
"recommended",
|
|
809
|
+
"%s" % subtype,
|
|
810
|
+
"non-required subtype not included",
|
|
811
|
+
)
|
|
812
|
+
else:
|
|
813
|
+
validate_fields.discard(subtype)
|
|
814
|
+
if not _validate_values(subtype_dict, value):
|
|
815
|
+
_set_error(
|
|
816
|
+
"recommended",
|
|
817
|
+
"%s" % subtype,
|
|
818
|
+
"non-required subtype does not validate correctly",
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
if validate_fields:
|
|
822
|
+
errors["not_validated"] = list(validate_fields)
|
|
823
|
+
self._errors = errors
|
|
824
|
+
if errors["critical"]:
|
|
825
|
+
return False
|
|
826
|
+
return True
|
|
827
|
+
|
|
828
|
+
def errors(self) -> OGErrors:
|
|
829
|
+
if self._errors is None:
|
|
830
|
+
raise ValueError("You must call `.validate()` first")
|
|
831
|
+
return self._errors
|
|
832
|
+
|
|
833
|
+
def as_html(
|
|
834
|
+
self,
|
|
835
|
+
debug: bool = False,
|
|
836
|
+
) -> str:
|
|
837
|
+
_output = []
|
|
838
|
+
_keys = sorted(self._data.keys())
|
|
839
|
+
if self._errors is None:
|
|
840
|
+
return ""
|
|
841
|
+
for k in _keys:
|
|
842
|
+
v = self._data[k]
|
|
843
|
+
_error = ""
|
|
844
|
+
if debug:
|
|
845
|
+
if k in self._errors["critical"]:
|
|
846
|
+
_error = ' critical-error="%s"' % html_attribute_escape(
|
|
847
|
+
self._errors["critical"][k]
|
|
848
|
+
)
|
|
849
|
+
elif k in self._errors["recommended"]:
|
|
850
|
+
_error = ' recommended-error="%s"' % html_attribute_escape(
|
|
851
|
+
self._errors["recommended"][k]
|
|
852
|
+
)
|
|
853
|
+
if isinstance(v, list):
|
|
854
|
+
for i in v:
|
|
855
|
+
i = stringify(i)
|
|
856
|
+
_output.append(
|
|
857
|
+
"""<meta property="%s" content="%s"%s/>"""
|
|
858
|
+
% (html_attribute_escape(k), html_attribute_escape(i), _error)
|
|
859
|
+
)
|
|
860
|
+
else:
|
|
861
|
+
v = stringify(v)
|
|
862
|
+
_output.append(
|
|
863
|
+
"""<meta property="%s" content="%s"%s/>"""
|
|
864
|
+
% (html_attribute_escape(k), html_attribute_escape(v), _error)
|
|
865
|
+
)
|
|
866
|
+
output = "\n".join(_output)
|
|
867
|
+
return output
|
|
File without changes
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# stdlib
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
# local
|
|
5
|
+
from . import OpenGraphItem
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from pyramid.config import Configurator
|
|
9
|
+
from pyramid.request import Request
|
|
10
|
+
|
|
11
|
+
# ==============================================================================
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def new_OpenGraphItem(request: "Request") -> OpenGraphItem:
|
|
15
|
+
"""simply creates a new hub"""
|
|
16
|
+
return OpenGraphItem()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def includeme(config: "Configurator") -> None:
|
|
20
|
+
"""
|
|
21
|
+
the pyramid includeme command
|
|
22
|
+
including this will automatically setup the OpenGraphItem object
|
|
23
|
+
for every request
|
|
24
|
+
"""
|
|
25
|
+
config.add_request_method(new_OpenGraphItem, "opengraph_item", reify=True)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: opengraph_writer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Lightweight OpenGraph support for writing and validating objects.
|
|
5
|
+
Home-page: https://github.com/jvanasco/opengraph_writer
|
|
6
|
+
Author: Jonathan Vanasco
|
|
7
|
+
Author-email: jvanasco@2xlp.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: facebook opengraph open graph web pyramid
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Framework :: Pyramid
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
23
|
+
Requires-Python: >=3.6
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE.txt
|
|
26
|
+
Requires-Dist: metadata_utils>=0.2.0
|
|
27
|
+
Requires-Dist: setuptools<82; python_version >= "3.7" and python_version < "3.10"
|
|
28
|
+
Provides-Extra: testing
|
|
29
|
+
Requires-Dist: pytest; extra == "testing"
|
|
30
|
+
Requires-Dist: pyramid; extra == "testing"
|
|
31
|
+
Requires-Dist: setuptools<82; extra == "testing"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
opengraph_writer
|
|
35
|
+
================
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
About
|
|
40
|
+
=====
|
|
41
|
+
|
|
42
|
+
This library was created to both manage writing OpenGraph data, and perform
|
|
43
|
+
lightweight verifications for offline debugging.
|
|
44
|
+
|
|
45
|
+
This library does the following:
|
|
46
|
+
|
|
47
|
+
- CREATE an object to stash OGP data
|
|
48
|
+
- VALIDATE the data, offering debug information on the object
|
|
49
|
+
- RENDER the object as html, and optionally include debug data on incorrect elements
|
|
50
|
+
|
|
51
|
+
Each `OpenGraphItem` represents one object. A property can have multiple
|
|
52
|
+
values, such as several images for the same page.
|
|
53
|
+
|
|
54
|
+
Most of the validation routines were derived from regexing the `https://ogp.me`
|
|
55
|
+
site into a Python dict, and researching what they are supposed to do.
|
|
56
|
+
The validation routes themselves are super trivial.
|
|
57
|
+
|
|
58
|
+
At the time of initially writing this library, and several checks ever since, the
|
|
59
|
+
specification on the `https://ogp.me` seems out of date.
|
|
60
|
+
|
|
61
|
+
In practice, Facebook's "premier" partners seem to all be using URLs which point
|
|
62
|
+
to certain data , however there aren't any guidelines for this within the
|
|
63
|
+
public OGP spec.
|
|
64
|
+
|
|
65
|
+
An example would be on the Spotify integration: when looking at a `SONG`, the
|
|
66
|
+
`ARTIST` and `ALBUM` refer to URLs which contain the relevant data.
|
|
67
|
+
|
|
68
|
+
Documentation
|
|
69
|
+
=============
|
|
70
|
+
|
|
71
|
+
The Python file contains extensive inline documentation.
|
|
72
|
+
|
|
73
|
+
### Multiple images
|
|
74
|
+
|
|
75
|
+
Use `append=True` to add another value to the same property. Each image is
|
|
76
|
+
validated individually, and the rendered tags keep the order in which the
|
|
77
|
+
images were added.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from opengraph_writer import OpenGraphItem
|
|
81
|
+
|
|
82
|
+
item = OpenGraphItem(
|
|
83
|
+
[
|
|
84
|
+
("og:title", "Example page"),
|
|
85
|
+
("og:type", "website"),
|
|
86
|
+
("og:url", "https://example.com/page"),
|
|
87
|
+
("og:image", "https://example.com/cover.jpg"),
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
item.set("og:image", "https://example.com/detail.jpg", append=True)
|
|
91
|
+
|
|
92
|
+
if item.validate():
|
|
93
|
+
print(item.as_html())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
Contributions Welcome!
|
|
98
|
+
======================
|
|
99
|
+
|
|
100
|
+
The test matrix covers CPython 3.6 through 3.15. Run the tests for your
|
|
101
|
+
active Python interpreter with `tox -e py`. The run writes its HTML coverage
|
|
102
|
+
report to `htmlcov/index.html`.
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
Framework Support: Pyramid
|
|
106
|
+
==========================
|
|
107
|
+
|
|
108
|
+
This package offers an includeme for Pyramid to set up an `opengraph_item`
|
|
109
|
+
as a `@reify` request method.
|
|
110
|
+
|
|
111
|
+
Simply update your application:
|
|
112
|
+
|
|
113
|
+
config.include("opengraph_writer.pyramid_helpers")
|
|
114
|
+
|
|
115
|
+
And your `Request` objects will be extended as such:
|
|
116
|
+
|
|
117
|
+
request.opengraph_item
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
opengraph_writer/__init__.py,sha256=1vbpgEFGFGaNKSZPPl-J5WX2wAtL-HbmTA6cTpU2Uzw,35215
|
|
2
|
+
opengraph_writer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
opengraph_writer/pyramid_helpers.py,sha256=EuAps9Gud6Cy4O4Z7_zWSIZVHuUPZaZY_HUFEPWCkUo,657
|
|
4
|
+
opengraph_writer-1.0.0.dist-info/licenses/LICENSE.txt,sha256=Trd9rbku6NQ62nWv31FZpUwHI8ccq48Rfzl9NF05SuU,1148
|
|
5
|
+
opengraph_writer-1.0.0.dist-info/METADATA,sha256=ME5PC5cBvqOAWSuF6mtMM4O4coro6QGgF57yrTz9xBE,3854
|
|
6
|
+
opengraph_writer-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
7
|
+
opengraph_writer-1.0.0.dist-info/top_level.txt,sha256=sRNxh_Br997xv6u202wF_lTIPKdjcYz1_aR6lqJI8Jw,17
|
|
8
|
+
opengraph_writer-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2012, Jonathan Vanasco <jonathan@findmeon.com>
|
|
2
|
+
|
|
3
|
+
MIT License -- http://www.opensource.org/licenses/mit-license
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
opengraph_writer
|