Skip to content

Commit b48d3ed

Browse files
ghyatzojuliohm
andauthored
adds scoped values credential to support multiple endpoints. (#58)
* adds scoped values credential to support multiple endpoints. * fix naming Co-authored-by: Júlio Hoffimann <[email protected]> * clearer check Co-authored-by: Júlio Hoffimann <[email protected]> * rename CDScredentials Co-authored-by: Júlio Hoffimann <[email protected]> * WIP: for now adding compatibility for 1.11 only. * fix: better docs of credentials, moved the check inside the credentials. and various renames * better docstring Co-authored-by: Júlio Hoffimann <[email protected]> * add logic for ScopedValues compat * fix wrong string interpolation of `$HOME` * fix: logic error. auth[] is valid only if both entries are non empty. * fix: even better logic. clearer. * add ScopedValues as a direct Dependency * fix: leftover * added README entry about multiple token use with examples. * export with from ScopedValues, so it is available when `using CDSAPI` * added tests * better ScopedValue versioning Co-authored-by: Júlio Hoffimann <[email protected]> * Documentation fixes Co-authored-by: Júlio Hoffimann <[email protected]> * don't export with Co-authored-by: Júlio Hoffimann <[email protected]> * added better explaination of the various priorities for default credentials * split the auth scoped value into key and url. Properly let the various cred inputs overwrite each other. * update README with new syntax for scoped values * clarified the readme and removed references to `credentialsfromfile` renamed the scoped values to the uppercase variant * Update README.md Co-authored-by: Júlio Hoffimann <[email protected]> * Update README.md Co-authored-by: Júlio Hoffimann <[email protected]> * default url as based, fix tests, fix leftover * typos and whitespace Co-authored-by: Júlio Hoffimann <[email protected]> * revert default url in scoped value. Enforce presence of key and url in file. * Apply suggestions from code review Co-authored-by: Júlio Hoffimann <[email protected]> * Final adjustments --------- Co-authored-by: Júlio Hoffimann <[email protected]>
1 parent 3a9b429 commit b48d3ed

File tree

4 files changed

+139
-15
lines changed

4 files changed

+139
-15
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ version = "2.0.4"
77
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
88
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
99
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
10+
ScopedValues = "7e506255-f358-4e82-b7e4-beb19740aa63"
1011

1112
[compat]
1213
Dates = "1.11"
1314
HTTP = "1"
1415
JSON = "0.21"
16+
ScopedValues = "1.3.0"
1517
julia = "1"
1618

1719
[extras]

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,26 @@ Please install the package with Julia's package manager:
1616
] add CDSAPI
1717
```
1818

19-
## Usage
19+
## Basic usage
2020

21-
Make sure your `~/.cdsapirc` file exists. Instructions on how to create the file for your user account can be found
21+
The package will attempt to use CDS credentials using three different methods with the following priority:
22+
23+
1. direct credentials provided through the scoped values `CDSAPI.KEY` and `CDSAPI.URL`
24+
2. environmental variables `CDSAPI_URL` and `CDSAPI_KEY`
25+
3. default credential file in home directory `~/.cdsapirc`
26+
27+
A valid credential file is a text file with two lines:
28+
```
29+
url: https://yourendpoint
30+
key: your-personal-api-token
31+
```
32+
33+
Instructions on how to create the file for your user account can be found
2234
[here](https://cds.climate.copernicus.eu/how-to-api).
2335

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

38+
Suppose that the `Show API request` button generated the following Python code:
2639
```python
2740
#!/usr/bin/env python
2841
import cdsapi
@@ -48,7 +61,6 @@ client.retrieve(dataset, request).download()
4861
```
4962

5063
You can obtain the same results in Julia:
51-
5264
```julia
5365
using CDSAPI
5466

@@ -82,6 +94,28 @@ Dict{String,Any} with 6 entries:
8294
"content_length" => 193660
8395
"state" => "completed"
8496
```
97+
# Multiple credentials
98+
99+
In case you want to use multiple credentials for different requests, pass the desired values to the corresponding scoped values `CDSAPI.URL` and `CDSAPI.KEY`:
100+
```julia
101+
using CDSAPI
102+
103+
dataset = "reanalysis-era5-single-levels"
104+
request = """ #= some request =# """
105+
106+
customkey = "an-example-of-key"
107+
customurl = "http://my-custom-endpoint"
108+
109+
# overwrite KEY and use URL from other methods
110+
CDSAPI.with(CDSAPI.KEY => customkey) do
111+
CDSAPI.retrieve(dataset, request, "download.nc")
112+
end
113+
114+
# overwrite URL and use KEY from other methods
115+
CDSAPI.with(CDSAPI.URL => customurl) do
116+
CDSAPI.retrieve(dataset, request, "download.nc")
117+
end
118+
```
85119

86120
[build-img]: https://img.shields.io/github/actions/workflow/status/JuliaClimate/CDSAPI.jl/CI.yml?branch=master&style=flat-square
87121
[build-url]: https://github.com/JuliaClimate/CDSAPI.jl/actions

src/CDSAPI.jl

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,78 @@ using HTTP
44
using JSON
55
using Dates
66

7+
using ScopedValues
8+
9+
const URL = ScopedValue("")
10+
const KEY = ScopedValue("")
11+
12+
"""
13+
credentials()
14+
15+
Attempt to find CDS credentials using different methods:
16+
17+
1. direct credentials provided through the scoped values `KEY` and `URL`
18+
2. environmental variables `CDSAPI_URL` and `CDSAPI_KEY`
19+
3. credential file in home directory `~/.cdsapirc`
20+
21+
A credential file is a text file with two lines:
22+
23+
url: https://yourendpoint
24+
key: your-personal-api-token
25+
"""
26+
function credentials()
27+
# attempt to retrieve url/key from dotfile
28+
dotrc = joinpath(homedir(), ".cdsapirc")
29+
if isfile(dotrc)
30+
url, key = credentialsfromfile(dotrc)
31+
else
32+
url = key = ""
33+
end
34+
35+
# overwrite with env values
36+
url = get(ENV, "CDSAPI_URL", url)
37+
key = get(ENV, "CDSAPI_KEY", key)
38+
39+
# overwrite with scoped values
40+
url = isempty(URL[]) ? url : URL[]
41+
key = isempty(KEY[]) ? key : KEY[]
42+
43+
if isempty(url) || isempty(key)
44+
error("""
45+
Missing credentials. Either add the CDSAPI_URL and CDSAPI_KEY env variables
46+
or create a .cdsapirc file (default location: '$(homedir())').
47+
""")
48+
end
49+
50+
return url, key
51+
end
52+
53+
"""
54+
credentials(file)
55+
56+
Parse the CDS credentials from a provided `file`.
57+
"""
58+
function credentialsfromfile(file)
59+
creds = Dict()
60+
open(realpath(file)) do f
61+
for line in readlines(f)
62+
key, val = strip.(split(line, ':', limit=2))
63+
creds[key] = val
64+
end
65+
end
66+
67+
if !(haskey(creds, "url") && haskey(creds, "key"))
68+
error("""
69+
The credentials' file must have both a `url` value and a `key` value in the following format:
70+
71+
url: https://yourendpoint
72+
key: your-personal-api-token
73+
""")
74+
end
75+
76+
return get(creds, "url", ""), get(creds, "key", "")
77+
end
78+
779
"""
880
retrieve(name, params, filename; wait=1.0)
981
@@ -20,18 +92,12 @@ retrieve(name, params::AbstractString, filename; wait=1.0) =
2092
# CDSAPI.parse can be used to convert the request params into a
2193
# Julia dictionary for additional manipulation before retrieval
2294
function retrieve(name, params::AbstractDict, filename; wait=1.0)
23-
creds = Dict()
24-
open(joinpath(homedir(), ".cdsapirc")) do f
25-
for line in readlines(f)
26-
key, val = strip.(split(line, ':', limit=2))
27-
creds[key] = val
28-
end
29-
end
95+
url, key = credentials()
3096

3197
try
3298
response = HTTP.request("POST",
33-
creds["url"] * "/retrieve/v1/processes/$name/execute",
34-
["PRIVATE-TOKEN" => creds["key"]],
99+
url * "/retrieve/v1/processes/$name/execute",
100+
["PRIVATE-TOKEN" => key],
35101
body=JSON.json(Dict("inputs" => params))
36102
)
37103
catch e
@@ -56,7 +122,7 @@ function retrieve(name, params::AbstractDict, filename; wait=1.0)
56122
laststatus = nothing
57123
while data["status"] != "successful"
58124
data = HTTP.request("GET", endpoint,
59-
["PRIVATE-TOKEN" => creds["key"]]
125+
["PRIVATE-TOKEN" => key]
60126
)
61127
data = JSON.parse(String(data.body))
62128

@@ -81,7 +147,7 @@ function retrieve(name, params::AbstractDict, filename; wait=1.0)
81147

82148
response = HTTP.request("GET",
83149
endpoint * "/results",
84-
["PRIVATE-TOKEN" => creds["key"]]
150+
["PRIVATE-TOKEN" => key]
85151
)
86152
body = JSON.parse(String(response.body))
87153

test/runtests.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,26 @@ datadir = joinpath(@__DIR__, "data")
8585
@test_throws ArgumentError CDSAPI.retrieve(goodname, badrequest, "unreachable")
8686
@test_throws ArgumentError CDSAPI.retrieve(badname, badrequest, "unreachable")
8787
end
88+
89+
@testset "Credentials with scoped values" begin
90+
filename = joinpath(datadir, "sea_ice_type.zip")
91+
dataset = "satellite-sea-ice-edge-type"
92+
request = """{
93+
"variable": "sea_ice_type",
94+
"region": "northern_hemisphere",
95+
"cdr_type": "cdr",
96+
"year": "1979",
97+
"month": "01",
98+
"day": "02",
99+
"version": "3_0",
100+
"data_format": "zip"
101+
}"""
102+
103+
url, key = CDSAPI.credentials() # grab the default ones
104+
CDSAPI.with(CDSAPI.URL => url, CDSAPI.KEY => key) do
105+
response = CDSAPI.retrieve(dataset, request, filename)
106+
end
107+
108+
rm(filename)
109+
end
88110
end

0 commit comments

Comments
 (0)