Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace cartopy dependency with own downloader. #28

Merged
merged 30 commits into from
Nov 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9c74aaf
Move download code to hyoga.core.download.
juseg Nov 14, 2022
b5bf0b6
Replace _download method with a downloader class.
juseg Nov 14, 2022
0e313fd
Separate generic and basename downloaders.
juseg Nov 14, 2022
76b8ed3
Remove _download_example private method.
juseg Nov 14, 2022
18c3a9d
Add tiny OSFDownloader class.
juseg Nov 14, 2022
622b5ec
Add ZipShapeDownloader for Ehlers et al data.
juseg Nov 14, 2022
dc000c8
Fix download module header and docstring.
juseg Nov 14, 2022
e7c8504
Move hyoga.core.download to hyoga.open.downloader.
juseg Nov 15, 2022
68638b5
Add commend with overnight ideas for subclassing.
juseg Nov 15, 2022
5296559
Add quick and dirty natural earth downloader.
juseg Nov 15, 2022
558a7f5
Use XDG_CACHE_HOME env variable if present.
juseg Nov 15, 2022
e4f9e75
Store Natural Earth shapefiles in subdirectories.
juseg Nov 15, 2022
01a7a7d
Fix plotting ehl11 paleoglaciers, crs is in .prj.
juseg Nov 15, 2022
5b13a22
Remove now-unused BasenameDownloader.
juseg Nov 15, 2022
42c9553
Require shapefile dbf, prj, shp, shx, ignore cpg.
juseg Nov 15, 2022
7f50b2a
Split downloader call into url, path, check, get.
juseg Nov 15, 2022
4025ab1
Add CacheDownloader to download files in cache.
juseg Nov 15, 2022
9a56452
Rewrite, document OSFDownloader.
juseg Nov 15, 2022
65debbd
Add generic ArchiveDownloader base class.
juseg Nov 17, 2022
c763e16
Separate args, kwargs in generic Downloader call.
juseg Nov 17, 2022
ba0652d
Use new logic in NaturalEarthDownloader, it works!
juseg Nov 17, 2022
5344795
Add missing downloader dosctrings, homogenize.
juseg Nov 17, 2022
f83ce10
Pylint had a lot of complaints on my code.
juseg Nov 17, 2022
0756ed8
Remove outdated cities plot methods.
juseg Nov 17, 2022
9263331
Remove dependency on Cartopy.
juseg Nov 17, 2022
027d575
Remove cartopy dependency in doc, whatsnew.
juseg Nov 17, 2022
333288f
Replace conda with pip for readthedocs build.
juseg Nov 17, 2022
993255f
Remove readthedocs build settings.
juseg Nov 17, 2022
53c81ed
Require Python >= 3.8.
juseg Nov 17, 2022
d5aece5
Ask readthedocs to use Python 3.8.
juseg Nov 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ version: 2
# install hyoga for autodoc etc
python:
install:
- path: .
- method: pip
path: .
extra_requirements:
- docs

# use mamba instead of conda
# Set the version of Python and other tools you might need
build:
os: "ubuntu-20.04"
os: ubuntu-20.04
tools:
python: "mambaforge-4.10"

# conda is required by cartopy (proj)
conda:
environment: doc/environment.yml
python: "3.8"
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ Hyoga
:target: https://zenodo.org/badge/latestdoi/227465038

Hyoga_ is a small Python_ library to visualize ice-sheet model datasets.
It acts as a thin wrapper around cartopy_ and xarray_ for CF_-compliant
It acts as a thin wrapper around GeoPandas_ and xarray_ for CF_-compliant
datasets on regular grids used for instance in PISM_. Hyoga (氷河) is the
Japanese word for glacier (lit. ice river). See the documentation_ for more.

.. _cartopy: https://scitools.org.uk/cartopy/
.. _CF: https://cfconventions.org
.. _documentation: https://hyoga.readthedocs.io
.. _GeoPandas: https://geopandas.org
.. _Hyoga: https://hyoga.readthedocs.io
.. _PISM: https://pism.io
.. _Python: https://python.org
Expand Down
2 changes: 0 additions & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@
# object shortcuts
'Axes': '~matplotlib.axes.Axes',
'AxesImage': '~matplotlib.image.AxesImage',
'CRS': '~cartopy.crs.CRS',
'GeoAxes': '~cartopy.mpl.geoaxes.GeoAxes',
'GeoDataFrame': '~geopandas.GeoDataFrame',
'QuadContourSet': '~matplotlib.contour.QuadContourSet',
'StreamplotSet': '~matplotlib.streamplot.StreamplotSet',
Expand Down
1 change: 0 additions & 1 deletion doc/datasets/plotting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,5 @@ Last Glacial Maximum.

.. plot:: ../examples/datasets/plot_bedrock_isostasy.py

.. _cartopy: https://scitools.org.uk/cartopy/
.. _matplotlib: https://matplotlib.org
.. _xarray: https//xarray.pydata.org
1 change: 0 additions & 1 deletion doc/environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ channels:
- conda-forge
dependencies:
- python
- cartopy
- contextily
- cf_xarray
- geopandas
Expand Down
3 changes: 1 addition & 2 deletions doc/foreword/startup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ Getting started
Installing hyoga
----------------

Assuming xarray_, geopandas_ and cartopy_ are already installed, I recommend
Assuming GeoPandas_ and xarray_ are already installed, I recommend
installing hyoga using pip::

pip install hyoga

.. _cartopy: https://scitools.org.uk/cartopy/
.. _geopandas: https://geopandas.org
.. _xarray: https://xarray.pydata.org/en/stable/

Expand Down
4 changes: 2 additions & 2 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ Hyoga
=====

Hyoga is a small Python_ library to visualize ice-sheet model datasets.
It acts as a thin wrapper around cartopy_ and xarray_ for CF_-compliant
It acts as a thin wrapper around GeoPandas_ and xarray_ for CF_-compliant
datasets on regular grids used for instance in PISM_. Hyoga (氷河) is the
Japanese word for glacier (lit. ice river).

.. _cartopy: https://scitools.org.uk/cartopy/
.. _GeoPandas: https://geopandas.org
.. _CF: https://cfconventions.org
.. _PISM: https://pism.io
.. _Python: https://python.org
Expand Down
23 changes: 23 additions & 0 deletions doc/whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@
What's new
==========

.. _v0.2.1:

v0.2.1 (unreleased)
-------------------

Breaking changes
~~~~~~~~~~~~~~~~

- Require Python 3.8 or newer (see
`xarray#7115 <https://github.com/pydata/xarray/issues/7115>`_,
`NEP-29 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`_)

New features
~~~~~~~~~~~~

- Remove dependency on cartopy_ (:issue:`25`, :pull:`28`).

Internal changes
~~~~~~~~~~~~~~~~

- Cache data in ``XDG_CACHE_HOME`` if variable if present in environment.
- Add downloaders in :mod:`hyoga.open.downloader` (:issue:`25`, :pull:`28`).

.. _v0.2.0:

v0.2.0 Bale (1 Nov. 2022)
Expand Down
210 changes: 210 additions & 0 deletions hyoga/open/downloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Copyright (c) 2022, Julien Seguinot (juseg.github.io)
# GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)

"""
This module provide so-called downloader classes for various websites and data
formats as needed by hyoga. Downloader objects are callable that retrieve one
or several files from the web, store them in hyoga's cache directory, and
return a path to that file, or to the main file.
"""

import os.path
import zipfile
import requests


class Downloader:
"""A callable that downloads a file and returns its path when called.

This is a base class for callable downloaders. Customization can be done by
subclassing and overwriting the following methods:

* :meth:`url`: return the url of file to download.
* :meth:`path`: return local path of downloaded file.
* :meth:`check`: check whether file is present or valid.
* :meth:`get`: actually download file (and any meta file).

Call parameters
---------------
url : str
The url of the file to download.
path : str
The local path of the downloaded file.

Returns
-------
path : str
The local path of the downloaded file.
"""

def __call__(self, *args, **kwargs):
"""See class documentation for actual signature.

Parameters
----------
*args :
Positional arguments are passed to :meth:`url` and :meth:`path`.
These two methods need to have compatible signatures.
**kwargs :
Keyword arguments are passed to :meth:`get` to alter the download
recipe. This is used to provide a member filename in an archive.
"""
url = self.url(*args)
path = self.path(*args)
if not self.check(path):
self.get(url, path, **kwargs)
return path

def url(self, *args):
"""Return url of file to download."""
return args[0]

def path(self, *args):
"""Return local path of downloaded file."""
return args[1]

def check(self, path):
"""Check whether file is present."""
return os.path.isfile(path)

def get(self, url, path):
"""Download online `url` to local `path`."""

# create directory if missing
os.makedirs(os.path.dirname(path), exist_ok=True)

# download file
with open(path, 'wb') as binaryfile:
print(f"downloading {url}...")
binaryfile.write(requests.get(url, timeout=5).content)


class CacheDownloader(Downloader):
"""A downloader that stores files in hyoga's cache directory.

Call parameters
---------------
url : str
The url of the file to download
path : str
The path of the downloaded file relative to the cache directory.
"""

def path(self, *args):
path = super().path(*args)
xdg_cache = os.environ.get("XDG_CACHE_HOME", os.path.join(
os.path.expanduser('~'), '.cache'))
return os.path.join(xdg_cache, 'hyoga', path)


class OSFDownloader(CacheDownloader):
"""A class to download files by record key from osf.io.

Call parameters
---------------
record : str
Record key of the file to download on osf.io.
path : str
The path of the downloaded file relative to the cache directory.
"""

def url(self, *args):
record = super().url(*args)
return 'https://osf.io/' + record + '/download'


class ArchiveDownloader(CacheDownloader):
"""A base class to download archives and extract member files.

Call parameters
---------------
url : str
The url of the file to download
path : str
The path of the extracted file relative to the cache directory.
member : str, optional
Member file to extract from archive, default to basename of ``path``.
"""

def get(self, url, path, member=None):

# save archive as named online
outdir, basename = os.path.split(path)
archivepath = os.path.join(outdir, url.split('/')[-1])

# download it only if missing
if not super().check(archivepath):
super().get(url, archivepath)

# by default assume member name is path basename
member = member or basename

# this needs to be implemented in subclasses
self.deflate(archivepath, member, outdir)

def deflate(self, archivepath, member, outdir):
"""Extract member and any other needed files from the archive."""
raise NotImplementedError("This should be implemented in subclasses.")


class ShapeZipDownloader(ArchiveDownloader):
"""A downloader that extract shapefiles and metafiles from zip archives.

Call parameters
---------------
url : str
The url of the file to download
path : str
The path of the extracted file relative to the cache directory.
member : str, optional
Member file to extract from , default to the basename of ``path``.
"""

def deflate(self, archivepath, member, outdir):
stem, ext = os.path.splitext(member)
with zipfile.ZipFile(archivepath, 'r') as archive:
for ext in ('.shp', '.dbf', '.prj', '.shx'):
archive.extract(stem+ext, path=outdir)


class NaturalEarthDownloader(ShapeZipDownloader):
"""A downloader for Natural Earth Data.

Call parameters
---------------
scale : {'10m', '50m', '110m'}
Natural Earth data scale.
category : {'cultural', 'physical'}
Natural Earth data category.
theme : str or iterable
Natural Earth data theme.
"""

def url(self, *args):
scale, category, theme = args
return (
f'https://naturalearth.s3.amazonaws.com/{scale}_{category}/'
f'ne_{scale}_{theme}.zip')

def path(self, *args):

# this is where cartopy stores the same data
scale, category, theme = args
xdg_data_home = os.environ.get("XDG_DATA_HOME", os.path.join(
os.path.expanduser('~'), '.local', 'share'))
cartopy_stem = os.path.join(
xdg_data_home, 'cartopy', 'shapefiles', 'natural_earth',
category, f'ne_{scale}_{theme}')

# if all files are there, return this path
extensions = ('.shp', '.dbf', '.prj', '.shx')
if all(os.path.isfile(cartopy_stem+ext) for ext in extensions):
return cartopy_stem + '.shp'

# otherwise return path relative to hyoga cache
xdg_cache = os.environ.get("XDG_CACHE_HOME", os.path.join(
os.path.expanduser('~'), '.cache'))
path = os.path.join(
xdg_cache, 'hyoga', 'natural_earth', f'{scale}_{category}',
f'ne_{scale}_{theme}.shp')
return path
33 changes: 6 additions & 27 deletions hyoga/open/example.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2021, Julien Seguinot (juseg.github.io)
# Copyright (c) 2021-2022, Julien Seguinot (juseg.github.io)
# GNU General Public License v3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)

"""
Expand All @@ -8,36 +8,15 @@
series and plotting output from other models.
"""

import os.path
import urllib.parse
import requests
import xarray as xr

import hyoga.open.downloader

def _download(url, filename=None):
"""Download a file from the web, store in cache dir and return path."""
cachedir = os.path.expanduser(os.path.join('~', '.cache', 'hyoga'))
if filename is None:
filename = urllib.parse.urlparse(url).path
filename = os.path.basename(filename)
filename = filename.split('/')[-1]
filepath = os.path.join(cachedir, filename)
if not os.path.isfile(filepath):
os.makedirs(cachedir, exist_ok=True)
with open(filepath, 'wb') as binaryfile:
print(f"downloading {url}...")
binaryfile.write(requests.get(url).content)
return filepath


def _download_example(filename):
"""Download a file from hyoga-data github repository."""
def example(filename='pism.alps.out.2d.nc'):
"""Open cached example dataset from hyoga-data github repository."""
repo = 'https://raw.githubusercontent.com/juseg/hyoga-data/main'
model = filename.split('.')[0]
url = '/'.join((repo, model, filename))
return _download(url)


def example(filename='pism.alps.out.2d.nc'):
"""Open cached example dataset from hyoga-data github repository."""
return xr.open_dataset(_download_example(filename))
path = hyoga.open.downloader.CacheDownloader()(url, filename)
return xr.open_dataset(path)
Loading