diff --git a/.gitignore b/.gitignore index 1b0dfad7..748a679d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ *pyc .DS_Store -.vedo_recorded_events.log -.vedo_pipeline_graphviz* docs/examples_db.js docs/pdoc/html diff --git a/README.md b/README.md index b70c0029..3f2c3e75 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![lics](https://img.shields.io/badge/license-MIT-blue.svg)](https://en.wikipedia.org/wiki/MIT_License) [![Anaconda-Server Badge](https://anaconda.org/conda-forge/vedo/badges/version.svg)](https://anaconda.org/conda-forge/vedo) [![Ubuntu 23.10 package](https://repology.org/badge/version-for-repo/ubuntu_23_10/vedo.svg)](https://repology.org/project/vedo/versions) -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7734756.svg)](https://doi.org/10.5281/zenodo.7734756) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4587871.svg)](https://doi.org/10.5281/zenodo.4587871) [![Downloads](https://static.pepy.tech/badge/vedo)](https://pepy.tech/project/vedo) [![CircleCI](https://circleci.com/gh/marcomusy/vedo.svg?style=svg)](https://circleci.com/gh/marcomusy/vedo) A lightweight and powerful python module diff --git a/vedo/cli.py b/vedo/cli.py index ddb1ade7..bf301988 100644 --- a/vedo/cli.py +++ b/vedo/cli.py @@ -436,7 +436,7 @@ def exe_search_vtk(args): xref_url = "https://raw.githubusercontent.com/Kitware/vtk-examples/gh-pages/src/Coverage/vtk_vtk-examples_xref.json" - def download_file(dl_path, dl_url, overwrite=False): + def _download_file(dl_path, dl_url, overwrite=False): file_name = dl_url.split("/")[-1] # Create necessary sub-directories in the dl_path (if they don't exist). Path(dl_path).mkdir(parents=True, exist_ok=True) @@ -449,7 +449,7 @@ def download_file(dl_path, dl_url, overwrite=False): raise RuntimeError(f"Failed to download {dl_url}. {e.reason}") return path - def get_examples(d, vtk_class, lang): + def _get_examples(d, vtk_class, lang): try: kv = d[vtk_class][lang].items() except KeyError as e: @@ -461,18 +461,18 @@ def get_examples(d, vtk_class, lang): vtk_class, language, all_values, number = args.search_vtk, "Python", True, 10000 tmp_dir = tempfile.gettempdir() - path = download_file(tmp_dir, xref_url, overwrite=False) + path = _download_file(tmp_dir, xref_url, overwrite=False) if not path.is_file(): print(f"The path: {str(path)} does not exist.") dt = datetime.today().timestamp() - os.path.getmtime(path) # Force a new download if the time difference is > 10 minutes. if dt > 600: - path = download_file(tmp_dir, xref_url, overwrite=True) + path = _download_file(tmp_dir, xref_url, overwrite=True) with open(path, "r", encoding="UTF-8") as json_file: xref_dict = json.load(json_file) - total_number, examples = get_examples(xref_dict, vtk_class, language) + total_number, examples = _get_examples(xref_dict, vtk_class, language) if examples: if total_number <= number or all_values: print( diff --git a/vedo/file_io.py b/vedo/file_io.py index 89686a48..7ad6e81c 100644 --- a/vedo/file_io.py +++ b/vedo/file_io.py @@ -441,54 +441,71 @@ def _load_file(filename, unpack): return actor ######################################################################## -def download(url, force=False, verbose=True): +def download(url, to_local_file="", force=False, verbose=True): """ - Retrieve a file from a URL, save it locally and return its path. - Use `force=True` to force a reload and discard cached copies. + Downloads a file from `url` to `to_local_file` if the local copy is outdated. + + Arguments: + url : (str) + The URL to download the file from. + to_local_file : (str) + The local file name to save the file to. + If not specified, the file name will be the same as the remote file name. + force : (bool) + Force a new download even if the local file is up to date. + verbose : (bool) + Print verbose messages. """ if not url.startswith("https://"): - # vedo.logger.error(f"Invalid URL (must start with https):\n{url}") - # assume it's a file so no need to download - return url - url = url.replace("www.dropbox", "dl.dropbox") + if os.path.exists(url): + # Assume the url is already the local file path + return url + else: + raise FileNotFoundError(f"File not found: {url}") - if "github.com" in url: - url = url.replace("/blob/", "/raw/") + from datetime import datetime + import requests - basename = os.path.basename(url) + # Get the user's home directory + home_directory = os.path.expanduser("~") - if "?" in basename: - basename = basename.split("?")[0] + # Define the path for the cache directory + cachedir = os.path.join(home_directory, settings.cache_directory, "vedo") - tmp_file = NamedTemporaryFile(delete=False) - tmp_file.name = os.path.join(os.path.dirname(tmp_file.name), os.path.basename(basename)) + # Create the directory if it does not exist + if not os.path.exists(cachedir): + os.makedirs(cachedir) - if not force and os.path.exists(tmp_file.name): - if verbose: - colors.printc("reusing cached file:", tmp_file.name) - # colors.printc(" (use force=True to force a new download)") - return tmp_file.name + if not to_local_file: + to_local_file = os.path.join(cachedir, os.path.basename(url)) + if verbose: print(f"Using local file name: {to_local_file}") + + # Check if the local file exists and get its last modified time + if os.path.exists(to_local_file): + to_local_file_modified_time = os.path.getmtime(to_local_file) + else: + to_local_file_modified_time = 0 + + # Send a HEAD request to get last modified time of the remote file + response = requests.head(url) + if 'Last-Modified' in response.headers: + remote_file_modified_time = datetime.strptime( + response.headers['Last-Modified'], '%a, %d %b %Y %H:%M:%S GMT' + ).timestamp() + else: + # If the Last-Modified header not available, assume file needs to be downloaded + remote_file_modified_time = float('inf') + + # Download the file if the remote file is newer + if force or remote_file_modified_time > to_local_file_modified_time: + response = requests.get(url) + with open(to_local_file, 'wb') as file: + file.write(response.content) + if verbose: print(f"Downloaded file from {url} -> {to_local_file}") + else: + if verbose: print("Local file is up to date.") + return to_local_file - try: - from urllib.request import urlopen, Request - req = Request(url, headers={"User-Agent": "Mozilla/5.0"}) - if verbose: - colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="") - - except ImportError: - import urllib2 - import contextlib - urlopen = lambda url_: contextlib.closing(urllib2.urlopen(url_)) - req = url - if verbose: - colors.printc("reading", basename, "from", url.split("/")[2][:40], "...", end="") - - with urlopen(req) as response, open(tmp_file.name, "wb") as output: - output.write(response.read()) - - if verbose: - colors.printc(" done.") - return tmp_file.name ######################################################################## def gunzip(filename): diff --git a/vedo/plotter.py b/vedo/plotter.py index 58ece2d6..23196032 100644 --- a/vedo/plotter.py +++ b/vedo/plotter.py @@ -1468,14 +1468,15 @@ def look_at(self, plane="xy"): vedo.logger.error(f"in plotter.look() cannot understand argument {plane}") return self - def record(self, filename=".vedo_recorded_events.log"): + def record(self, filename=""): """ Record camera, mouse, keystrokes and all other events. Recording can be toggled on/off by pressing key "R". Arguments: filename : (str) - ascii file to store events. The default is '.vedo_recorded_events.log'. + ascii file to store events. + The default is `settings.cache_directory+"vedo/recorded_events.log"`. Returns: a string descriptor of events. @@ -1490,6 +1491,13 @@ def record(self, filename=".vedo_recorded_events.log"): return self erec = vtk.new("InteractorEventRecorder") erec.SetInteractor(self.interactor) + if not filename: + if not os.path.exists(settings.cache_directory): + os.makedirs(settings.cache_directory) + home_dir = os.path.expanduser("~") + filename = os.path.join( + home_dir, settings.cache_directory, "vedo", "recorded_events.log") + print("Events will be recorded in", filename) erec.SetFileName(filename) erec.SetKeyPressActivationValue("R") erec.EnabledOn() @@ -1502,13 +1510,14 @@ def record(self, filename=".vedo_recorded_events.log"): erec = None return events - def play(self, events=".vedo_recorded_events.log", repeats=0): + def play(self, recorded_events="", repeats=0): """ Play camera, mouse, keystrokes and all other events. Arguments: events : (str) - file o string of events. The default is '.vedo_recorded_events.log'. + file o string of events. + The default is `settings.cache_directory+"vedo/recorded_events.log"`. repeats : (int) number of extra repeats of the same events. The default is 0. @@ -1524,12 +1533,17 @@ def play(self, events=".vedo_recorded_events.log", repeats=0): erec = vtk.new("InteractorEventRecorder") erec.SetInteractor(self.interactor) - if events.endswith(".log"): + if not recorded_events: + home_dir = os.path.expanduser("~") + recorded_events = os.path.join( + home_dir, settings.cache_directory, "vedo", "recorded_events.log") + + if recorded_events.endswith(".log"): erec.ReadFromInputStringOff() - erec.SetFileName(events) + erec.SetFileName(recorded_events) else: erec.ReadFromInputStringOn() - erec.SetInputString(events) + erec.SetInputString(recorded_events) erec.Play() for _ in range(repeats): diff --git a/vedo/settings.py b/vedo/settings.py index 3b1d25a0..4a880265 100644 --- a/vedo/settings.py +++ b/vedo/settings.py @@ -44,6 +44,9 @@ class Settings: # To run a demo try: # vedo --run fonts + # Use this local folder to store downloaded files (default is ~/.cache/vedo) + cache_directory = ".cache" + # Palette number when using an integer to choose a color palette = 0 @@ -165,11 +168,11 @@ class Settings: ``` """ - # Restrict the attributes so accidental typos will generate - # an AttributeError exception + # Restrict the attributes so accidental typos will generate an AttributeError exception __slots__ = [ "default_font", "default_backend", + "cache_directory", "palette", "remember_last_figure_format", "screenshot_transparent_background", @@ -253,6 +256,8 @@ def __init__(self): self.palette = 0 self.remember_last_figure_format = False + self.cache_directory = ".cache" # "/vedo" is added automatically + self.screenshot_transparent_background = False self.screeshot_large_image = False diff --git a/vedo/utils.py b/vedo/utils.py index 7070e379..f885b7f9 100644 --- a/vedo/utils.py +++ b/vedo/utils.py @@ -227,7 +227,12 @@ def show(self, orientation="LR", popup=True): self.counts = 0 self._build_tree(dot) self.dot = dot - dot.render(".vedo_pipeline_graphviz", view=popup) + + home_dir = os.path.expanduser("~") + gpath = os.path.join( + home_dir, vedo.settings.cache_directory, "vedo", "pipeline_graphviz") + + dot.render(gpath, view=popup) ########################################################################### diff --git a/vedo/version.py b/vedo/version.py index e7495b36..d2a2c8f6 100644 --- a/vedo/version.py +++ b/vedo/version.py @@ -1 +1 @@ -_version = '2023.5.0+dev1' +_version = '2023.5.0+dev2'