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.
@@ -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
+ ![Python package](https://github.com/jvanasco/opengraph_writer/workflows/Python%20package/badge.svg)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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