From a8bf8a7bae82c0bc7edd7311fc460174ad99ed1d Mon Sep 17 00:00:00 2001 From: Rob Purser Date: Sat, 18 Jun 2022 20:25:19 -0400 Subject: [PATCH] Version 2.0. Rewrite the structure of how requests are made. Closes 1. Closes 2. --- .gitignore | 3 + CONTRIBUTING.MD | 2 +- README.md | 19 +- climateDataStoreDownload.m | 117 +++---- climateDataStoreDownloadAsync.m | 61 ++++ climateDataStoreDownloadFuture.m | 303 ++++++++++++++++++ doc/GettingStarted.mlx | Bin 261427 -> 261421 bytes .../6Qs0zy_xL8w5LlCriG1YSKS4c1Up.xml | 2 - .../HijoyS_64DkOOAoyRR4POJTVd5Ip.xml | 2 - .../VVjNhDqUv_EfLSA-gIMBB4Hr5nop.xml | 2 - .../i8H86N7Wp9VkJX8VNFw61Ce6OD4p.xml | 2 - .../aeJiMMu8h7RomwdS0y3vinxk4bcp.xml | 2 - .../q_SIXYHhcOVj8Ng1kvp0NbWmwy4d.xml | 2 +- .../xYnUlcu4Eh0PKXg1-f4AttsdxCId.xml | 2 + .../xYnUlcu4Eh0PKXg1-f4AttsdxCIp.xml | 2 + .../5Lk_HLOyBtA13ybsC7_0l8N5sPcd.xml} | 2 +- .../5Lk_HLOyBtA13ybsC7_0l8N5sPcp.xml | 2 + .../NdAIzigXKuGeE8mznIQJtzJigdgd.xml} | 2 +- .../NdAIzigXKuGeE8mznIQJtzJigdgp.xml | 2 + .../OHJQigMu3q1B5FOfzzAahtJ5SDsd.xml} | 2 +- .../OHJQigMu3q1B5FOfzzAahtJ5SDsp.xml | 2 + .../p_IQDyqTeBgr4AKHqMhR0WFqDQAd.xml} | 2 +- .../p_IQDyqTeBgr4AKHqMhR0WFqDQAp.xml | 2 + .../BWqpowtnvNmi2uu1DaissESSZygp.xml | 2 - .../2uRqUwgzsMPtKV1vY4JvGEQwdJId.xml | 6 +- .../7jPt_znhfWPW_xTjFajix6Io0KId.xml} | 2 +- .../7jPt_znhfWPW_xTjFajix6Io0KIp.xml | 2 + .../8JROiIwiztKKAYd8t-TcpJAG_ycd.xml | 6 + .../8JROiIwiztKKAYd8t-TcpJAG_ycp.xml | 2 + .../FI0gxbH-PhwjE_riDQGHPyYMHksd.xml} | 2 +- .../FI0gxbH-PhwjE_riDQGHPyYMHksp.xml | 2 + .../MNy1Id1HkwAmZsn0aYjeanlt4i4d.xml | 6 +- .../TMK4UzWHdRLhy_w-CHt9y11Q8XAd.xml | 6 +- .../kbMR0mB3PsmvdJiP4KJDMaIADIwd.xml | 6 + .../kbMR0mB3PsmvdJiP4KJDMaIADIwp.xml | 2 + test/climateDataStoreDownloadTest.m | 206 ++++++++++++ test/longTest.m | 59 ++++ test/smokeTest.m | 33 ++ util/retrieveFromCDS.m | 12 - util/setupCDSAPIIfNeeded.m | 36 ++- 40 files changed, 804 insertions(+), 123 deletions(-) create mode 100644 climateDataStoreDownloadAsync.m create mode 100644 climateDataStoreDownloadFuture.m delete mode 100644 resources/project/BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Up.xml delete mode 100644 resources/project/BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Ip.xml delete mode 100644 resources/project/BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nop.xml delete mode 100644 resources/project/BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4p.xml delete mode 100644 resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcp.xml create mode 100644 resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCId.xml create mode 100644 resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCIp.xml rename resources/project/{BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nod.xml => FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcd.xml} (82%) create mode 100644 resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcp.xml rename resources/project/{BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4d.xml => FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgd.xml} (77%) create mode 100644 resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgp.xml rename resources/project/{BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Ud.xml => FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsd.xml} (77%) create mode 100644 resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsp.xml rename resources/project/{BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Id.xml => FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAd.xml} (77%) create mode 100644 resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAp.xml delete mode 100644 resources/project/YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygp.xml rename resources/project/{E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcd.xml => qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KId.xml} (77%) create mode 100644 resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KIp.xml create mode 100644 resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycd.xml create mode 100644 resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycp.xml rename resources/project/{YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygd.xml => qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksd.xml} (82%) create mode 100644 resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksp.xml create mode 100644 resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwd.xml create mode 100644 resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwp.xml create mode 100644 test/climateDataStoreDownloadTest.m create mode 100644 test/longTest.m create mode 100644 test/smokeTest.m delete mode 100644 util/retrieveFromCDS.m diff --git a/.gitignore b/.gitignore index 08e8079..ad4917f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ *.mltbx doc/html/*.html **/*.nc +**/*.csv +**/*.zip +**/*.grib \ No newline at end of file diff --git a/CONTRIBUTING.MD b/CONTRIBUTING.MD index ea5705f..0f851c4 100644 --- a/CONTRIBUTING.MD +++ b/CONTRIBUTING.MD @@ -15,7 +15,7 @@ Thank you for your interest in contributing to a MathWorks repository! We encou ## Guidelines -We don't have best practices for writing MATLAB code, but we do have some recommendations: +We don't have best practices for writing MATLAB® code, but we do have some recommendations: * You should not have any warnings or errors in the [code analyzer report](http://www.mathworks.com/help/matlab/matlab_prog/matlab-code-analyzer-report.html) * [Loren Shure's blog](https://blogs.mathworks.com/loren) has [great advice on improving your MATLAB code](https://blogs.mathworks.com/loren/category/best-practice/) diff --git a/README.md b/README.md index 2105dd7..a95b639 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,22 @@ MATLAB® Tools to access [The Climate Data Store](https://cds.climate.coperni | Function | Description | | ------ | ------ | | `climateDataStoreDownload` | Get data from Copernicus Climate Data Store | +| `climateDataStoreDownloadASync` | Queue a request data from Copernicus Climate Data Store and continue working in MATLAB. | ## Usage 1. See the notes below for information on first time install - 2. type `help climateDataStoreDownload` for help on using the function + 2. type `help climateDataStoreDownload` or `help climateDataStoreDownloadAsync` for help on using the functions 3. Find your dataset at [Climate Data Store](https://cds.climate.copernicus.eu/#!/home) and click on the "download data" tab. Make your selections for the subset of data you want. Click "show API request" at the bottom. - 4. Use `climateDataStoreDownload` to get the data. The first parameter is the name of the data set to retrieve, and will be used as the name of the directory to put the downloaded files in. The second parameter is a MATLAB version of the python structure that selects what subset of the data to download. `climateDataStoreDownload` downloads the files, and returns a list of files that were downloaded. Typically, these are NetCDF files with an .nu extension, which are read using the [ncinfo](https://www.mathworks.com/help/matlab/ref/ncinfo.html) and [ncread](https://www.mathworks.com/help/matlab/ref/ncread.html) functions. Note that downloading the files can take some time, depending on how large they are. - + 4. Use `climateDataStoreDownload` to get the data. The first parameter is the name of the data set to retrieve. The second parameter is a MATLAB version of the python structure that selects what subset of the data to download. `climateDataStoreDownload` downloads the files, and returns a list of files that were downloaded. Note that downloading the files can take some time, depending on how large they are. If you have really large files, `climateDataStoreDownloadAsync can be helpful. + + Typically, files returned are: + + | File Type | Extension | MATLAB Functions | + |-----------|-----------|------------------| + | NetCDF | `.nu` | [`ncinfo`](https://www.mathworks.com/help/matlab/ref/ncinfo.html) , [`ncread`](https://www.mathworks.com/help/matlab/ref/ncread.html) | + | GRIB | `.grib` | [`ncinfo`](https://www.mathworks.com/help/matlab/ref/ncinfo.html) , [`ncread`](https://www.mathworks.com/help/matlab/ref/ncread.html) | + | text | `.txt` , `.csv` | [`readtable`](https://www.mathworks.com/help/matlab/ref/readtable.html) ## First time Install * Requires MATLAB release R2019a or newer @@ -33,7 +41,10 @@ This demonstrates a number of MATLAB features, including: * [Toolbox Packaging](https://www.mathworks.com/help/matlab/matlab_prog/create-and-share-custom-matlab-toolboxes.html) * [MATLAB Projects](https://www.mathworks.com/help/matlab/projects.html) * [Argument validation](https://www.mathworks.com/help/matlab/matlab_prog/function-argument-validation-1.html) - +* [MATLAB Classes](https://www.mathworks.com/help/matlab/object-oriented-programming.html) + * [Static Methods](https://www.mathworks.com/help/matlab/matlab_oop/static-methods.html) + * [Property Access Methods](https://www.mathworks.com/help/matlab/matlab_oop/property-access-methods.html) + ## Example: Getting Started with Copernicus Climate Data Store Toolbox [The sea ice thickness dataset](https://cds.climate.copernicus.eu/cdsapp#!/dataset/satellite-sea-ice-thickness) provides monthly gridded data of sea ice thickness for the Arctic region based on satellite radar altimetry observations. Sea ice is an important component of our climate system and a sensitive indicator of climate change. Its presence or its retreat has a strong impact on air-sea interactions, the Earth’s energy budget as well as marine ecosystems. It is recognized by the Global Climate Observing System as an Essential Climate Variable. Sea ice thickness is one of the parameters commonly used to characterize sea ice, alongside sea ice concentration, sea ice edge, and sea ice type, also available in the Climate Data Store. diff --git a/climateDataStoreDownload.m b/climateDataStoreDownload.m index 3fd3e30..bbba2c1 100644 --- a/climateDataStoreDownload.m +++ b/climateDataStoreDownload.m @@ -1,82 +1,59 @@ -function [filePaths, citation] = climateDataStoreDownload(name,options) +function [filePaths, citation] = climateDataStoreDownload(datasetName,datasetOptions,options) %climateDataStoreDownload Get data from Copernicus Climate Data Store -% Download a data set from the Copernicus Climate Data Store. -% (https://cds.climate.copernicus.eu/cdsapp) +% Download a data set from the Copernicus Climate Data Store. (https://cds.climate.copernicus.eu/cdsapp) % -% Find your dataset at Climate Data Store and click on the "download data" -% tab. Make your selections for the subset of data you want. Click "show -% API request" at the bottom. +% Find your dataset at Climate Data Store and click on the "download data" tab. Make your selections for the subset of data you want. Click "show +% API request" at the bottom. % -% NAME is the name of the data set to retrieve, and will also be used as -% the name of the directory to put the downloaded files in. OPTIONS is a -% MATLAB structure matching the python structure shown when you choose -% "Show API request". The data files are downloaded, and the function -% returns FILEPATHS, a list of files downloaded. In addition, the function -% provides CITATION, which is the correct citiation to use with the data -% retrieved. +% [filePaths, citation] = climateDataStoreDownload(datasetName,datasetOptions) retrieves the data set with the name datasetName, and will also be used +% as the name of the directory or file downloaded files, with a date/time stamp added. datasetOptions is a MATLAB structure matching the python +% structure shown when you choose "Show API request". filePaths is string array of the files downloaded. The function returns FILEPATHS, a list of +% files downloaded. The function also returns citation, which is the correct citiation to use with the data retrieved. +% +% climateDataStoreDownload(...,Timeout Sets the maximum time in seconds to wait for a response. +% climateDataStoreDownload(...,DontExpandZIP=true Results that are ZIP files are not automatically expanded. +% climateDataStoreDownload(...,DontPromptForCredentials=true If no credentials are present, don't request them (intended for tests) % -% Downloading the files can take some time, depending on how large they are. +% Downloading the files can take some time, depending on how large they are (I've had it take 30 minutes!). You can check on the status of your +% request by visiting https://cds.climate.copernicus.eu/cdsapp#!/yourrequests. For large requests, you may want to consider using +% climateDataStoreDownloadAsync, which queues a request and returns, allowing you to keep working. % -% Notes: -% * You must have: -% * python 3.8 installed -% (https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe) +% You must have: +% * python 3.8 installed (https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe) % * the cdsapi python package (pip3 install cdsapi) -% * Your credentials need to be in a .cdsapirc file in your user -% directory. See https://cds.climate.copernicus.eu/api-how-to for more info -% * This function relies on the Python API to access the Copernicus Climate -% Data Store (CDS) (https://github.com/ecmwf/cdsapi) by the European Centre -% for Medium-Range Weather Forecasts (ECMWF) +% * Your credentials need to be in a .cdsapirc file in your user directory. See https://cds.climate.copernicus.eu/api-how-to for more info +% * This function relies on the Python API to access the Copernicus Climate Data Store (CDS) (https://github.com/ecmwf/cdsapi) by the European +% Centre for Medium-Range Weather Forecasts (ECMWF) % -% Example: Sea Ice thickness -% (https://cds.climate.copernicus.eu/cdsapp#!/dataset/satellite-sea-ice-thickness) -% options.version = "1_0"; -% options.variable = "all"; -% options.satellite = "cryosat_2"; -% options.cdr_type = ["cdr","icdr"]; -% options.year = ["2011","2021"]; -% options.month = "03"; -% downloadedFilePaths = climateDataStoreDownload('satellite-sea-ice-thickness',options); - -% Copyright 2021 The MathWorks, Inc. +% Example: Sea Ice thickness (https://cds.climate.copernicus.eu/cdsapp#!/dataset/satellite-sea-ice-thickness) +% datasetOptions.version = "1_0"; +% datasetOptions.variable = "all"; +% datasetOptions.satellite = "cryosat_2"; +% datasetOptions.cdr_type = ["cdr","icdr"]; +% datasetOptions.year = ["2011","2021"]; +% datasetOptions.month = "03"; +% downloadedFilePaths = climateDataStoreDownload('satellite-sea-ice-thickness',datasetOptions); +% +% See Also: climateDataStoreDownloadAsync - validateattributes(name,{'string','char'},{'scalartext'}); - validateattributes(options,'struct',{'scalar'}); +% Copyright 2022 The MathWorks, Inc. - % Diagnostics - setupPythonIfNeeded(); - setupCDSAPIIfNeeded(); + arguments + datasetName (1,1) string {mustBeNonzeroLengthText} + datasetOptions (1,1) struct + options.DontExpandZIP (1,1) logical = false; + options.DontPromptForCredentials (1,1) logical = false; + options.Timeout (1,1) double {mustBePositive} = Inf; + end - - - % Convert the structure from strings to char and cellstr. - options = makeStringsChars(options); - - % Force download option to ZIP - options.format = 'zip'; - - zipfilePath = string(tempname) + ".zip"; - - retrieveFromCDS(name,options,zipfilePath); - - filePaths = string(unzip(zipfilePath,name)'); - citation = "Generated using Copernicus Climate Change Service information " + string(datetime('now','Format','y')); - - % Delete the temporary ZIP file - delete(zipfilePath); -end - -function theStruct = makeStringsChars(theStruct) - fields = string(fieldnames(theStruct)); - for iField = 1:numel(fields) - if isstring(theStruct.(fields(iField))) - if isscalar(theStruct.(fields(iField))) - % Convert scalar strings to char - theStruct.(fields(iField)) = char(theStruct.(fields(iField))); - else - % Convert string arrays to cell array of chars - theStruct.(fields(iField)) = cellstr(theStruct.(fields(iField))); - end - end + f = climateDataStoreDownloadFuture(datasetName, datasetOptions,options); + f.wait(options.Timeout); + if f.State == "failed" + throwAsCaller(f.Error) + elseif f.State ~= "completed" + throwAsCaller(MException("climateDataStore:UnexpectedState","cdsRequestState should be complete, it's %s",f.State)) end + + filePaths = f.OutputArguments{1}; + citation = f.OutputArguments{2}; end diff --git a/climateDataStoreDownloadAsync.m b/climateDataStoreDownloadAsync.m new file mode 100644 index 0000000..6cdd4af --- /dev/null +++ b/climateDataStoreDownloadAsync.m @@ -0,0 +1,61 @@ +function F = climateDataStoreDownloadAsync(datasetName,datasetOptions,options) +%climateDataStoreDownload Queue a data request from Copernicus Climate Data Store +% Queue a data set request with the Copernicus Climate Data Store. (https://cds.climate.copernicus.eu/cdsapp) +% +% Find your dataset at Climate Data Store and click on the "download data" tab. Make your selections for the subset of data you want. Click "show +% API request" at the bottom. +% +% F = climateDataStoreDownloadAsync(datasetName,datasetOptions) queues a request for the data set with the name datasetName, and will also be used +% as the name of the directory or file downloaded files, with a date/time stamp added. datasetOptions is a MATLAB structure matching the python +% structure shown when you choose "Show API request". F is an object you can use to query the state of your request, and cancel the request. Note +% that if you delete F, or allow it to be cleared, your request with CDS will be cancelled. Note that the operation does not take place on a +% seperate thread -- download of files will not take place until you interact with F, either through checking Status or using wait(). +% +% climateDataStoreDownloadAsync(...,DontExpandZIP=true Results that are ZIP files are not automatically expanded. +% climateDataStoreDownloadAsync(...,DontPromptForCredentials=true If no credentials are present, don't request them (intended for tests) +% +% Downloading the files can take some time, depending on how large they are (I've had it take 30 minutes!). You can check on the status of your +% request by visiting https://cds.climate.copernicus.eu/cdsapp#!/yourrequests. For simple/quick requests, you may want to consider using +% climateDataStoreDownload, which is easier to use, but blocks MATLAB. +% +% Note that many requests return immediately. You should check Status property right away. Results may be available. +% +% Notes: +% You must have: +% * python 3.8 installed (https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe) +% * the cdsapi python package (pip3 install cdsapi) +% * Your credentials need to be in a .cdsapirc file in your user directory. See https://cds.climate.copernicus.eu/api-how-to for more info +% * This function relies on the Python API to access the Copernicus Climate Data Store (CDS) (https://github.com/ecmwf/cdsapi) by the European +% Centre for Medium-Range Weather Forecasts (ECMWF) +% +% Example: ERA5 hourly data on pressure levels (https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-era5-pressure-levels) +% datasetName ="reanalysis-era5-pressure-levels"; +% datasetOptions.product_type = "reanalysis"; +% datasetOptions.format = "grib"; +% datasetOptions.year = "2020"; +% datasetOptions.month = "01"; +% datasetOptions.day = "01"; +% datasetOptions.pressure_level = "1"; +% datasetOptions.variable = "divergence"; +% datasetOptions.time = "06:00"; +% F = climateDataStoreDownloadAsync(datasetName, datasetOptions); +% % Run whatever MATLAB code you want in here. +% F.wait(); +% if F.state == "completed" +% downloadedFilePaths = OutputArguments{1}; +% citation = OutputArguments{2}; +% end +% +% See Also: climateDataStoreDownload, climateDataStoreDownloadFuture + +% Copyright 2022 The MathWorks, Inc. + + arguments + datasetName (1,1) string {mustBeNonzeroLengthText} + datasetOptions (1,1) struct + options.DontExpandZIP (1,1) logical = false; + options.DontPromptForCredentials (1,1) logical = false; + end + + F = climateDataStoreDownloadFuture(datasetName,datasetOptions, options); +end diff --git a/climateDataStoreDownloadFuture.m b/climateDataStoreDownloadFuture.m new file mode 100644 index 0000000..e0a2d54 --- /dev/null +++ b/climateDataStoreDownloadFuture.m @@ -0,0 +1,303 @@ +classdef climateDataStoreDownloadFuture < handle +%climateDataStoreDownloadFuture Represents a queued request with the Copernicus Climate Data Store. (https://cds.climate.copernicus.eu/cdsapp) +% +% climateDataStoreDownloadFuture objects are not created directly. Use climateDataStoreDownloadAsync +% +% climateDataStoreDownloadAsync returns this object you can use to query the state of your request, and cancel the request. Note that if you delete +% this object, or allow it to be cleared, your request with CDS will be cancelled. Note that this is not a true Future: the operation does not take +% place on a seperate thread. Download of files will not take place until you interact with the object, either through checking Status or using wait(). +% +% You can check on the status of your request by visiting https://cds.climate.copernicus.eu/cdsapp#!/yourrequests. +% +% The methods and properties are modeled on the Future object in MATLAB +% +% climateDataStoreDownloadFuture methods: +% cancel - Cancel a queued, or running request +% wait - Wait for request to complete, and download results +% +% climateDataStoreDownloadFuture properties: +% CreateDateTime - Date and time at which this request was created +% Error - Request error information (filled in if State is "failed") +% FinishDateTime - Date and time at which this request finished running +% Function - Function to evaluate +% ID - CDS query identifier +% InputArguments - Input arguments to function +% NumOutputArguments - Number of arguments returned by function (filled in if State is "completed") +% OutputArguments - Output arguments from running Function (filled in if State is "completed") +% RunningDuration - The duration of time the request has been running for, if it has started +% StartDateTime - Date and time at which this future request running +% State - Current state of request from CDS (completed|queued|running|failed) +% +% See Also: climateDataStoreDownloadAsync, Future + +% Copyright 2022 The MathWorks, Inc. + + properties (SetAccess=private) + % Date and time at which this request was created + CreateDateTime (1,1) datetime = datetime('now'); + + % Request error information (filled in if State is "failed") + Error MException = MException.empty(); + + % Date and time at which this request finished running + FinishDateTime datetime = datetime.empty(); + + % Function to evaluate + Function function_handle + + % CDS query identifier + ID string = "" + + % Input arguments to function + InputArguments cell + + % Number of arguments returned by function. Filled in if State is "completed" + NumOutputArguments (1,1) double = 0; + + % Output arguments from running Function filled in if State is "completed". filepaths is in element 1, citation in element 2. + OutputArguments cell = {}; + + % The duration of time the request has been running for, if it has started + RunningDuration duration = duration.empty(); + + % Date and time at which this future request running + StartDateTime datetime = datetime.empty(); + + % Current state of request from CDS (completed|queued|running|failed) + State (1,1) string = "unavailable" + end + + methods + function obj = climateDataStoreDownloadFuture(datasetName, datasetOptions,options) + arguments + datasetName (1,1) string {mustBeNonzeroLengthText} + datasetOptions (1,1) struct + options (1,1) struct; + end + + % Diagnostics + setupPythonIfNeeded(); + setupCDSAPIIfNeeded(~options.DontPromptForCredentials); + + % Get the name of the calling function + frames = dbstack(1); + obj.Function = str2func(frames(1).name); + obj.InputArguments = {datasetName,datasetOptions}; + obj.ExpandZip = ~options.DontExpandZIP; + + % Convert the structure from strings to char and cellstr. + datasetOptions = climateDataStoreDownloadFuture.makeStringsChars(datasetOptions); + + obj.CdsapiClient = py.cdsapi.Client(); + + % Don't show the progress information + obj.CdsapiClient.quiet = true; + obj.CdsapiClient.progress = false; + obj.CdsapiClient.wait_until_complete = false; + + obj.StartDateTime = datetime('now'); + try + obj.CdsResult = obj.CdsapiClient.retrieve(datasetName,datasetOptions); + catch e + obj.FinishDateTime = datetime('now'); + if e.identifier == "MATLAB:Python:PyException" + if contains(string(e.message),"name not found") + obj.Error = MException("climateDataStore:NameNotFound","Data Set Name not found"); + else + obj.Error = e; + end + else + obj.Error = e; + end + obj.StateInternal = "failed"; + return + end + reply = obj.CdsResult.reply; + obj.ID = string(reply{'request_id'}); + obj.update(); + end + + function cancel(obj) + % Cancel a queued, or running request + % + % cancel(F) cancels the request associated with F. + + if isempty(obj.CdsResult) + return + end + + obj.update(); + if obj.StateInternal ~= "completed" && obj.StateInternal ~= "failed" + % Deleting the python result object will cancel the request with CDS. + obj.CdsResult = []; + obj.FinishDateTime = datetime('now'); + obj.Error = MException("climateDataStore:RequestCancelled","The request with ID ''%s'' was cancelled.",obj.ID); + obj.StateInternal = "failed"; + end + end + + function wait(obj,timeout) + % Wait for request to complete, and download results + % + % wait(F) waits for the request associated with F to complete or fail, blocking MATLAB execution. On a successful completion, the + % results are downloaded, and OutputArguments property is updated with the path and citation. On failure, the Error property is updated with error + % information. + % wait(..., timeToWait) limits the time to wait to timeToWait seconds. + + arguments + obj + timeout (1,1) double {mustBePositive} = Inf; + end + + if isempty(obj.CdsResult) + return + end + + obj.update(); + waitTimer = tic; + while obj.StateInternal ~= "completed" && obj.StateInternal ~= "failed" && toc(waitTimer) < timeout + drawnow(); + pause(.5); + obj.update(); + end + if obj.StateInternal ~= "completed" && obj.StateInternal ~= "failed" + error("climateDataStore:timeout","The wait operation timed out.") + end + end + end + + % ======================== PROPERTY SET/GET METHODS ======================== + methods + function result = get.OutputArguments(obj) + obj.update(); + result = obj.OutputArguments; + end + + function result = get.RunningDuration(obj) + obj.update(); + if isempty(obj.FinishDateTime) + % If it's still running, calculate elaspsed time + result = datetime('now') - obj.StartDateTime; + else + % If it's done, return the time it took + result = obj.FinishDateTime - obj.StartDateTime; + end + end + + function result = get.State(obj) + obj.update() + result = obj.StateInternal; + end + end + + % ======================== INTERNAL PROPERTIES AND METHODS ======================== + properties (Access=private) + CdsapiClient + CdsResult + areResultsUpdated logical = false; + ExpandZip logical = true; + StateInternal + end + + methods (Access=private) + function update(obj) + if isempty(obj.CdsResult) + return; + end + + try + obj.CdsResult.update; + catch e + obj.FinishDateTime = datetime('now'); + if contains(e.message,"HTTPError: 404") + obj.Error = MException("climateDataStore:RequestDeleted","The request with ID ''%s'' was deleted.",obj.ID); + else + obj.Error = e; + end + obj.StateInternal = "failed"; + return + end + reply = obj.CdsResult.reply; + obj.StateInternal = string(reply{'state'}); + if obj.StateInternal == "completed" + obj.getResultsIfAvailable(); + elseif obj.StateInternal == "failed" + obj.getErrorInfo(); + end + end + + function getResultsIfAvailable(obj) + if isempty(obj.CdsResult) || obj.areResultsUpdated + return + end + + reply = obj.CdsResult.reply; + if reply{'state'} == "completed" + obj.FinishDateTime = datetime('now'); + [~,filenameOnCDS,extOnCDS] = fileparts(string(reply{'location'})); + filenameOnCDS = filenameOnCDS + extOnCDS; + localFilename = obj.InputArguments{1} + "-" + string(obj.FinishDateTime,"yyyyMMddhhmmss"); + downloadedFileName = string(obj.CdsResult.download(filenameOnCDS)); + + % Generate the citation + citation = "Generated using Copernicus Climate Change Service information " + string(datetime('now','Format','y')); + + % check if file exists + if exist(downloadedFileName,"file") ~= 2 + %error + end + + if lower(extOnCDS) == ".zip" && obj.ExpandZip + % Expand the ZIP in a directory + filePaths = string(unzip(downloadedFileName,localFilename)'); + obj.OutputArguments = {filePaths,citation}; + + % Delete the temporary ZIP file + delete(downloadedFileName); + else + % Rename the file to something better + localFilename = localFilename + extOnCDS; + movefile(downloadedFileName,localFilename); + obj.OutputArguments = {localFilename,citation}; + end + obj.NumOutputArguments = 2; + obj.areResultsUpdated = true; + end + end + + function getErrorInfo(obj) + if isempty(obj.CdsResult) || obj.areResultsUpdated + return + end + + obj.FinishDateTime = datetime('now'); + error = obj.CdsResult.reply{'error'}; + if contains(string(error{'message'}),"not valid") + obj.Error = MException("climateDataStore:InvalidRequest",string(error{'reason'})); + else + obj.Error = MException("climateDataStore:UnknownError",string(error{'reason'})); + end + obj.StateInternal = "failed"; + obj.areResultsUpdated = true; + return + end + end + + methods (Access=private, Static) + function theStruct = makeStringsChars(theStruct) + fields = string(fieldnames(theStruct)); + for iField = 1:numel(fields) + if isstring(theStruct.(fields(iField))) + if isscalar(theStruct.(fields(iField))) + % Convert scalar strings to char + theStruct.(fields(iField)) = char(theStruct.(fields(iField))); + else + % Convert string arrays to cell array of chars + theStruct.(fields(iField)) = cellstr(theStruct.(fields(iField))); + end + end + end + end + end +end + diff --git a/doc/GettingStarted.mlx b/doc/GettingStarted.mlx index ea6e92b4e3aedefbd46fd78c4a86c87e94600d46..52846c65947f9f7c892e1fdcea30f4a48ef29ee5 100644 GIT binary patch delta 615 zcmV-t0+{`?`VX!84}gRLgaWh!qV@%yObd@6m#6jvD1Ti~!ypuf@AoSR_msjuL)z{_ zvnx$ZtkHN!=vmiTC;_zFzn9gmPF><9d7ty-aNcaa*Dd-0=bO&tm?bGjU{qH%rq1zG zxgjfze2}J+ZD%0I2k;oJi%F)eP@RJZ*I96(0gs}|c%f{LcOh6osaHGD(kGEHG2C`e z%MhQgrhitdH(3MaX}X{qf~;ha^e7~DG~$!8QlrwoyLJ#%ib4w-Oz@N?jDEAa2?q;T zC(hvo9l&XS6&{k2-1}yv`o2&4>A=PP8GX6GdmO$>8gsmr0$fZG%1(Bn9LN9_iVpF$ z+c|lj-rSZOT=0}FNV+6!Rswmlx8r zAXZ8LbXiP30k`V+0|NpNcOJ}TxBvhEE&%`lC;$MLp!@?Gmk#{{4}Wz|I}d^|5Z?I} zCwp63l|WL;#^yqNjHcqLghx$_#-A@0H7;)7`%dPMu7x{VSDQWyIgLWVzB0$UKWE`) zU63TqRUl`Cz;;SO-Jgu@^x*M`V5Nu4&e+C5DIy^>l-Qzn2!xEyue*O=+55f<0OVUx zDdmDjlqbnz9tp`lx;f9-X-iGhl6|=Mj_4{|U;QIWvS0Rc^?<6t1MnDpc}#f4Th#@t z#@#xV@@35DX}px|s|Aw%`}qmC<@y5(0R^2*3y&X{^!o!|0_^vfpZo(Gmv8(7E&=nG zp8Nwj9CsefWw-zU04@Ol04M+e00000009610002t`In#k0~?nP{R1Bc-}(ap001P| B9VP$( delta 643 zcmZ4ci+}Sk{)QID7N#xCi@vcMDLcu^PGA0wS)=~6-(dp**YAJ1PFElApVG8OCAMJ~ z6KAJVK>0M|T;-(Wn|L$-%b9Ok=%707nr*Skyy!FK%UtCjIA6Xg^tmUIC9J$O*<7_Q zPVWS-o1#|q>TR0?9@(<=JiEH&g5RrihZnz>`EkR+;bcnlm32pMOUzwj@s zk#*MjAwK^5>lgaHG~ddoB{4gYIq<5MR+WH|xzxwN$k%}{@6}p!Wce;+Wj13wW$d>< z^74_r&E3MwKlJoH@ZG_|f70WflW|b({q$P9g3Nlqc=>np+tyDyCYQIAFif>} zH5mxBytg@eYOk)Yp;yr9v$Ky$)b}kr7|L;BmRN7UzNksup0jnv*1L`BxAEFs_<3rU z>zPa;=2?g4X&1*H`F(fNna44%MNS+tZ6YDhl44iyUQo@deRrz+>n#50i5v?PyLq$v z9X$5`EcJgpHTXujV1rEKDGhOxg*AFX^ZXoCe}9-z=saC&mFn*^2dx+D$CPsSujoB| zanhUV`-^uuZ{5Z(p!vmgk3Wmhnngzzefu7E_DD#I&BK`~@nW9e-v1Wge*YIU7b7$^ zy!*`@$NcI$^K^lq%u>_k{xFM7ul>U;#r*LHSV&zrz?+eYfk6Zq4je$N$bb?mKoJlC W1`rV3{|PmP<1e#3+tXjnKrsMGM-j^a diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Up.xml b/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Up.xml deleted file mode 100644 index 9129cce..0000000 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Up.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Ip.xml b/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Ip.xml deleted file mode 100644 index 6f8b013..0000000 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Ip.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nop.xml b/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nop.xml deleted file mode 100644 index 5199d61..0000000 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nop.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4p.xml b/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4p.xml deleted file mode 100644 index 348b373..0000000 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4p.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcp.xml b/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcp.xml deleted file mode 100644 index f450f2a..0000000 --- a/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcp.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/q_SIXYHhcOVj8Ng1kvp0NbWmwy4d.xml b/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/q_SIXYHhcOVj8Ng1kvp0NbWmwy4d.xml index 89348cf..7a6326b 100644 --- a/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/q_SIXYHhcOVj8Ng1kvp0NbWmwy4d.xml +++ b/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/q_SIXYHhcOVj8Ng1kvp0NbWmwy4d.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCId.xml b/resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCId.xml new file mode 100644 index 0000000..5be48de --- /dev/null +++ b/resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCId.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCIp.xml b/resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCIp.xml new file mode 100644 index 0000000..371ee1d --- /dev/null +++ b/resources/project/EEtUlUb-dLAdf0KpMVivaUlztwA/xYnUlcu4Eh0PKXg1-f4AttsdxCIp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nod.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcd.xml similarity index 82% rename from resources/project/BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nod.xml rename to resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcd.xml index 1c0844e..a75f7a8 100644 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/VVjNhDqUv_EfLSA-gIMBB4Hr5nod.xml +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcd.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcp.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcp.xml new file mode 100644 index 0000000..842de6a --- /dev/null +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/5Lk_HLOyBtA13ybsC7_0l8N5sPcp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4d.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgd.xml similarity index 77% rename from resources/project/BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4d.xml rename to resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgd.xml index 89348cf..30f473b 100644 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/i8H86N7Wp9VkJX8VNFw61Ce6OD4d.xml +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgd.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgp.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgp.xml new file mode 100644 index 0000000..5a68234 --- /dev/null +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/NdAIzigXKuGeE8mznIQJtzJigdgp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Ud.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsd.xml similarity index 77% rename from resources/project/BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Ud.xml rename to resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsd.xml index 80b5b16..30f473b 100644 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/6Qs0zy_xL8w5LlCriG1YSKS4c1Ud.xml +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsd.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsp.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsp.xml new file mode 100644 index 0000000..604d8bc --- /dev/null +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/OHJQigMu3q1B5FOfzzAahtJ5SDsp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Id.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAd.xml similarity index 77% rename from resources/project/BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Id.xml rename to resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAd.xml index 80b5b16..30f473b 100644 --- a/resources/project/BWqpowtnvNmi2uu1DaissESSZyg/HijoyS_64DkOOAoyRR4POJTVd5Id.xml +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAd.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAp.xml b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAp.xml new file mode 100644 index 0000000..1e83f5d --- /dev/null +++ b/resources/project/FI0gxbH-PhwjE_riDQGHPyYMHks/p_IQDyqTeBgr4AKHqMhR0WFqDQAp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygp.xml b/resources/project/YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygp.xml deleted file mode 100644 index 200914d..0000000 --- a/resources/project/YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygp.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/2uRqUwgzsMPtKV1vY4JvGEQwdJId.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/2uRqUwgzsMPtKV1vY4JvGEQwdJId.xml index 1c0844e..e74dec3 100644 --- a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/2uRqUwgzsMPtKV1vY4JvGEQwdJId.xml +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/2uRqUwgzsMPtKV1vY4JvGEQwdJId.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcd.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KId.xml similarity index 77% rename from resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcd.xml rename to resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KId.xml index 80b5b16..7a6326b 100644 --- a/resources/project/E2mMq2X73DyjKhlQAouGqrsyLgg/aeJiMMu8h7RomwdS0y3vinxk4bcd.xml +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KId.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KIp.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KIp.xml new file mode 100644 index 0000000..52efd35 --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/7jPt_znhfWPW_xTjFajix6Io0KIp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycd.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycd.xml new file mode 100644 index 0000000..7a6326b --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycp.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycp.xml new file mode 100644 index 0000000..b20565b --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/8JROiIwiztKKAYd8t-TcpJAG_ycp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygd.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksd.xml similarity index 82% rename from resources/project/YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygd.xml rename to resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksd.xml index 1c0844e..a75f7a8 100644 --- a/resources/project/YhuPDDuEWIGe0C07ihHYccefon0/BWqpowtnvNmi2uu1DaissESSZygd.xml +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksd.xml @@ -1,2 +1,2 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksp.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksp.xml new file mode 100644 index 0000000..3c4de0f --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/FI0gxbH-PhwjE_riDQGHPyYMHksp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/MNy1Id1HkwAmZsn0aYjeanlt4i4d.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/MNy1Id1HkwAmZsn0aYjeanlt4i4d.xml index 1c0844e..e74dec3 100644 --- a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/MNy1Id1HkwAmZsn0aYjeanlt4i4d.xml +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/MNy1Id1HkwAmZsn0aYjeanlt4i4d.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TMK4UzWHdRLhy_w-CHt9y11Q8XAd.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TMK4UzWHdRLhy_w-CHt9y11Q8XAd.xml index 1c0844e..e74dec3 100644 --- a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TMK4UzWHdRLhy_w-CHt9y11Q8XAd.xml +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/TMK4UzWHdRLhy_w-CHt9y11Q8XAd.xml @@ -1,2 +1,6 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwd.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwd.xml new file mode 100644 index 0000000..e74dec3 --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwd.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwp.xml b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwp.xml new file mode 100644 index 0000000..769e7c2 --- /dev/null +++ b/resources/project/qaw0eS1zuuY1ar9TdPn1GMfrjbQ/kbMR0mB3PsmvdJiP4KJDMaIADIwp.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/climateDataStoreDownloadTest.m b/test/climateDataStoreDownloadTest.m new file mode 100644 index 0000000..ea391e4 --- /dev/null +++ b/test/climateDataStoreDownloadTest.m @@ -0,0 +1,206 @@ +classdef climateDataStoreDownloadTest < matlab.unittest.TestCase +% Basic tests to check majority of functionality. + + methods(TestClassSetup) + % Shared setup for the entire test class + end + + methods(TestMethodSetup) + % Setup for each test + end + + methods(Test) + % Test methods + + function climateDataStoreDownloadAsyncTest(testCase) + datasetName ="satellite-sea-ice-thickness"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "cryosat_2"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + cdsFuture = climateDataStoreDownloadAsync(datasetName, datasetOptions); + cdsFuture.wait(); + % Validate that the returned class has not changed since test was written + verifyClass(testCase, cdsFuture, ?climateDataStoreDownloadFuture) + verifyEqual(testCase, 11, numel(properties(cdsFuture))) + verifyEqual(testCase, 16, numel(methods(cdsFuture))) + + verifyClass(testCase, cdsFuture.CreateDateTime,?datetime) + + verifyClass(testCase, cdsFuture.Error,?MException) + verifyEmpty(testCase, cdsFuture.Error) + + verifyClass(testCase, cdsFuture.FinishDateTime,?datetime) + + verifyClass(testCase, cdsFuture.Function,?function_handle) + verifyEqual(testCase, cdsFuture.Function,@climateDataStoreDownloadAsync) + + verifyClass(testCase, cdsFuture.ID,?string) + + verifyClass(testCase, cdsFuture.InputArguments, ?cell) + verifyEqual(testCase, cdsFuture.InputArguments, {datasetName, datasetOptions}) + + verifyClass(testCase, cdsFuture.NumOutputArguments,?double) + verifyEqual(testCase, cdsFuture.NumOutputArguments,2) + verifyClass(testCase, cdsFuture.OutputArguments,?cell) + verifyTrue(testCase, exist(cdsFuture.OutputArguments{1},"file") == 2) + [filepath,~,ext] = fileparts(cdsFuture.OutputArguments{1}); + verifyEqual(testCase, ext,".nc") + verifyEqual(testCase, cdsFuture.OutputArguments{2}, "Generated using Copernicus Climate Change Service information " + string(datetime('today'),'yyyy')) + + verifyClass(testCase, cdsFuture.RunningDuration,?duration) + verifyClass(testCase, cdsFuture.StartDateTime,?datetime) + verifyClass(testCase, cdsFuture.State,?string) + verifyEqual(testCase, cdsFuture.State,"completed") + rmdir(filepath,"s") + end + + function climateDataStoreDownloadAsyncTestNoUnzip(testCase) + datasetName ="satellite-sea-ice-thickness"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "cryosat_2"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + cdsFuture = climateDataStoreDownloadAsync(datasetName, datasetOptions,DontExpandZIP=true); + cdsFuture.wait(); + % Validate that the returned class has not changed since test was written + verifyTrue(testCase, exist(cdsFuture.OutputArguments{1},"file") == 2) + [~,~,ext] = fileparts(cdsFuture.OutputArguments{1}); + verifyEqual(testCase, ext,".zip") + delete(cdsFuture.OutputArguments{1}) + end + + function seaIceTest(testCase) + datasetName ="satellite-sea-ice-thickness"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "cryosat_2"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + [downloadedFilePaths,citation] = climateDataStoreDownload(datasetName,datasetOptions); + [filepath,name,ext] = fileparts(downloadedFilePaths); + verifyEqual(testCase, 7,exist(filepath,"dir")) + verifyTrue(testCase, contains(filepath,datasetName)) + verifyEqual(testCase, 2,exist(fullfile(filepath, name + ext), "file")) + verifyEqual(testCase, ".nc", ext) + verifyEqual(testCase, "Generated using Copernicus Climate Change Service information " + string(datetime("now","Format","yyyy")), citation) + rmdir(filepath,"s") + end + + function gribAsyncTest(testCase) + datasetName = "cems-glofas-reforecast"; + datasetOptions.variable = "river_discharge_in_the_last_24_hours"; + datasetOptions.product_type = "control_reforecast"; + datasetOptions.format = "grib"; + datasetOptions.system_version = "version_3_1"; + datasetOptions.hydrological_model = "lisflood"; + datasetOptions.hyear = "2018"; + datasetOptions.hmonth = "january"; + datasetOptions.hday = "03"; + datasetOptions.leadtime_hour = "24"; + datasetOptions.area = ["31","-91","29","-89"]; + + cdsFuture = climateDataStoreDownloadAsync(datasetName, datasetOptions); + try + % This can take a long time. Limit the test to 10 seconds. + cdsFuture.wait(10); + catch + assumeFail(testCase,"Timeout waiting for response"); + return + end + verifyTrue(testCase, exist(cdsFuture.OutputArguments{1},"file") == 2) + [~,~,ext] = fileparts(cdsFuture.OutputArguments{1}); + verifyEqual(testCase, ext,".grib") + verifyEqual(testCase, cdsFuture.OutputArguments{2}, "Generated using Copernicus Climate Change Service information " + string(datetime('today'),'yyyy')) + + verifyEqual(testCase, cdsFuture.State,"completed") + delete(cdsFuture.OutputArguments{1}) + end + + function csvTest(testCase) + datasetName = "insitu-observations-surface-land"; + datasetOptions.time_aggregation = "daily"; + datasetOptions.variable = "accumulated_precipitation"; + datasetOptions.usage_restrictions = "unrestricted"; + datasetOptions.data_quality = "passed"; + datasetOptions.year = "2020"; + datasetOptions.month = "01"; + datasetOptions.day = "01"; + datasetOptions.area = ["31","-91","29","-89"]; + [downloadedFilePaths,citation] = climateDataStoreDownload(datasetName,datasetOptions); + verifyTrue(testCase, exist(downloadedFilePaths(1),"file") == 2) + verifyTrue(testCase, exist(downloadedFilePaths(2),"file") == 2) + [~,~,ext1] = fileparts(downloadedFilePaths(1)); + verifyEqual(testCase, ext1,".csv") + [filepath,~,ext2] = fileparts(downloadedFilePaths(2)); + verifyEqual(testCase, ext2,".txt") + verifyEqual(testCase, citation, "Generated using Copernicus Climate Change Service information " + string(datetime('today'),'yyyy')) + + rmdir(filepath,"s") + end + + function badDatasetNameAsyncTest(testCase) + datasetName ="invalidname"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "cryosat_2"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + cdsFuture = climateDataStoreDownloadAsync(datasetName, datasetOptions); + verifyEqual(testCase, 'climateDataStore:NameNotFound', cdsFuture.Error.identifier) + verifyEqual(testCase, 'Data Set Name not found', cdsFuture.Error.message) + verifyClass(testCase, cdsFuture.FinishDateTime,?datetime) + verifySize (testCase, cdsFuture.FinishDateTime,[1 1]) + verifyEqual(testCase, cdsFuture.NumOutputArguments,0) + verifyEmpty(testCase, cdsFuture.OutputArguments) + verifyEqual(testCase, cdsFuture.State,"failed") + end + + function badDatasetNameTest(testCase) + datasetName ="invalidname"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "cryosat_2"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + failingFunction = @()(climateDataStoreDownload(datasetName, datasetOptions)); + verifyError(testCase,failingFunction,'climateDataStore:NameNotFound') + end + + function badParameterTest(testCase) + datasetName ="satellite-sea-ice-thickness"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "invalidsat"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + failingFunction = @()(climateDataStoreDownload(datasetName, datasetOptions)); + verifyError(testCase,failingFunction,'climateDataStore:InvalidRequest') + end + + function noCredentials(testCase) + %rename the credential file and set up teardown function to restore it + movefile(fullfile(getUserDirectory,".cdsapirc"),fullfile(getUserDirectory,".cdsapirc_renamed")) + addTeardown(testCase,@movefile,fullfile(getUserDirectory,".cdsapirc_renamed"),fullfile(getUserDirectory,".cdsapirc")) + + datasetName ="satellite-sea-ice-thickness"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "invalidsat"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + failingFunction = @()(climateDataStoreDownload(datasetName, datasetOptions,DontPromptForCredentials=true)); + verifyError(testCase,failingFunction,'climateDataStore:needCredentialFile') + end + end + +end \ No newline at end of file diff --git a/test/longTest.m b/test/longTest.m new file mode 100644 index 0000000..161f1e1 --- /dev/null +++ b/test/longTest.m @@ -0,0 +1,59 @@ +classdef longTest < matlab.unittest.TestCase + % Tests that can take a long time to run, if CDS is backed up + + methods(TestClassSetup) + % Shared setup for the entire test class + end + + methods(TestMethodSetup) + % Setup for each test + end + + methods(Test) + % Test methods + + % Can take 25 minutes! + function ERA5FileTest(testCase) + datasetName ="reanalysis-era5-pressure-levels"; + datasetOptions.product_type = "reanalysis"; + datasetOptions.format = "grib"; + datasetOptions.year = "2020"; + datasetOptions.month = "01"; + datasetOptions.day = "01"; + datasetOptions.pressure_level = "1"; + datasetOptions.variable = "divergence"; + datasetOptions.time = "06:00"; + [downloadedFilePaths,citation] = climateDataStoreDownload(datasetName,datasetOptions); + verifyTrue(testCase, exist(downloadedFilePaths,"file") == 2) + [~,~,ext] = fileparts(downloadedFilePaths); + verifyEqual(testCase, ext,".grib") + verifyEqual(testCase, citation, "Generated using Copernicus Climate Change Service information " + string(datetime('today'),'yyyy')) + + delete(downloadedFilePaths) + end + + % Can take 25 minutes! + function ERA5FileAsyncTest(testCase) + datasetName ="reanalysis-era5-pressure-levels"; + datasetOptions.product_type = "reanalysis"; + datasetOptions.format = "grib"; + datasetOptions.year = "2020"; + datasetOptions.month = "01"; + datasetOptions.day = "01"; + datasetOptions.pressure_level = "1"; + datasetOptions.variable = "divergence"; + datasetOptions.time = "06:00"; + + cdsFuture = climateDataStoreDownloadAsync(datasetName, datasetOptions); + cdsFuture.wait(); + verifyTrue(testCase, exist(cdsFuture.OutputArguments{1},"file") == 2) + [~,~,ext] = fileparts(cdsFuture.OutputArguments{1}); + verifyEqual(testCase, ext,".grib") + verifyEqual(testCase, cdsFuture.OutputArguments{2}, "Generated using Copernicus Climate Change Service information " + string(datetime('today'),'yyyy')) + + verifyEqual(testCase, cdsFuture.State,"completed") + delete(cdsFuture.OutputArguments{1}) + end + end + +end \ No newline at end of file diff --git a/test/smokeTest.m b/test/smokeTest.m new file mode 100644 index 0000000..91d78c0 --- /dev/null +++ b/test/smokeTest.m @@ -0,0 +1,33 @@ +classdef smokeTest < matlab.unittest.TestCase +% General purpose test that runs quickly to exercise high percentage of toolbox for use in version testing + + methods(TestClassSetup) + % Shared setup for the entire test class + end + + methods(TestMethodSetup) + % Setup for each test + end + + methods(Test) + + function basicDataRequest(testCase) + datasetName ="satellite-sea-ice-thickness"; + datasetOptions.version = "1_0"; + datasetOptions.variable = "all"; + datasetOptions.satellite = "cryosat_2"; + datasetOptions.cdr_type = ["cdr","icdr"]; + datasetOptions.year = ["2021"]; + datasetOptions.month = "03"; + [downloadedFilePaths,citation] = climateDataStoreDownload(datasetName,datasetOptions); + [filepath,name,ext] = fileparts(downloadedFilePaths(1)); + verifyEqual(testCase, 7,exist(filepath,"dir")) + verifyTrue(testCase, contains(filepath, datasetName)) + verifyEqual(testCase, 2,exist(fullfile(filepath, name + ext), "file")) + verifyEqual(testCase, ".nc", ext) + verifyEqual(testCase, "Generated using Copernicus Climate Change Service information " + string(datetime("now","Format","yyyy")), citation) + rmdir(filepath,"s") + end + end + +end \ No newline at end of file diff --git a/util/retrieveFromCDS.m b/util/retrieveFromCDS.m deleted file mode 100644 index f96fa8e..0000000 --- a/util/retrieveFromCDS.m +++ /dev/null @@ -1,12 +0,0 @@ -function retrieveFromCDS(name,options,zipfilePath) -% Utility function to isolate python code so that we don't trigger the -% python check until after python install checks are done. - -% Copyright 2021 The MathWorks, Inc. - c = py.cdsapi.Client(); - - % Don't show the progress information - c.quiet = true; - c.progress = false; - c.retrieve(name,options,zipfilePath); -end \ No newline at end of file diff --git a/util/setupCDSAPIIfNeeded.m b/util/setupCDSAPIIfNeeded.m index f579689..005ad04 100644 --- a/util/setupCDSAPIIfNeeded.m +++ b/util/setupCDSAPIIfNeeded.m @@ -1,13 +1,17 @@ -function setupCDSAPIIfNeeded -%SETUPCDSAPIIFNEEDED Install CSAPI module if it is not installed +function setupCDSAPIIfNeeded(promptForCredentials) +%SETUPCDSAPIIFNEEDED Install CSAPI module if it is not installed. Get credentials if no .cdsapirc file does not exist. Visit +%https://cds.climate.copernicus.eu/api-how-to for more info on credentials. -% Copyright 2021 The MathWorks, Inc. +% Copyright 2022 The MathWorks, Inc. +arguments + promptForCredentials (1,1) logical = true; +end pythonInfo = pyenv; if pythonInfo.Version == "" % Python not set up - error("setupCDSAPI:pythonNotInstalled","Python not installed.") + error("climateDataStore:pythonNotInstalled","Python not installed.") end % Call PIP to see if cdsapi is installed @@ -16,7 +20,7 @@ % it's not installed [result, ~] = system(fullfile(pythonInfo.Home,"Scripts","pip3")+" install cdsapi"); if result ~= 0 - error("setupCDSAPI:unableToInstall","Could not use PIP3 to install CDSAPI") + error("climateDataStore:unableToInstallCSAPI","Could not use PIP3 to install CDSAPI") end end @@ -24,15 +28,19 @@ cdsCredentialPath = fullfile(getUserDirectory(),".cdsapirc"); if ~exist(cdsCredentialPath,'file') - % Prompt user for credentials - prompt = {'url','key'}; - dlgtitle = 'Enter your CDS credentials (visit https://cds.climate.copernicus.eu/api-how-to)'; - dims = [1 35]; - definput = {'https://cds.climate.copernicus.eu/api/v2',''}; - answer = inputdlg(prompt,dlgtitle,dims,definput); - + + answer = []; + if promptForCredentials + % Prompt user for credentials + prompt = {'url','key'}; + dlgtitle = 'Enter your CDS credentials (visit https://cds.climate.copernicus.eu/api-how-to)'; + dims = [1 35]; + definput = {'https://cds.climate.copernicus.eu/api/v2',''}; + answer = inputdlg(prompt,dlgtitle,dims,definput); + end + if isempty(answer) - error("setupCDSAPI:needCredentialFile","You must create a credential file. visit https://cds.climate.copernicus.eu/api-how-to for more info.") + error("climateDataStore:needCredentialFile","You must create a credential file. Visit https://cds.climate.copernicus.eu/api-how-to for more info.") end url = string(answer{1}); @@ -42,7 +50,7 @@ fileContents = "url: " + url + newline + "key: " + key + newline; [fid,errmsg] = fopen(cdsCredentialPath,"wt","n","UTF-8"); if fid == -1 - error("setupCDSAPI:cannotCreateCredentials","Cannot create a credential file (%s). %s", cdsCredentialPath, errmsg) + error("climateDataStore:cannotCreateCredentials","Cannot create a credential file (%s). %s", cdsCredentialPath, errmsg) end fwrite(fid,fileContents); fclose(fid);