comparison env/lib/python3.9/site-packages/boltons/ecoutils.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 # -*- coding: utf-8 -*-
2 """As a programming ecosystem grows, so do the chances of runtime
3 variability.
4
5 Python boasts one of the widest deployments for a high-level
6 programming environment, making it a viable target for all manner of
7 application. But with breadth comes variance, so it's important to
8 know what you're working with.
9
10 Some basic variations that are common among development machines:
11
12 * **Executable runtime**: CPython, PyPy, Jython, etc., plus build date and compiler
13 * **Language version**: 2.4, 2.5, 2.6, 2.7... 3.4, 3.5, 3.6
14 * **Host operating system**: Windows, OS X, Ubuntu, Debian, CentOS, RHEL, etc.
15 * **Features**: 64-bit, IPv6, Unicode character support (UCS-2/UCS-4)
16 * **Built-in library support**: OpenSSL, threading, SQLite, zlib
17 * **User environment**: umask, ulimit, working directory path
18 * **Machine info**: CPU count, hostname, filesystem encoding
19
20 See the full example profile below for more.
21
22 ecoutils was created to quantify that variability. ecoutils quickly
23 produces an information-dense description of critical runtime factors,
24 with minimal side effects. In short, ecoutils is like browser and user
25 agent analytics, but for Python environments.
26
27 Transmission and collection
28 ---------------------------
29
30 The data is all JSON serializable, and is suitable for sending to a
31 central analytics server. An HTTP-backed service for this can be found
32 at: https://github.com/mahmoud/espymetrics/
33
34 Notable omissions
35 -----------------
36
37 Due to space constraints (and possibly latency constraints), the
38 following information is deemed not dense enough, and thus omitted:
39
40 * :data:`sys.path`
41 * full :mod:`sysconfig`
42 * environment variables (:data:`os.environ`)
43
44 Compatibility
45 -------------
46
47 So far ecoutils has has been tested on Python 2.4, 2.5, 2.6, 2.7, 3.4,
48 3.5, and PyPy. Various versions have been tested on Ubuntu, Debian,
49 RHEL, OS X, FreeBSD, and Windows 7.
50
51 .. note:: Boltons typically only support back to Python 2.6, but due
52 to its nature, ecoutils extends backwards compatibility to Python
53 2.4 and 2.5.
54
55 Profile generation
56 ------------------
57
58 Profiles are generated by :func:`ecoutils.get_profile`.
59
60 When run as a module, ecoutils will call :func:`~ecoutils.get_profile`
61 and print a profile in JSON format::
62
63 $ python -m boltons.ecoutils
64 {
65 "_eco_version": "1.0.0",
66 "cpu_count": 4,
67 "cwd": "/home/mahmoud/projects/boltons",
68 "fs_encoding": "UTF-8",
69 "guid": "6b139e7bbf5ad4ed8d4063bf6235b4d2",
70 "hostfqdn": "mahmoud-host",
71 "hostname": "mahmoud-host",
72 "linux_dist_name": "Ubuntu",
73 "linux_dist_version": "14.04",
74 "python": {
75 "argv": "boltons/ecoutils.py",
76 "bin": "/usr/bin/python",
77 "build_date": "Jun 22 2015 17:58:13",
78 "compiler": "GCC 4.8.2",
79 "features": {
80 "64bit": true,
81 "expat": "expat_2.1.0",
82 "ipv6": true,
83 "openssl": "OpenSSL 1.0.1f 6 Jan 2014",
84 "readline": true,
85 "sqlite": "3.8.2",
86 "threading": true,
87 "tkinter": "8.6",
88 "unicode_wide": true,
89 "zlib": "1.2.8"
90 },
91 "version": "2.7.6 (default, Jun 22 2015, 17:58:13) [GCC 4.8.2]",
92 "version_info": [
93 2,
94 7,
95 6,
96 "final",
97 0
98 ]
99 },
100 "time_utc": "2016-05-24 07:59:40.473140",
101 "time_utc_offset": -8.0,
102 "ulimit_hard": 4096,
103 "ulimit_soft": 1024,
104 "umask": "002",
105 "uname": {
106 "machine": "x86_64",
107 "node": "mahmoud-host",
108 "processor": "x86_64",
109 "release": "3.13.0-85-generic",
110 "system": "Linux",
111 "version": "#129-Ubuntu SMP Thu Mar 17 20:50:15 UTC 2016"
112 },
113 "username": "mahmoud"
114 }
115
116 ``pip install boltons`` and try it yourself!
117
118 """
119
120 import re
121 import os
122 import sys
123 import time
124 import pprint
125 import random
126 import socket
127 import struct
128 import getpass
129 import datetime
130 import platform
131
132 ECO_VERSION = '1.0.1' # see version history below
133
134 PY_GT_2 = sys.version_info[0] > 2
135
136 try:
137 getrandbits = random.SystemRandom().getrandbits
138 HAVE_URANDOM = True
139 except Exception:
140 HAVE_URANDOM = False
141 getrandbits = random.getrandbits
142
143
144 # 128-bit GUID just like a UUID, but backwards compatible to 2.4
145 INSTANCE_ID = hex(getrandbits(128))[2:-1].lower()
146
147 IS_64BIT = struct.calcsize("P") > 4
148 HAVE_UCS4 = getattr(sys, 'maxunicode', 0) > 65536
149 HAVE_READLINE = True
150
151 try:
152 import readline
153 except Exception:
154 HAVE_READLINE = False
155
156 try:
157 import sqlite3
158 SQLITE_VERSION = sqlite3.sqlite_version
159 except Exception:
160 # note: 2.5 and older have sqlite, but not sqlite3
161 SQLITE_VERSION = ''
162
163
164 try:
165
166 import ssl
167 try:
168 OPENSSL_VERSION = ssl.OPENSSL_VERSION
169 except AttributeError:
170 # This is a conservative estimate for Python <2.6
171 # SSL module added in 2006, when 0.9.7 was standard
172 OPENSSL_VERSION = 'OpenSSL >0.8.0'
173 except Exception:
174 OPENSSL_VERSION = ''
175
176
177 try:
178 if PY_GT_2:
179 import tkinter
180 else:
181 import Tkinter as tkinter
182 TKINTER_VERSION = str(tkinter.TkVersion)
183 except Exception:
184 TKINTER_VERSION = ''
185
186
187 try:
188 import zlib
189 ZLIB_VERSION = zlib.ZLIB_VERSION
190 except Exception:
191 ZLIB_VERSION = ''
192
193
194 try:
195 from xml.parsers import expat
196 EXPAT_VERSION = expat.EXPAT_VERSION
197 except Exception:
198 EXPAT_VERSION = ''
199
200
201 try:
202 from multiprocessing import cpu_count
203 CPU_COUNT = cpu_count()
204 except Exception:
205 CPU_COUNT = 0
206
207 try:
208 import threading
209 HAVE_THREADING = True
210 except Exception:
211 HAVE_THREADING = False
212
213
214 try:
215 HAVE_IPV6 = socket.has_ipv6
216 except Exception:
217 HAVE_IPV6 = False
218
219
220 try:
221 from resource import getrlimit, RLIMIT_NOFILE
222 RLIMIT_FDS_SOFT, RLIMIT_FDS_HARD = getrlimit(RLIMIT_NOFILE)
223 except Exception:
224 RLIMIT_FDS_SOFT, RLIMIT_FDS_HARD = 0, 0
225
226
227 START_TIME_INFO = {'time_utc': str(datetime.datetime.utcnow()),
228 'time_utc_offset': -time.timezone / 3600.0}
229
230
231 def get_python_info():
232 ret = {}
233 ret['argv'] = _escape_shell_args(sys.argv)
234 ret['bin'] = sys.executable
235
236 # Even though compiler/build_date are already here, they're
237 # actually parsed from the version string. So, in the rare case of
238 # the unparsable version string, we're still transmitting it.
239 ret['version'] = ' '.join(sys.version.split())
240
241 ret['compiler'] = platform.python_compiler()
242 ret['build_date'] = platform.python_build()[1]
243 ret['version_info'] = list(sys.version_info)
244
245 ret['features'] = {'openssl': OPENSSL_VERSION,
246 'expat': EXPAT_VERSION,
247 'sqlite': SQLITE_VERSION,
248 'tkinter': TKINTER_VERSION,
249 'zlib': ZLIB_VERSION,
250 'unicode_wide': HAVE_UCS4,
251 'readline': HAVE_READLINE,
252 '64bit': IS_64BIT,
253 'ipv6': HAVE_IPV6,
254 'threading': HAVE_THREADING,
255 'urandom': HAVE_URANDOM}
256
257 return ret
258
259
260 def get_profile(**kwargs):
261 """The main entrypoint to ecoutils. Calling this will return a
262 JSON-serializable dictionary of information about the current
263 process.
264
265 It is very unlikely that the information returned will change
266 during the lifetime of the process, and in most cases the majority
267 of the information stays the same between runs as well.
268
269 :func:`get_profile` takes one optional keyword argument, *scrub*,
270 a :class:`bool` that, if True, blanks out identifiable
271 information. This includes current working directory, hostname,
272 Python executable path, command-line arguments, and
273 username. Values are replaced with '-', but for compatibility keys
274 remain in place.
275
276 """
277 scrub = kwargs.pop('scrub', False)
278 if kwargs:
279 raise TypeError('unexpected keyword arguments: %r' % (kwargs.keys(),))
280 ret = {}
281 try:
282 ret['username'] = getpass.getuser()
283 except Exception:
284 ret['username'] = ''
285 ret['guid'] = str(INSTANCE_ID)
286 ret['hostname'] = socket.gethostname()
287 ret['hostfqdn'] = socket.getfqdn()
288 uname = platform.uname()
289 ret['uname'] = {'system': uname[0],
290 'node': uname[1],
291 'release': uname[2], # linux: distro name
292 'version': uname[3], # linux: kernel version
293 'machine': uname[4],
294 'processor': uname[5]}
295 try:
296 linux_dist = platform.linux_distribution()
297 except Exception:
298 linux_dist = ('', '', '')
299 ret['linux_dist_name'] = linux_dist[0]
300 ret['linux_dist_version'] = linux_dist[1]
301 ret['cpu_count'] = CPU_COUNT
302
303 ret['fs_encoding'] = sys.getfilesystemencoding()
304 ret['ulimit_soft'] = RLIMIT_FDS_SOFT
305 ret['ulimit_hard'] = RLIMIT_FDS_HARD
306 ret['cwd'] = os.getcwd()
307 ret['umask'] = oct(os.umask(os.umask(2))).rjust(3, '0')
308
309 ret['python'] = get_python_info()
310 ret.update(START_TIME_INFO)
311 ret['_eco_version'] = ECO_VERSION
312
313 if scrub:
314 # mask identifiable information
315 ret['cwd'] = '-'
316 ret['hostname'] = '-'
317 ret['hostfqdn'] = '-'
318 ret['python']['bin'] = '-'
319 ret['python']['argv'] = '-'
320 ret['uname']['node'] = '-'
321 ret['username'] = '-'
322
323 return ret
324
325
326 _real_safe_repr = pprint._safe_repr
327
328
329 def _fake_json_dumps(val, indent=2):
330 # never do this. this is a hack for Python 2.4. Python 2.5 added
331 # the json module for a reason.
332 def _fake_safe_repr(*a, **kw):
333 res, is_read, is_rec = _real_safe_repr(*a, **kw)
334 if res == 'None':
335 res = 'null'
336 if res == 'True':
337 res = 'true'
338 if res == 'False':
339 res = 'false'
340 if not (res.startswith("'") or res.startswith("u'")):
341 res = res
342 else:
343 if res.startswith('u'):
344 res = res[1:]
345
346 contents = res[1:-1]
347 contents = contents.replace('"', '').replace(r'\"', '')
348 res = '"' + contents + '"'
349 return res, is_read, is_rec
350
351 pprint._safe_repr = _fake_safe_repr
352 try:
353 ret = pprint.pformat(val, indent=indent)
354 finally:
355 pprint._safe_repr = _real_safe_repr
356
357 return ret
358
359
360 def get_profile_json(indent=False):
361 if indent:
362 indent = 2
363 else:
364 indent = 0
365 try:
366 import json
367
368 def dumps(val, indent):
369 if indent:
370 return json.dumps(val, sort_keys=True, indent=indent)
371 return json.dumps(val, sort_keys=True)
372
373 except ImportError:
374 def dumps(val, indent):
375 ret = _fake_json_dumps(val, indent=indent)
376 if not indent:
377 ret = re.sub(r'\n\s*', ' ', ret)
378 return ret
379
380 data_dict = get_profile()
381 return dumps(data_dict, indent)
382
383
384 def main():
385 print(get_profile_json(indent=True))
386
387 #############################################
388 # The shell escaping copied in from strutils
389 #############################################
390
391
392 def _escape_shell_args(args, sep=' ', style=None):
393 if not style:
394 if sys.platform == 'win32':
395 style = 'cmd'
396 else:
397 style = 'sh'
398
399 if style == 'sh':
400 return _args2sh(args, sep=sep)
401 elif style == 'cmd':
402 return _args2cmd(args, sep=sep)
403
404 raise ValueError("style expected one of 'cmd' or 'sh', not %r" % style)
405
406
407 _find_sh_unsafe = re.compile(r'[^a-zA-Z0-9_@%+=:,./-]').search
408
409
410 def _args2sh(args, sep=' '):
411 # see strutils
412 ret_list = []
413
414 for arg in args:
415 if not arg:
416 ret_list.append("''")
417 continue
418 if _find_sh_unsafe(arg) is None:
419 ret_list.append(arg)
420 continue
421 # use single quotes, and put single quotes into double quotes
422 # the string $'b is then quoted as '$'"'"'b'
423 ret_list.append("'" + arg.replace("'", "'\"'\"'") + "'")
424
425 return ' '.join(ret_list)
426
427
428 def _args2cmd(args, sep=' '):
429 # see strutils
430 result = []
431 needquote = False
432 for arg in args:
433 bs_buf = []
434
435 # Add a space to separate this argument from the others
436 if result:
437 result.append(' ')
438
439 needquote = (" " in arg) or ("\t" in arg) or not arg
440 if needquote:
441 result.append('"')
442
443 for c in arg:
444 if c == '\\':
445 # Don't know if we need to double yet.
446 bs_buf.append(c)
447 elif c == '"':
448 # Double backslashes.
449 result.append('\\' * len(bs_buf)*2)
450 bs_buf = []
451 result.append('\\"')
452 else:
453 # Normal char
454 if bs_buf:
455 result.extend(bs_buf)
456 bs_buf = []
457 result.append(c)
458
459 # Add remaining backslashes, if any.
460 if bs_buf:
461 result.extend(bs_buf)
462
463 if needquote:
464 result.extend(bs_buf)
465 result.append('"')
466
467 return ''.join(result)
468
469
470 ############################
471 # End shell escaping code
472 ############################
473
474 if __name__ == '__main__':
475 main()
476
477
478 """
479
480 ecoutils protocol version history
481 ---------------------------------
482
483 The version is ECO_VERSION module-level constant, and _eco_version key
484 in the dictionary returned from ecoutils.get_profile().
485
486 1.0.1 - (boltons version 16.3.2+) Remove uuid dependency and add HAVE_URANDOM
487 1.0.0 - (boltons version 16.3.0-16.3.1) Initial release
488
489 """