123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704 |
- """A file interface for handling local and remote data files.
- The goal of datasource is to abstract some of the file system operations
- when dealing with data files so the researcher doesn't have to know all the
- low-level details. Through datasource, a researcher can obtain and use a
- file with one function call, regardless of location of the file.
- DataSource is meant to augment standard python libraries, not replace them.
- It should work seamlessly with standard file IO operations and the os
- module.
- DataSource files can originate locally or remotely:
- - local files : '/home/guido/src/local/data.txt'
- - URLs (http, ftp, ...) : 'http://www.scipy.org/not/real/data.txt'
- DataSource files can also be compressed or uncompressed. Currently only
- gzip, bz2 and xz are supported.
- Example::
- >>> # Create a DataSource, use os.curdir (default) for local storage.
- >>> from numpy import DataSource
- >>> ds = DataSource()
- >>>
- >>> # Open a remote file.
- >>> # DataSource downloads the file, stores it locally in:
- >>> # './www.google.com/index.html'
- >>> # opens the file and returns a file object.
- >>> fp = ds.open('http://www.google.com/') # doctest: +SKIP
- >>>
- >>> # Use the file as you normally would
- >>> fp.read() # doctest: +SKIP
- >>> fp.close() # doctest: +SKIP
- """
- import os
- import io
- from .._utils import set_module
- _open = open
- def _check_mode(mode, encoding, newline):
- """Check mode and that encoding and newline are compatible.
- Parameters
- ----------
- mode : str
- File open mode.
- encoding : str
- File encoding.
- newline : str
- Newline for text files.
- """
- if "t" in mode:
- if "b" in mode:
- raise ValueError("Invalid mode: %r" % (mode,))
- else:
- if encoding is not None:
- raise ValueError("Argument 'encoding' not supported in binary mode")
- if newline is not None:
- raise ValueError("Argument 'newline' not supported in binary mode")
- # Using a class instead of a module-level dictionary
- # to reduce the initial 'import numpy' overhead by
- # deferring the import of lzma, bz2 and gzip until needed
- # TODO: .zip support, .tar support?
- class _FileOpeners:
- """
- Container for different methods to open (un-)compressed files.
- `_FileOpeners` contains a dictionary that holds one method for each
- supported file format. Attribute lookup is implemented in such a way
- that an instance of `_FileOpeners` itself can be indexed with the keys
- of that dictionary. Currently uncompressed files as well as files
- compressed with ``gzip``, ``bz2`` or ``xz`` compression are supported.
- Notes
- -----
- `_file_openers`, an instance of `_FileOpeners`, is made available for
- use in the `_datasource` module.
- Examples
- --------
- >>> import gzip
- >>> np.lib._datasource._file_openers.keys()
- [None, '.bz2', '.gz', '.xz', '.lzma']
- >>> np.lib._datasource._file_openers['.gz'] is gzip.open
- True
- """
- def __init__(self):
- self._loaded = False
- self._file_openers = {None: io.open}
- def _load(self):
- if self._loaded:
- return
- try:
- import bz2
- self._file_openers[".bz2"] = bz2.open
- except ImportError:
- pass
- try:
- import gzip
- self._file_openers[".gz"] = gzip.open
- except ImportError:
- pass
- try:
- import lzma
- self._file_openers[".xz"] = lzma.open
- self._file_openers[".lzma"] = lzma.open
- except (ImportError, AttributeError):
- # There are incompatible backports of lzma that do not have the
- # lzma.open attribute, so catch that as well as ImportError.
- pass
- self._loaded = True
- def keys(self):
- """
- Return the keys of currently supported file openers.
- Parameters
- ----------
- None
- Returns
- -------
- keys : list
- The keys are None for uncompressed files and the file extension
- strings (i.e. ``'.gz'``, ``'.xz'``) for supported compression
- methods.
- """
- self._load()
- return list(self._file_openers.keys())
- def __getitem__(self, key):
- self._load()
- return self._file_openers[key]
- _file_openers = _FileOpeners()
- def open(path, mode='r', destpath=os.curdir, encoding=None, newline=None):
- """
- Open `path` with `mode` and return the file object.
- If ``path`` is an URL, it will be downloaded, stored in the
- `DataSource` `destpath` directory and opened from there.
- Parameters
- ----------
- path : str
- Local file path or URL to open.
- mode : str, optional
- Mode to open `path`. Mode 'r' for reading, 'w' for writing, 'a' to
- append. Available modes depend on the type of object specified by
- path. Default is 'r'.
- destpath : str, optional
- Path to the directory where the source file gets downloaded to for
- use. If `destpath` is None, a temporary directory will be created.
- The default path is the current directory.
- encoding : {None, str}, optional
- Open text file with given encoding. The default encoding will be
- what `io.open` uses.
- newline : {None, str}, optional
- Newline to use when reading text file.
- Returns
- -------
- out : file object
- The opened file.
- Notes
- -----
- This is a convenience function that instantiates a `DataSource` and
- returns the file object from ``DataSource.open(path)``.
- """
- ds = DataSource(destpath)
- return ds.open(path, mode, encoding=encoding, newline=newline)
- @set_module('numpy')
- class DataSource:
- """
- DataSource(destpath='.')
- A generic data source file (file, http, ftp, ...).
- DataSources can be local files or remote files/URLs. The files may
- also be compressed or uncompressed. DataSource hides some of the
- low-level details of downloading the file, allowing you to simply pass
- in a valid file path (or URL) and obtain a file object.
- Parameters
- ----------
- destpath : str or None, optional
- Path to the directory where the source file gets downloaded to for
- use. If `destpath` is None, a temporary directory will be created.
- The default path is the current directory.
- Notes
- -----
- URLs require a scheme string (``http://``) to be used, without it they
- will fail::
- >>> repos = np.DataSource()
- >>> repos.exists('www.google.com/index.html')
- False
- >>> repos.exists('http://www.google.com/index.html')
- True
- Temporary directories are deleted when the DataSource is deleted.
- Examples
- --------
- ::
- >>> ds = np.DataSource('/home/guido')
- >>> urlname = 'http://www.google.com/'
- >>> gfile = ds.open('http://www.google.com/')
- >>> ds.abspath(urlname)
- '/home/guido/www.google.com/index.html'
- >>> ds = np.DataSource(None) # use with temporary file
- >>> ds.open('/home/guido/foobar.txt')
- <open file '/home/guido.foobar.txt', mode 'r' at 0x91d4430>
- >>> ds.abspath('/home/guido/foobar.txt')
- '/tmp/.../home/guido/foobar.txt'
- """
- def __init__(self, destpath=os.curdir):
- """Create a DataSource with a local path at destpath."""
- if destpath:
- self._destpath = os.path.abspath(destpath)
- self._istmpdest = False
- else:
- import tempfile # deferring import to improve startup time
- self._destpath = tempfile.mkdtemp()
- self._istmpdest = True
- def __del__(self):
- # Remove temp directories
- if hasattr(self, '_istmpdest') and self._istmpdest:
- import shutil
- shutil.rmtree(self._destpath)
- def _iszip(self, filename):
- """Test if the filename is a zip file by looking at the file extension.
- """
- fname, ext = os.path.splitext(filename)
- return ext in _file_openers.keys()
- def _iswritemode(self, mode):
- """Test if the given mode will open a file for writing."""
- # Currently only used to test the bz2 files.
- _writemodes = ("w", "+")
- for c in mode:
- if c in _writemodes:
- return True
- return False
- def _splitzipext(self, filename):
- """Split zip extension from filename and return filename.
- Returns
- -------
- base, zip_ext : {tuple}
- """
- if self._iszip(filename):
- return os.path.splitext(filename)
- else:
- return filename, None
- def _possible_names(self, filename):
- """Return a tuple containing compressed filename variations."""
- names = [filename]
- if not self._iszip(filename):
- for zipext in _file_openers.keys():
- if zipext:
- names.append(filename+zipext)
- return names
- def _isurl(self, path):
- """Test if path is a net location. Tests the scheme and netloc."""
- # We do this here to reduce the 'import numpy' initial import time.
- from urllib.parse import urlparse
- # BUG : URLs require a scheme string ('http://') to be used.
- # www.google.com will fail.
- # Should we prepend the scheme for those that don't have it and
- # test that also? Similar to the way we append .gz and test for
- # for compressed versions of files.
- scheme, netloc, upath, uparams, uquery, ufrag = urlparse(path)
- return bool(scheme and netloc)
- def _cache(self, path):
- """Cache the file specified by path.
- Creates a copy of the file in the datasource cache.
- """
- # We import these here because importing them is slow and
- # a significant fraction of numpy's total import time.
- import shutil
- from urllib.request import urlopen
- upath = self.abspath(path)
- # ensure directory exists
- if not os.path.exists(os.path.dirname(upath)):
- os.makedirs(os.path.dirname(upath))
- # TODO: Doesn't handle compressed files!
- if self._isurl(path):
- with urlopen(path) as openedurl:
- with _open(upath, 'wb') as f:
- shutil.copyfileobj(openedurl, f)
- else:
- shutil.copyfile(path, upath)
- return upath
- def _findfile(self, path):
- """Searches for ``path`` and returns full path if found.
- If path is an URL, _findfile will cache a local copy and return the
- path to the cached file. If path is a local file, _findfile will
- return a path to that local file.
- The search will include possible compressed versions of the file
- and return the first occurrence found.
- """
- # Build list of possible local file paths
- if not self._isurl(path):
- # Valid local paths
- filelist = self._possible_names(path)
- # Paths in self._destpath
- filelist += self._possible_names(self.abspath(path))
- else:
- # Cached URLs in self._destpath
- filelist = self._possible_names(self.abspath(path))
- # Remote URLs
- filelist = filelist + self._possible_names(path)
- for name in filelist:
- if self.exists(name):
- if self._isurl(name):
- name = self._cache(name)
- return name
- return None
- def abspath(self, path):
- """
- Return absolute path of file in the DataSource directory.
- If `path` is an URL, then `abspath` will return either the location
- the file exists locally or the location it would exist when opened
- using the `open` method.
- Parameters
- ----------
- path : str
- Can be a local file or a remote URL.
- Returns
- -------
- out : str
- Complete path, including the `DataSource` destination directory.
- Notes
- -----
- The functionality is based on `os.path.abspath`.
- """
- # We do this here to reduce the 'import numpy' initial import time.
- from urllib.parse import urlparse
- # TODO: This should be more robust. Handles case where path includes
- # the destpath, but not other sub-paths. Failing case:
- # path = /home/guido/datafile.txt
- # destpath = /home/alex/
- # upath = self.abspath(path)
- # upath == '/home/alex/home/guido/datafile.txt'
- # handle case where path includes self._destpath
- splitpath = path.split(self._destpath, 2)
- if len(splitpath) > 1:
- path = splitpath[1]
- scheme, netloc, upath, uparams, uquery, ufrag = urlparse(path)
- netloc = self._sanitize_relative_path(netloc)
- upath = self._sanitize_relative_path(upath)
- return os.path.join(self._destpath, netloc, upath)
- def _sanitize_relative_path(self, path):
- """Return a sanitised relative path for which
- os.path.abspath(os.path.join(base, path)).startswith(base)
- """
- last = None
- path = os.path.normpath(path)
- while path != last:
- last = path
- # Note: os.path.join treats '/' as os.sep on Windows
- path = path.lstrip(os.sep).lstrip('/')
- path = path.lstrip(os.pardir).lstrip('..')
- drive, path = os.path.splitdrive(path) # for Windows
- return path
- def exists(self, path):
- """
- Test if path exists.
- Test if `path` exists as (and in this order):
- - a local file.
- - a remote URL that has been downloaded and stored locally in the
- `DataSource` directory.
- - a remote URL that has not been downloaded, but is valid and
- accessible.
- Parameters
- ----------
- path : str
- Can be a local file or a remote URL.
- Returns
- -------
- out : bool
- True if `path` exists.
- Notes
- -----
- When `path` is an URL, `exists` will return True if it's either
- stored locally in the `DataSource` directory, or is a valid remote
- URL. `DataSource` does not discriminate between the two, the file
- is accessible if it exists in either location.
- """
- # First test for local path
- if os.path.exists(path):
- return True
- # We import this here because importing urllib is slow and
- # a significant fraction of numpy's total import time.
- from urllib.request import urlopen
- from urllib.error import URLError
- # Test cached url
- upath = self.abspath(path)
- if os.path.exists(upath):
- return True
- # Test remote url
- if self._isurl(path):
- try:
- netfile = urlopen(path)
- netfile.close()
- del(netfile)
- return True
- except URLError:
- return False
- return False
- def open(self, path, mode='r', encoding=None, newline=None):
- """
- Open and return file-like object.
- If `path` is an URL, it will be downloaded, stored in the
- `DataSource` directory and opened from there.
- Parameters
- ----------
- path : str
- Local file path or URL to open.
- mode : {'r', 'w', 'a'}, optional
- Mode to open `path`. Mode 'r' for reading, 'w' for writing,
- 'a' to append. Available modes depend on the type of object
- specified by `path`. Default is 'r'.
- encoding : {None, str}, optional
- Open text file with given encoding. The default encoding will be
- what `io.open` uses.
- newline : {None, str}, optional
- Newline to use when reading text file.
- Returns
- -------
- out : file object
- File object.
- """
- # TODO: There is no support for opening a file for writing which
- # doesn't exist yet (creating a file). Should there be?
- # TODO: Add a ``subdir`` parameter for specifying the subdirectory
- # used to store URLs in self._destpath.
- if self._isurl(path) and self._iswritemode(mode):
- raise ValueError("URLs are not writeable")
- # NOTE: _findfile will fail on a new file opened for writing.
- found = self._findfile(path)
- if found:
- _fname, ext = self._splitzipext(found)
- if ext == 'bz2':
- mode.replace("+", "")
- return _file_openers[ext](found, mode=mode,
- encoding=encoding, newline=newline)
- else:
- raise FileNotFoundError(f"{path} not found.")
- class Repository (DataSource):
- """
- Repository(baseurl, destpath='.')
- A data repository where multiple DataSource's share a base
- URL/directory.
- `Repository` extends `DataSource` by prepending a base URL (or
- directory) to all the files it handles. Use `Repository` when you will
- be working with multiple files from one base URL. Initialize
- `Repository` with the base URL, then refer to each file by its filename
- only.
- Parameters
- ----------
- baseurl : str
- Path to the local directory or remote location that contains the
- data files.
- destpath : str or None, optional
- Path to the directory where the source file gets downloaded to for
- use. If `destpath` is None, a temporary directory will be created.
- The default path is the current directory.
- Examples
- --------
- To analyze all files in the repository, do something like this
- (note: this is not self-contained code)::
- >>> repos = np.lib._datasource.Repository('/home/user/data/dir/')
- >>> for filename in filelist:
- ... fp = repos.open(filename)
- ... fp.analyze()
- ... fp.close()
- Similarly you could use a URL for a repository::
- >>> repos = np.lib._datasource.Repository('http://www.xyz.edu/data')
- """
- def __init__(self, baseurl, destpath=os.curdir):
- """Create a Repository with a shared url or directory of baseurl."""
- DataSource.__init__(self, destpath=destpath)
- self._baseurl = baseurl
- def __del__(self):
- DataSource.__del__(self)
- def _fullpath(self, path):
- """Return complete path for path. Prepends baseurl if necessary."""
- splitpath = path.split(self._baseurl, 2)
- if len(splitpath) == 1:
- result = os.path.join(self._baseurl, path)
- else:
- result = path # path contains baseurl already
- return result
- def _findfile(self, path):
- """Extend DataSource method to prepend baseurl to ``path``."""
- return DataSource._findfile(self, self._fullpath(path))
- def abspath(self, path):
- """
- Return absolute path of file in the Repository directory.
- If `path` is an URL, then `abspath` will return either the location
- the file exists locally or the location it would exist when opened
- using the `open` method.
- Parameters
- ----------
- path : str
- Can be a local file or a remote URL. This may, but does not
- have to, include the `baseurl` with which the `Repository` was
- initialized.
- Returns
- -------
- out : str
- Complete path, including the `DataSource` destination directory.
- """
- return DataSource.abspath(self, self._fullpath(path))
- def exists(self, path):
- """
- Test if path exists prepending Repository base URL to path.
- Test if `path` exists as (and in this order):
- - a local file.
- - a remote URL that has been downloaded and stored locally in the
- `DataSource` directory.
- - a remote URL that has not been downloaded, but is valid and
- accessible.
- Parameters
- ----------
- path : str
- Can be a local file or a remote URL. This may, but does not
- have to, include the `baseurl` with which the `Repository` was
- initialized.
- Returns
- -------
- out : bool
- True if `path` exists.
- Notes
- -----
- When `path` is an URL, `exists` will return True if it's either
- stored locally in the `DataSource` directory, or is a valid remote
- URL. `DataSource` does not discriminate between the two, the file
- is accessible if it exists in either location.
- """
- return DataSource.exists(self, self._fullpath(path))
- def open(self, path, mode='r', encoding=None, newline=None):
- """
- Open and return file-like object prepending Repository base URL.
- If `path` is an URL, it will be downloaded, stored in the
- DataSource directory and opened from there.
- Parameters
- ----------
- path : str
- Local file path or URL to open. This may, but does not have to,
- include the `baseurl` with which the `Repository` was
- initialized.
- mode : {'r', 'w', 'a'}, optional
- Mode to open `path`. Mode 'r' for reading, 'w' for writing,
- 'a' to append. Available modes depend on the type of object
- specified by `path`. Default is 'r'.
- encoding : {None, str}, optional
- Open text file with given encoding. The default encoding will be
- what `io.open` uses.
- newline : {None, str}, optional
- Newline to use when reading text file.
- Returns
- -------
- out : file object
- File object.
- """
- return DataSource.open(self, self._fullpath(path), mode,
- encoding=encoding, newline=newline)
- def listdir(self):
- """
- List files in the source Repository.
- Returns
- -------
- files : list of str
- List of file names (not containing a directory part).
- Notes
- -----
- Does not currently work for remote repositories.
- """
- if self._isurl(self._baseurl):
- raise NotImplementedError(
- "Directory listing of URLs, not supported yet.")
- else:
- return os.listdir(self._baseurl)
|