Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
14ef047
adds scoped values credential to support multiple endpoints.
ghyatzo Jan 15, 2025
bc31426
fix naming
ghyatzo Jan 15, 2025
3cfd2d5
clearer check
ghyatzo Jan 15, 2025
559d662
rename CDScredentials
ghyatzo Jan 15, 2025
8cc75b7
WIP: for now adding compatibility for 1.11 only.
ghyatzo Jan 15, 2025
41c6caf
fix: better docs of credentials, moved the check inside the credentials.
ghyatzo Jan 15, 2025
dcb997e
better docstring
ghyatzo Jan 15, 2025
2c13a25
add logic for ScopedValues compat
ghyatzo Jan 15, 2025
94bc65d
fix wrong string interpolation of `$HOME`
ghyatzo Jan 15, 2025
70e812f
fix: logic error. auth[] is valid only if both entries are non empty.
ghyatzo Jan 15, 2025
3a52887
fix: even better logic. clearer.
ghyatzo Jan 15, 2025
664ec81
add ScopedValues as a direct Dependency
ghyatzo Jan 16, 2025
20358b0
fix: leftover
ghyatzo Jan 16, 2025
d4f10d7
added README entry about multiple token use with examples.
ghyatzo Jan 16, 2025
361b3d6
export with from ScopedValues, so it is available when `using CDSAPI`
ghyatzo Jan 16, 2025
cc27e08
added tests
ghyatzo Jan 16, 2025
289a848
better ScopedValue versioning
ghyatzo Feb 12, 2025
d920c64
Documentation fixes
ghyatzo Feb 12, 2025
913ce2b
don't export with
ghyatzo Feb 12, 2025
7965bbb
added better explaination of the various priorities for default crede…
ghyatzo Feb 12, 2025
4e25dbf
split the auth scoped value into key and url.
ghyatzo Feb 14, 2025
f2bb4f4
update README with new syntax for scoped values
ghyatzo Feb 14, 2025
0ed846d
clarified the readme and removed references to `credentialsfromfile`
ghyatzo Feb 19, 2025
0620f52
Update README.md
ghyatzo Feb 20, 2025
aca5093
Update README.md
ghyatzo Feb 20, 2025
359a846
default url as based, fix tests, fix leftover
ghyatzo Feb 20, 2025
6dd7238
Merge branch 'master' into scoped-auth
ghyatzo Feb 20, 2025
f6f54f6
typos and whitespace
ghyatzo Feb 21, 2025
a44d217
revert default url in scoped value.
ghyatzo Feb 21, 2025
e0baca5
Apply suggestions from code review
ghyatzo Feb 21, 2025
58bc951
Final adjustments
juliohm Feb 24, 2025
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
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ version = "2.0.3"
[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63"

[compat]
HTTP = "1"
JSON = "0.21"
ScopedValues = "1.3.0"
julia = "1"

[extras]
Expand Down
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@ Please install the package with Julia's package manager:
] add CDSAPI
```

## Usage
## Basic usage
`CDSAPI.jl` will attempt to use CDS credentials using three different methods with the following priority:

Make sure your `~/.cdsapirc` file exists. Instructions on how to create the file for your user account can be found
1. direct credentials provided through the scoped values `KEY` and `URL`
2. environmental variables `CDSAPI_URL` and `CDSAPI_KEY`
3. default credential file in home directory `~/.cdsapirc`

A valid credential file is a text file with two lines:
```
url: https://yourendpoint
key: your-personal-api-token
```
Instructions on how to create the file for your user account can be found
[here](https://cds.climate.copernicus.eu/how-to-api).

Suppose that the `Show API request` button generated the following Python code:
For the following example to work, make sure your `~/.cdsapirc` file exists or the env vars `CDSAPI_URL` and `CDSAPI_KEY` are set.

Suppose that the `Show API request` button generated the following Python code:
```python
#!/usr/bin/env python
import cdsapi
Expand All @@ -48,7 +59,6 @@ client.retrieve(dataset, request).download()
```

You can obtain the same results in Julia:

```julia
using CDSAPI

Expand Down Expand Up @@ -82,6 +92,33 @@ Dict{String,Any} with 6 entries:
"content_length" => 193660
"state" => "completed"
```
# Multiple credentials
In case you want to use multiple api-tokens for different requests, you can specify the token to use with each different request.

Pass the desired values to the corresponding scoped values `CDSAPI.URL` and `CDSAPI.KEY`:
If the `URL` or the `KEY` are not specified, the default values obtained from the default methods are used.
```julia
using CDSAPI

dataset = "reanalysis-era5-single-levels"
request = """ #= some request =# """

customkey = "an-example-of-key"
customurl = "http://my-custom-endpoint"

# In this case the `URL` argument will be taken from the default locations
# and only the KEY is overwritten.
CDSAPI.with( CDSAPI.KEY => customkey ) do
CDSAPI.retrieve(dataset, request, "download.nc")
end

# In this case is the other way around, so all requests will be made with the
# credentials (customurl, defaultkey)
CDSAPI.with( CDSAPI.URL => customurl) do
CDSAPI.retrieve(dataset, request, "download.nc")
end
```


[build-img]: https://img.shields.io/github/actions/workflow/status/JuliaClimate/CDSAPI.jl/CI.yml?branch=master&style=flat-square
[build-url]: https://github.com/JuliaClimate/CDSAPI.jl/actions
Expand Down
91 changes: 80 additions & 11 deletions src/CDSAPI.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,80 @@ module CDSAPI
using HTTP
using JSON

using ScopedValues

const KEY = ScopedValue("")
const URL = ScopedValue("")

"""
credentials()

Attempt to find CDS credentials using different methods:

1. direct credentials provided via a specific file
2. environmental variables `CDSAPI_URL` and `CDSAPI_KEY`
3. credential file in home directory `~/.cdsapirc`

A credential file is a text file with two lines:

url: https://yourendpoint
key: your-personal-api-token
"""
function credentials()

dotrc = joinpath(homedir(), ".cdsapirc")
if isfile(dotrc)
url, key = credentialsfromfile(dotrc)
else
url = key = ""
end

# overwrite with environmental variables
url = get(ENV, "CDSAPI_URL", url)
key = get(ENV, "CDSAPI_KEY", key)

# overwrite with ScopedValues provided by user

url = isempty(URL[]) ? url : URL[]
key = isempty(KEY[]) ? key : KEY[]


if isempty(url) || isempty(key)
error("""
Missing credentials. Either add the CDSAPI_URL and CDSAPI_KEY env variables
or create a .cdsapirc file (default location: '$(homedir())').
""")
end

return url, key
end

"""
credentials(file)

Parse the cds credentials from a provided file
"""
function credentialsfromfile(file)
creds = Dict()
open(realpath(file)) do f
for line in readlines(f)
key, val = strip.(split(line, ':', limit=2))
creds[key] = val
end
end

if !(haskey(creds, "url") || haskey(creds, "key")) # we can allow files with only one of the keys.
error("""
The credentials' file must have at least a `key` value or a `url` value in the following format:

url: https://yourendpoint
key: your-personal-api-token
""")
end

return get(creds, "url", ""), get(creds, "key", "")
end

"""
retrieve(name, params, filename; wait=1.0)

Expand All @@ -19,18 +93,13 @@ retrieve(name, params::AbstractString, filename; wait=1.0) =
# CDSAPI.parse can be used to convert the request params into a
# Julia dictionary for additional manipulation before retrieval
function retrieve(name, params::AbstractDict, filename; wait=1.0)
creds = Dict()
open(joinpath(homedir(), ".cdsapirc")) do f
for line in readlines(f)
key, val = strip.(split(line, ':', limit=2))
creds[key] = val
end
end

_url, _key = credentials()

try
response = HTTP.request("POST",
creds["url"] * "/retrieve/v1/processes/$name/execute",
["PRIVATE-TOKEN" => creds["key"]],
_url * "/retrieve/v1/processes/$name/execute",
["PRIVATE-TOKEN" => _key],
body=JSON.json(Dict("inputs" => params))
)
catch e
Expand All @@ -54,7 +123,7 @@ function retrieve(name, params::AbstractDict, filename; wait=1.0)

while data["status"] != "successful"
data = HTTP.request("GET", endpoint,
["PRIVATE-TOKEN" => creds["key"]]
["PRIVATE-TOKEN" => _key]
)
data = JSON.parse(String(data.body))
@info "CDS request" dataset=name status=data["status"]
Expand All @@ -75,7 +144,7 @@ function retrieve(name, params::AbstractDict, filename; wait=1.0)

response = HTTP.request("GET",
endpoint * "/results",
["PRIVATE-TOKEN" => creds["key"]]
["PRIVATE-TOKEN" => _key]
)
body = JSON.parse(String(response.body))
HTTP.download(body["asset"]["value"]["href"], filename)
Expand Down
22 changes: 22 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,26 @@ datadir = joinpath(@__DIR__, "data")
@test_throws ArgumentError CDSAPI.retrieve(goodname, badrequest, "unreachable")
@test_throws ArgumentError CDSAPI.retrieve(badname, badrequest, "unreachable")
end

@testset "Credentials with scoped values" begin
filename = joinpath(datadir, "sea_ice_type.zip")
dataset = "satellite-sea-ice-edge-type"
request = """{
"variable": "sea_ice_type",
"region": "northern_hemisphere",
"cdr_type": "cdr",
"year": "1979",
"month": "01",
"day": "02",
"version": "3_0",
"data_format": "zip"
}"""

creds = CDSAPI.credentials() # grab the default ones
with( CDSAPI.auth => creds ) do
response = CDSAPI.retrieve(dataset, request, filename)
end

rm(filename)
end
end
Loading