comparison planemo/lib/python3.7/site-packages/virtualenv/seed/wheels/periodic_update.py @ 1:56ad4e20f292 draft

"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author guerler
date Fri, 31 Jul 2020 00:32:28 -0400
parents
children
comparison
equal deleted inserted replaced
0:d30785e31577 1:56ad4e20f292
1 """
2 Periodically update bundled versions.
3 """
4
5 from __future__ import absolute_import, unicode_literals
6
7 import json
8 import logging
9 import os
10 import ssl
11 import subprocess
12 import sys
13 from datetime import datetime, timedelta
14 from itertools import groupby
15 from shutil import copy2
16 from textwrap import dedent
17 from threading import Thread
18
19 from six.moves.urllib.error import URLError
20 from six.moves.urllib.request import urlopen
21
22 from virtualenv.app_data import AppDataDiskFolder
23 from virtualenv.info import PY2
24 from virtualenv.util.path import Path
25 from virtualenv.util.subprocess import DETACHED_PROCESS, Popen
26
27 from ..wheels.embed import BUNDLE_SUPPORT
28 from ..wheels.util import Wheel
29
30 if PY2:
31 # on Python 2 datetime.strptime throws the error below if the import did not trigger on main thread
32 # Failed to import _strptime because the import lock is held by
33 try:
34 import _strptime # noqa
35 except ImportError: # pragma: no cov
36 pass # pragma: no cov
37
38
39 def periodic_update(distribution, for_py_version, wheel, search_dirs, app_data, do_periodic_update):
40 if do_periodic_update:
41 handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data)
42
43 now = datetime.now()
44
45 u_log = UpdateLog.from_app_data(app_data, distribution, for_py_version)
46 u_log_older_than_hour = now - u_log.completed > timedelta(hours=1) if u_log.completed is not None else False
47 for _, group in groupby(u_log.versions, key=lambda v: v.wheel.version_tuple[0:2]):
48 version = next(group) # use only latest patch version per minor, earlier assumed to be buggy
49 if wheel is not None and Path(version.filename).name == wheel.name:
50 break
51 if u_log.periodic is False or (u_log_older_than_hour and version.use(now)):
52 updated_wheel = Wheel(app_data.house / version.filename)
53 logging.debug("using %supdated wheel %s", "periodically " if updated_wheel else "", updated_wheel)
54 wheel = updated_wheel
55 break
56
57 return wheel
58
59
60 def handle_auto_update(distribution, for_py_version, wheel, search_dirs, app_data):
61 embed_update_log = app_data.embed_update_log(distribution, for_py_version)
62 u_log = UpdateLog.from_dict(embed_update_log.read())
63 if u_log.needs_update:
64 u_log.periodic = True
65 u_log.started = datetime.now()
66 embed_update_log.write(u_log.to_dict())
67 trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, periodic=True)
68
69
70 DATETIME_FMT = "%Y-%m-%dT%H:%M:%S.%fZ"
71
72
73 def dump_datetime(value):
74 return None if value is None else value.strftime(DATETIME_FMT)
75
76
77 def load_datetime(value):
78 return None if value is None else datetime.strptime(value, DATETIME_FMT)
79
80
81 class NewVersion(object):
82 def __init__(self, filename, found_date, release_date):
83 self.filename = filename
84 self.found_date = found_date
85 self.release_date = release_date
86
87 @classmethod
88 def from_dict(cls, dictionary):
89 return cls(
90 filename=dictionary["filename"],
91 found_date=load_datetime(dictionary["found_date"]),
92 release_date=load_datetime(dictionary["release_date"]),
93 )
94
95 def to_dict(self):
96 return {
97 "filename": self.filename,
98 "release_date": dump_datetime(self.release_date),
99 "found_date": dump_datetime(self.found_date),
100 }
101
102 def use(self, now):
103 compare_from = self.release_date or self.found_date
104 return now - compare_from >= timedelta(days=28)
105
106 def __repr__(self):
107 return "{}(filename={}), found_date={}, release_date={})".format(
108 self.__class__.__name__, self.filename, self.found_date, self.release_date,
109 )
110
111 def __eq__(self, other):
112 return type(self) == type(other) and all(
113 getattr(self, k) == getattr(other, k) for k in ["filename", "release_date", "found_date"]
114 )
115
116 def __ne__(self, other):
117 return not (self == other)
118
119 @property
120 def wheel(self):
121 return Wheel(Path(self.filename))
122
123
124 class UpdateLog(object):
125 def __init__(self, started, completed, versions, periodic):
126 self.started = started
127 self.completed = completed
128 self.versions = versions
129 self.periodic = periodic
130
131 @classmethod
132 def from_dict(cls, dictionary):
133 if dictionary is None:
134 dictionary = {}
135 return cls(
136 load_datetime(dictionary.get("started")),
137 load_datetime(dictionary.get("completed")),
138 [NewVersion.from_dict(v) for v in dictionary.get("versions", [])],
139 dictionary.get("periodic"),
140 )
141
142 @classmethod
143 def from_app_data(cls, app_data, distribution, for_py_version):
144 raw_json = app_data.embed_update_log(distribution, for_py_version).read()
145 return cls.from_dict(raw_json)
146
147 def to_dict(self):
148 return {
149 "started": dump_datetime(self.started),
150 "completed": dump_datetime(self.completed),
151 "periodic": self.periodic,
152 "versions": [r.to_dict() for r in self.versions],
153 }
154
155 @property
156 def needs_update(self):
157 now = datetime.now()
158 if self.completed is None: # never completed
159 return self._check_start(now)
160 else:
161 if now - self.completed <= timedelta(days=14):
162 return False
163 return self._check_start(now)
164
165 def _check_start(self, now):
166 return self.started is None or now - self.started > timedelta(hours=1)
167
168
169 def trigger_update(distribution, for_py_version, wheel, search_dirs, app_data, periodic):
170 wheel_path = None if wheel is None else str(wheel.path)
171 cmd = [
172 sys.executable,
173 "-c",
174 dedent(
175 """
176 from virtualenv.report import setup_report, MAX_LEVEL
177 from virtualenv.seed.wheels.periodic_update import do_update
178 setup_report(MAX_LEVEL, show_pid=True)
179 do_update({!r}, {!r}, {!r}, {!r}, {!r}, {!r})
180 """,
181 )
182 .strip()
183 .format(distribution, for_py_version, wheel_path, str(app_data), [str(p) for p in search_dirs], periodic),
184 ]
185 debug = os.environ.get(str("_VIRTUALENV_PERIODIC_UPDATE_INLINE")) == str("1")
186 pipe = None if debug else subprocess.PIPE
187 kwargs = {"stdout": pipe, "stderr": pipe}
188 if not debug and sys.platform == "win32":
189 kwargs["creationflags"] = DETACHED_PROCESS
190 process = Popen(cmd, **kwargs)
191 logging.info(
192 "triggered periodic upgrade of %s%s (for python %s) via background process having PID %d",
193 distribution,
194 "" if wheel is None else "=={}".format(wheel.version),
195 for_py_version,
196 process.pid,
197 )
198 if debug:
199 process.communicate() # on purpose not called to make it a background process
200
201
202 def do_update(distribution, for_py_version, embed_filename, app_data, search_dirs, periodic):
203 versions = None
204 try:
205 versions = _run_do_update(app_data, distribution, embed_filename, for_py_version, periodic, search_dirs)
206 finally:
207 logging.debug("done %s %s with %s", distribution, for_py_version, versions)
208 return versions
209
210
211 def _run_do_update(app_data, distribution, embed_filename, for_py_version, periodic, search_dirs):
212 from virtualenv.seed.wheels import acquire
213
214 wheel_filename = None if embed_filename is None else Path(embed_filename)
215 embed_version = None if wheel_filename is None else Wheel(wheel_filename).version_tuple
216 app_data = AppDataDiskFolder(app_data) if isinstance(app_data, str) else app_data
217 search_dirs = [Path(p) if isinstance(p, str) else p for p in search_dirs]
218 wheelhouse = app_data.house
219 embed_update_log = app_data.embed_update_log(distribution, for_py_version)
220 u_log = UpdateLog.from_dict(embed_update_log.read())
221 now = datetime.now()
222 if wheel_filename is not None:
223 dest = wheelhouse / wheel_filename.name
224 if not dest.exists():
225 copy2(str(wheel_filename), str(wheelhouse))
226 last, last_version, versions = None, None, []
227 while last is None or not last.use(now):
228 download_time = datetime.now()
229 dest = acquire.download_wheel(
230 distribution=distribution,
231 version_spec=None if last_version is None else "<{}".format(last_version),
232 for_py_version=for_py_version,
233 search_dirs=search_dirs,
234 app_data=app_data,
235 to_folder=wheelhouse,
236 )
237 if dest is None or (u_log.versions and u_log.versions[0].filename == dest.name):
238 break
239 release_date = release_date_for_wheel_path(dest.path)
240 last = NewVersion(filename=dest.path.name, release_date=release_date, found_date=download_time)
241 logging.info("detected %s in %s", last, datetime.now() - download_time)
242 versions.append(last)
243 last_wheel = Wheel(Path(last.filename))
244 last_version = last_wheel.version
245 if embed_version is not None:
246 if embed_version >= last_wheel.version_tuple: # stop download if we reach the embed version
247 break
248 u_log.periodic = periodic
249 if not u_log.periodic:
250 u_log.started = now
251 u_log.versions = versions + u_log.versions
252 u_log.completed = datetime.now()
253 embed_update_log.write(u_log.to_dict())
254 return versions
255
256
257 def release_date_for_wheel_path(dest):
258 wheel = Wheel(dest)
259 # the most accurate is to ask PyPi - e.g. https://pypi.org/pypi/pip/json,
260 # see https://warehouse.pypa.io/api-reference/json/ for more details
261 content = _pypi_get_distribution_info_cached(wheel.distribution)
262 if content is not None:
263 try:
264 upload_time = content["releases"][wheel.version][0]["upload_time"]
265 return datetime.strptime(upload_time, "%Y-%m-%dT%H:%M:%S")
266 except Exception as exception:
267 logging.error("could not load release date %s because %r", content, exception)
268 return None
269
270
271 def _request_context():
272 yield None
273 # fallback to non verified HTTPS (the information we request is not sensitive, so fallback)
274 yield ssl._create_unverified_context() # noqa
275
276
277 _PYPI_CACHE = {}
278
279
280 def _pypi_get_distribution_info_cached(distribution):
281 if distribution not in _PYPI_CACHE:
282 _PYPI_CACHE[distribution] = _pypi_get_distribution_info(distribution)
283 return _PYPI_CACHE[distribution]
284
285
286 def _pypi_get_distribution_info(distribution):
287 content, url = None, "https://pypi.org/pypi/{}/json".format(distribution)
288 try:
289 for context in _request_context():
290 try:
291 with urlopen(url, context=context) as file_handler:
292 content = json.load(file_handler)
293 break
294 except URLError as exception:
295 logging.error("failed to access %s because %r", url, exception)
296 except Exception as exception:
297 logging.error("failed to access %s because %r", url, exception)
298 return content
299
300
301 def manual_upgrade(app_data):
302 threads = []
303
304 for for_py_version, distribution_to_package in BUNDLE_SUPPORT.items():
305 # load extra search dir for the given for_py
306 for distribution in distribution_to_package.keys():
307 thread = Thread(target=_run_manual_upgrade, args=(app_data, distribution, for_py_version))
308 thread.start()
309 threads.append(thread)
310
311 for thread in threads:
312 thread.join()
313
314
315 def _run_manual_upgrade(app_data, distribution, for_py_version):
316 start = datetime.now()
317 from .bundle import from_bundle
318
319 current = from_bundle(
320 distribution=distribution,
321 version=None,
322 for_py_version=for_py_version,
323 search_dirs=[],
324 app_data=app_data,
325 do_periodic_update=False,
326 )
327 logging.warning(
328 "upgrade %s for python %s with current %s",
329 distribution,
330 for_py_version,
331 "" if current is None else current.name,
332 )
333 versions = do_update(
334 distribution=distribution,
335 for_py_version=for_py_version,
336 embed_filename=current.path,
337 app_data=app_data,
338 search_dirs=[],
339 periodic=False,
340 )
341 msg = "upgraded %s for python %s in %s {}".format(
342 "new entries found:\n%s" if versions else "no new versions found",
343 )
344 args = [
345 distribution,
346 for_py_version,
347 datetime.now() - start,
348 ]
349 if versions:
350 args.append("\n".join("\t{}".format(v) for v in versions))
351 logging.warning(msg, *args)
352
353
354 __all__ = (
355 "periodic_update",
356 "do_update",
357 "manual_upgrade",
358 "NewVersion",
359 "UpdateLog",
360 "load_datetime",
361 "dump_datetime",
362 "trigger_update",
363 "release_date_for_wheel_path",
364 )