pyLibravatar 2.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.
libravatar.py ADDED
@@ -0,0 +1,281 @@
1
+ """
2
+ pyLibravatar - Python module for Libravatar.
3
+
4
+ Easy way to make use of the federated Libravatar.org avatar hosting service
5
+ from within your Python applications.
6
+
7
+ Copyright (C) 2011, 2013, 2015 Francois Marier <francois@libravatar.org>
8
+ Copyright (C) 2016-2026 Oliver Falk <oliver@linux-kernel.at>
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to
12
+ deal in the Software without restriction, including without limitation the
13
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
14
+ sell copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in
18
+ all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
26
+ IN THE SOFTWARE.
27
+ """
28
+
29
+ import hashlib
30
+ import random
31
+ import re
32
+ import urllib.parse
33
+ from typing import Any, Dict, List, Optional, Tuple, Union, cast
34
+
35
+ import dns.resolver
36
+ from dns.rdtypes.IN.SRV import SRV
37
+
38
+ BASE_URL = "http://cdn.libravatar.org/avatar/"
39
+ SECURE_BASE_URL = "https://seccdn.libravatar.org/avatar/"
40
+ SERVICE_BASE = "_avatars._tcp"
41
+ SECURE_SERVICE_BASE = "_avatars-sec._tcp"
42
+ MIN_AVATAR_SIZE = 1
43
+ MAX_AVATAR_SIZE = 512
44
+
45
+
46
+ def libravatar_url(
47
+ email: Optional[str] = None,
48
+ openid: Optional[str] = None,
49
+ https: bool = False,
50
+ default: Optional[Union[str, int]] = None,
51
+ size: Optional[Union[int, str]] = None,
52
+ ) -> str:
53
+ """Return a URL to the appropriate avatar."""
54
+ avatar_hash: Optional[str]
55
+ domain: Optional[str]
56
+ avatar_hash, domain = parse_user_identity(email, openid)
57
+ query_string = parse_options(default, size)
58
+
59
+ delegation_server = lookup_avatar_server(domain, https)
60
+ return compose_avatar_url(
61
+ delegation_server, avatar_hash, query_string, https
62
+ )
63
+
64
+
65
+ def parse_options(
66
+ default: Optional[Union[str, int]], size: Optional[Union[int, str]]
67
+ ) -> str:
68
+ """Turn optional parameters into a query string."""
69
+ query_string = ""
70
+ if default:
71
+ query_string = "?d=%s" % urllib.parse.quote_plus(str(default))
72
+ if size:
73
+ try:
74
+ size = int(size)
75
+ except ValueError:
76
+ return query_string # invalid size, skip
77
+
78
+ if len(query_string) > 0:
79
+ query_string += "&"
80
+ else:
81
+ query_string = "?"
82
+ query_string += "s=%s" % max(
83
+ MIN_AVATAR_SIZE, min(MAX_AVATAR_SIZE, size)
84
+ )
85
+
86
+ return query_string
87
+
88
+
89
+ def parse_user_identity(
90
+ email: Optional[str], openid: Optional[str]
91
+ ) -> Tuple[Optional[str], Optional[str]]:
92
+ """
93
+ Generate user hash based on the email address or OpenID.
94
+
95
+ The hash will be returned along with the relevant domain.
96
+ """
97
+ hash_obj = None
98
+ if email:
99
+ lowercase_value = email.strip().lower()
100
+ domain = lowercase_value.split("@")[-1]
101
+ hash_obj = hashlib.new("md5")
102
+ elif openid:
103
+ # pylint: disable=E1103
104
+ url = urllib.parse.urlsplit(openid.strip())
105
+ if url.username and url.hostname:
106
+ password = url.password or ""
107
+ netloc = url.username + ":" + password + "@" + url.hostname
108
+ else:
109
+ netloc = url.hostname or ""
110
+ lowercase_value = urllib.parse.urlunsplit(
111
+ (url.scheme.lower(), netloc, url.path, url.query, url.fragment)
112
+ )
113
+ domain = url.hostname or ""
114
+ hash_obj = hashlib.new("sha256")
115
+
116
+ if not hash_obj: # email and openid both missing
117
+ return (None, None)
118
+
119
+ hash_obj.update(lowercase_value.encode("utf-8"))
120
+ return (hash_obj.hexdigest(), domain)
121
+
122
+
123
+ def compose_avatar_url(
124
+ delegation_server: Optional[str],
125
+ avatar_hash: Optional[str],
126
+ query_string: Optional[str],
127
+ https: bool,
128
+ ) -> str:
129
+ """Assemble the final avatar URL based on the provided components."""
130
+ avatar_hash = avatar_hash or ""
131
+ query_string = query_string or ""
132
+
133
+ base_url = BASE_URL
134
+ if https:
135
+ base_url = SECURE_BASE_URL
136
+
137
+ if delegation_server:
138
+ if https:
139
+ base_url = "https://%s/avatar/" % delegation_server
140
+ else:
141
+ base_url = "http://%s/avatar/" % delegation_server
142
+
143
+ return base_url + avatar_hash + query_string
144
+
145
+
146
+ def service_name(domain: Optional[str], https: bool) -> Optional[str]:
147
+ """Return the DNS service to query for a given domain and scheme."""
148
+ if not domain:
149
+ return None
150
+
151
+ if https:
152
+ return "{}.{}".format(SECURE_SERVICE_BASE, domain)
153
+ else:
154
+ return "{}.{}".format(SERVICE_BASE, domain)
155
+
156
+
157
+ def lookup_avatar_server(domain: Optional[str], https: bool) -> Optional[str]:
158
+ """
159
+ Extract the avatar server from an SRV record in the DNS zone.
160
+
161
+ The SRV records should look like this:
162
+
163
+ _avatars._tcp.example.com. IN SRV 0 0 80 avatars.example.com
164
+ _avatars-sec._tcp.example.com. IN SRV 0 0 443 avatars.example.com
165
+ """
166
+ service = service_name(domain, https)
167
+ if not service:
168
+ return None
169
+ try:
170
+ answers = dns.resolver.resolve(service, "SRV")
171
+ except dns.resolver.NXDOMAIN:
172
+ return None
173
+ except dns.resolver.NoAnswer:
174
+ return None
175
+ except Exception as e:
176
+ print("DNS Error: %s" % e)
177
+ return None
178
+
179
+ records = []
180
+ for rdata in answers:
181
+ srv_rdata = cast(SRV, rdata)
182
+ srv_record = {
183
+ "priority": srv_rdata.priority,
184
+ "weight": srv_rdata.weight,
185
+ "port": srv_rdata.port,
186
+ "target": str(srv_rdata.target),
187
+ }
188
+
189
+ records.append(srv_record)
190
+
191
+ return normalized_target(records, https)
192
+
193
+
194
+ def normalized_target(
195
+ records: List[Dict[str, Any]], https: bool
196
+ ) -> Optional[str]:
197
+ """
198
+ Pick the right server to use and return its normalized hostname.
199
+
200
+ The hostname will be returned but the port number will be omitted
201
+ unless it's non-standard.
202
+ """
203
+ target, port = sanitize_target(srv_hostname(records))
204
+
205
+ if target and ((https and port != 443) or (not https and port != 80)):
206
+ return "{}:{}".format(target, port)
207
+
208
+ return target
209
+
210
+
211
+ def sanitize_target(
212
+ args: Tuple[Any, Any],
213
+ ) -> Tuple[Optional[str], Optional[int]]:
214
+ """Ensure we are getting a valid hostname and port from DNS resolver."""
215
+ target, port = args
216
+
217
+ if not target or not port:
218
+ return (None, None)
219
+
220
+ if not re.match("^[0-9a-zA-Z.-]+$", str(target)):
221
+ return (None, None)
222
+
223
+ try:
224
+ if int(port) < 1 or int(port) > 65535:
225
+ return (None, None)
226
+ except ValueError:
227
+ return (None, None)
228
+
229
+ return (target, port)
230
+
231
+
232
+ def srv_hostname(
233
+ records: List[Dict[str, Any]],
234
+ ) -> Tuple[Optional[str], Optional[int]]:
235
+ """Return the right (target, port) pair from a list of SRV records."""
236
+ if len(records) < 1:
237
+ return (None, None)
238
+
239
+ if 1 == len(records):
240
+ srv_record = records[0]
241
+ return (srv_record["target"], srv_record["port"])
242
+
243
+ # Keep only the servers in the top priority
244
+ priority_records: List[Tuple[int, Dict[str, Any]]] = []
245
+ total_weight = 0
246
+ top_priority = records[0]["priority"] # highest priority = lowest number
247
+
248
+ for srv_record in records:
249
+ if srv_record["priority"] > top_priority:
250
+ # ignore the record (srv_record has lower priority)
251
+ continue
252
+ elif srv_record["priority"] < top_priority:
253
+ # reset the array (srv_record has higher priority)
254
+ top_priority = srv_record["priority"]
255
+ total_weight = 0
256
+ priority_records = []
257
+
258
+ total_weight += srv_record["weight"]
259
+
260
+ if srv_record["weight"] > 0:
261
+ priority_records.append((total_weight, srv_record))
262
+ else:
263
+ # zero-weigth elements must come first
264
+ priority_records.insert(0, (0, srv_record))
265
+
266
+ if 1 == len(priority_records):
267
+ srv_record = priority_records[0][1]
268
+ return (srv_record["target"], srv_record["port"])
269
+
270
+ # Select first record according to RFC2782 weight
271
+ # ordering algorithm (page 3)
272
+ random_number = random.randint(0, total_weight)
273
+
274
+ for record in priority_records:
275
+ weighted_index, srv_record = record
276
+
277
+ if weighted_index >= random_number:
278
+ return (srv_record["target"], srv_record["port"])
279
+
280
+ print("There is something wrong with our SRV weight ordering algorithm")
281
+ return (None, None)
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyLibravatar
3
+ Version: 2.0.0
4
+ Summary: Python module for Libravatar
5
+ Author-email: Francois Marier <francois@libravatar.org>, Oliver Falk <oliver@linux-kernel.at>
6
+ Maintainer-email: Oliver Falk <oliver@linux-kernel.at>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/libravatar/pylibravatar
9
+ Project-URL: Repository, https://github.com/libravatar/pylibravatar
10
+ Keywords: libravatar,avatars,autonomous,social,federated
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: dnspython>=2.0.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: mypy; extra == "dev"
27
+ Requires-Dist: build; extra == "dev"
28
+ Requires-Dist: twine; extra == "dev"
29
+
30
+ # pyLibravatar
31
+
32
+ PyLibravatar is an easy way to make use of the federated [Libravatar](http://www.libravatar.org)
33
+ avatar hosting service from within your Python applications.
34
+
35
+ See the [project page](https://github.com/libravatar/pylibravatar) for the bug tracker and downloads.
36
+
37
+ ## Installation
38
+
39
+ To install using pip, simply do this:
40
+
41
+ $ pip install pylibravatar
42
+
43
+ ## Usage
44
+
45
+ To generate the correct avatar URL based on someone's email address, use the
46
+ following:
47
+
48
+ >>> from libravatar import libravatar_url
49
+ >>> url = libravatar_url(email='person@example.com')
50
+ >>> print(f'<img src="{url}">')
51
+
52
+ Here are other options you can provide:
53
+
54
+ >>> url = libravatar_url(openid = 'http://example.org/id/Bob', https = True, size = 96, default = 'mm')
55
+
56
+ See the [Libravatar documentation](http://wiki.libravatar.org/api) for more
57
+ information on the special values for the "default" parameter.
58
+
59
+ ## License
60
+
61
+ Copyright (C) 2011, 2013, 2015 Francois Marier <francois@libravatar.org>
62
+
63
+ Copyright (C) 2016-2026 Oliver Falk <oliver@linux-kernel.at>
64
+
65
+ Permission is hereby granted, free of charge, to any person obtaining a copy
66
+ of this software and associated documentation files (the "Software"), to
67
+ deal in the Software without restriction, including without limitation the
68
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
69
+ sell copies of the Software, and to permit persons to whom the Software is
70
+ furnished to do so, subject to the following conditions:
71
+
72
+ The above copyright notice and this permission notice shall be included in
73
+ all copies or substantial portions of the Software.
74
+
75
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
76
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
77
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
78
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
79
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
80
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
81
+ IN THE SOFTWARE.
@@ -0,0 +1,5 @@
1
+ libravatar.py,sha256=MwltzOoezlst4pLh3Cx8ljI_JrUq1_lsyttt20VMow4,8839
2
+ pylibravatar-2.0.0.dist-info/METADATA,sha256=lCFTcfR46eCBXsHfafCu3_UcPAE2q2R_9e0Pyhqktag,3311
3
+ pylibravatar-2.0.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
4
+ pylibravatar-2.0.0.dist-info/top_level.txt,sha256=OoJjXf12iIFTrrmHC0Cui8ylmQxLpu5l3t_eyR5Ge6E,11
5
+ pylibravatar-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ libravatar