Skip to content

Commit

Permalink
Deploy gh-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
github-actions committed Dec 3, 2024
1 parent 0fd1016 commit b704817
Show file tree
Hide file tree
Showing 15 changed files with 1,523 additions and 4 deletions.
9 changes: 9 additions & 0 deletions genindex.html
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,13 @@ <h2>H</h2>

<ul>
<li><a href="hydrotools.nwis_client.iv.html#module-hydrotools.nwis_client.iv">module</a>
</li>
</ul></li>
<li>
hydrotools.nwm_client

<ul>
<li><a href="hydrotools.nwm_client.html#module-hydrotools.nwm_client">module</a>
</li>
</ul></li>
</ul></td>
Expand Down Expand Up @@ -661,6 +668,8 @@ <h2>M</h2>
<li><a href="hydrotools.nwis_client.html#module-hydrotools.nwis_client">hydrotools.nwis_client</a>
</li>
<li><a href="hydrotools.nwis_client.iv.html#module-hydrotools.nwis_client.iv">hydrotools.nwis_client.iv</a>
</li>
<li><a href="hydrotools.nwm_client.html#module-hydrotools.nwm_client">hydrotools.nwm_client</a>
</li>
</ul></li>
</ul></td>
Expand Down
6 changes: 3 additions & 3 deletions hydrotools.nwm_client.html
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ <h2>Submodules<a class="headerlink" href="#submodules" title="Link to this headi
</ul>
</div>
</section>
<section id="module-contents">
<h2>Module contents<a class="headerlink" href="#module-contents" title="Link to this heading"></a></h2>
<section id="module-hydrotools.nwm_client">
<span id="module-contents"></span><h2>Module contents<a class="headerlink" href="#module-hydrotools.nwm_client" title="Link to this heading"></a></h2>
</section>
</section>

Expand Down Expand Up @@ -347,7 +347,7 @@ <h2>Module contents<a class="headerlink" href="#module-contents" title="Link to
<ul>
<li><a class="reference internal" href="#">hydrotools.nwm_client subpackage</a><ul>
<li><a class="reference internal" href="#submodules">Submodules</a></li>
<li><a class="reference internal" href="#module-contents">Module contents</a></li>
<li><a class="reference internal" href="#module-hydrotools.nwm_client">Module contents</a></li>
</ul>
</li>
</ul>
Expand Down
Binary file modified objects.inv
Binary file not shown.
6 changes: 6 additions & 0 deletions py-modindex.html
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,12 @@ <h1>Python Module Index</h1>
<a href="hydrotools.nwis_client.iv.html#module-hydrotools.nwis_client.iv"><code class="xref">hydrotools.nwis_client.iv</code></a></td><td>
<em></em></td>
</tr>
<tr class="cg-1">
<td></td>
<td>&#160;&#160;&#160;
<a href="hydrotools.nwm_client.html#module-hydrotools.nwm_client"><code class="xref">hydrotools.nwm_client</code></a></td><td>
<em></em></td>
</tr>
</table>

</article>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
=============================================
NWM Azure Blob Storage Container File Catalog
=============================================
Concrete implementation of a National Water Model file client for discovering
files on Microsoft Azure.
https://planetarycomputer.microsoft.com/dataset/storage/noaa-nwm
Classes
-------
AzureFileCatalog
"""
from .NWMFileCatalog import NWMFileCatalog
from hydrotools._restclient.urllib import Url
import azure.storage.blob
import planetary_computer
import adlfs
from typing import List

class AzureFileCatalog(NWMFileCatalog):
"""An Azure Cloud client class for NWM data.
This AzureFileCatalog class provides various methods for discovering NWM
files on Azure Blob Storage.
"""

def __init__(
self,
server: str = 'https://noaanwm.blob.core.windows.net/'
) -> None:
"""Initialize catalog of NWM data source on Azure Blob Storage.
Parameters
----------
server : str, required
Fully qualified path to Azure Cloud endpoint.
Returns
-------
None
"""
super().__init__()
self.server = server

def list_blobs(
self,
configuration: str,
reference_time: str,
must_contain: str = 'channel_rt'
) -> List[str]:
"""List available blobs with provided parameters.
Parameters
----------
configuration : str, required
Particular model simulation or forecast configuration. For a list
of available configurations see NWMDataService.configurations
reference_time : str, required
Model simulation or forecast issuance/reference time in
YYYYmmddTHHZ format.
must_contain : str, optional, default 'channel_rt'
Optional substring found in each blob name.
Returns
-------
A list of blob names that satisfy the criteria set by the parameters.
"""
# Validate configuration
self.raise_invalid_configuration(configuration)

# Break-up reference time
issue_date, issue_time = self.separate_datetime(reference_time)

# Get list of blobs
fs = adlfs.AzureBlobFileSystem(
"noaanwm", credential=planetary_computer.sas.get_token("noaanwm", "nwm").token
)
blobs = fs.glob(f"nwm/nwm.{issue_date}/{configuration}/nwm.t{issue_time}*")

# Return blob URLs
return [
str(self.server / suffix)
for suffix in list(blobs)
if must_contain in suffix
]

@property
def server(self) -> str:
return self._server

@server.setter
def server(self, server: str) -> None:
self._server = Url(server)

209 changes: 209 additions & 0 deletions python/nwm_client/build/lib/hydrotools/nwm_client/FileDownloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
"""
===============
File Downloader
===============
Tool for downloading files asynchronously.
Classes
-------
FileDownloader
"""
import asyncio
import ssl
import aiohttp
import aiofiles
from pathlib import Path
from typing import List, Tuple, Union
import warnings
from http import HTTPStatus

class FileDownloader:
"""Provides a convenient interface to download a list of files
asynchronously using HTTP.
"""

def __init__(
self,
output_directory: Union[str, Path] = Path("."),
create_directory: bool = False,
ssl_context: ssl.SSLContext = ssl.create_default_context(),
limit: int = 10
) -> None:
"""Initialize File Downloader object with specified output directory.
Parameters
----------
output_directory: str, pathlib.Path, optional, default "."
Output directory where files are written.
create_directory: bool, options, default False
Indicates whether to create the output directory if it does not
exist.
ssl_context : ssl.SSLContext, optional, default context
SSL configuration context.
limit: int, optional, default 10
Number of simultaneous connections.
Returns
-------
None
"""
# Set output directory
self.output_directory = output_directory

# Set directory creation
self.create_directory = create_directory

# Setup SSL context
self.ssl_context = ssl_context

# Set limit
self.limit = limit

async def get_file(
self,
url: str,
filename: str,
session: aiohttp.ClientSession
) -> None:
"""Download a single file.
Parameters
----------
url: str, required
URL path to file.
filename: str, required
Local filename used to write downloaded file. This will save the file
to self.output_directory/filename
session: aiohttp.ClientSession, required
Session object used for retrieval.
Returns
-------
None
"""
# Retrieve a single file
async with session.get(url, ssl=self.ssl_context, timeout=900) as response:
# Warn if unable to locate file
if response.status != HTTPStatus.OK:
status = HTTPStatus(response.status_code)
message = (
f"HTTP Status: {status.value}" +
f" - {status.phrase}" +
f" - {status.description}\n" +
f"{response.url}"
)
warnings.warn(message, RuntimeWarning)
return

# Construct output file path
output_file = self.output_directory / filename

# Stream download
async with aiofiles.open(output_file, 'wb') as fo:
while True:
chunk = await response.content.read(1024)
if not chunk:
break
await fo.write(chunk)

async def get_files(self, src_dst_list: List[Tuple[str,str]]) -> None:
"""Asynchronously download multiple files.
Parameters
----------
src_dst_list: List[Tuple[str,str]], required
List of tuples containing two strings. The first string is the
source URL from which to retrieve a file, the second string is the
local filename where the file will be saved.
Returns
-------
None
"""
# Retrieve each file
connector = aiohttp.TCPConnector(limit=self.limit)
async with aiohttp.ClientSession(connector=connector) as session:
await asyncio.gather(*[self.get_file(url, filename, session) for url, filename in src_dst_list])

def get(self, src_dst_list: List[Tuple[str,str]], overwrite: bool = False) -> None:
"""Setup event loop and asynchronously download multiple files. If
self.create_directory is True, an output directory will be
created if needed.
Parameters
----------
src_dst_list: List[Tuple[str,str]], required
List of tuples containing two strings. The first string is the
source URL from which to retrieve a file, the second string is the
local filename where the file will be saved.
overwrite: bool, optional, default False
If True will overwrite destination file, if it exists. If False,
download of this file is skipped.
Returns
-------
None
Examples
--------
>>> from nwm_client.FileDownloader import FileDownloader
>>> downloader = FileDownloader(output_directory="my_output")
>>> # This will download the pandas homepage and save it to "my_output/index.html"
>>> downloader.get(
>>> [("https://pandas.pydata.org/docs/user_guide/index.html","index.html")]
>>> )
"""
# Shorten list to files that do not exist
if not overwrite:
short = []
for src, dst in src_dst_list:
if (self.output_directory / dst).exists():
message = f"File exists, skipping download of {self.output_directory / dst}"
warnings.warn(message, UserWarning)
continue
short.append((src, dst))
src_dst_list = short

# Check output directory, optionally create
if not self.output_directory.exists():
if self.create_directory:
self.output_directory.mkdir(parents=True)
else:
message = f"{self.output_directory} does not exist."
raise FileNotFoundError(message)

# Start event loop to retrieve files
asyncio.run(self.get_files(src_dst_list))

@property
def output_directory(self) -> Path:
return self._output_directory

@output_directory.setter
def output_directory(self, output_directory: Union[str, Path]):
self._output_directory = Path(output_directory).expanduser().resolve()

@property
def create_directory(self) -> bool:
return self._create_directory

@create_directory.setter
def create_directory(self, create_directory: bool):
self._create_directory = bool(create_directory)

@property
def ssl_context(self) -> ssl.SSLContext:
return self._ssl_context

@ssl_context.setter
def ssl_context(self, ssl_context: ssl.SSLContext) -> None:
self._ssl_context = ssl_context

@property
def limit(self) -> int:
return self._limit

@limit.setter
def limit(self, limit: int) -> None:
self._limit = limit
Loading

0 comments on commit b704817

Please sign in to comment.