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 @@
|
|
|
1
|
+
libravatar
|