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

Danlooo/develop #18

Merged
merged 11 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -11,6 +11,8 @@ HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Infiltrator = "5903a43b-9cc3-4c30-8d17-598619ec4e9b"
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"

[compat]
Expand Down
4 changes: 4 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[deps]
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
OpenEOClient = "453781fe-a3c2-4c28-a19a-0c7c3d89805f"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Rasters = "a3a2b9e3-a471-40c9-b274-f788e487c689"
ZipFile = "a5390f91-8eb1-5f08-bee0-b1d1ffed6cea"
2 changes: 1 addition & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ makedocs(;
assets=String[]
),
pages=[
"Home" => "index.md",
"Home" => "index.md"
]
)

Expand Down
38 changes: 38 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,44 @@ CurrentModule = OpenEOClient

Documentation for [OpenEOClient](https://github.com/Open-EO/openeo-julia-client).


# Tutorial

Processing data on a openEO server requires authentication.


```@example tutorial
using OpenEOClient
username = ENV["username"]
password = ENV["password"]
c = connect("earthengine.openeo.org", "v1.0", username, password)
step1 = c.load_collection(
"COPERNICUS/S2", BoundingBox(west=16.06, south=48.06, east=16.65, north=48.35),
["2020-01-01", "2020-01-31"], ["B10"]
)
step2 = c.reduce_dimension(step1, Reducer("median"), "t", nothing)
step3 = c.save_result(step2, "GTIFF-ZIP", Dict())
path = c.compute_result(step3)
```

The data is downloaded in zipped GeoTiff format.
It can be loaded into a local Julia session using [Rasters.jl](https://rafaqz.github.io/Rasters.jl/stable/):

```@example tutorial
using ZipFile, Rasters, Plots, ArchGDAL

f = ZipFile.Reader(path).files[1]
write(open(f.name, "w"), read(f, String))

cube = Raster(f.name)
```

Plotting:

```@example tutorial
plot(cube)
```

```@index
```

Expand Down
7 changes: 6 additions & 1 deletion src/API.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ end
Lists all information about a specific collection specified by the identifier
"""
function describe_collection(connection::AbstractConnection, id::String)
# TODO: parse to collection type
response = fetchApi(connection, "collections/$(id)")
response isa Exception ? throw(response) : true
return response
Expand All @@ -44,4 +45,8 @@ Base.@kwdef struct BoundingBox{T<:Real}
east::T
north::T
end
StructTypes.StructType(::Type{BoundingBox}) = StructTypes.Struct()
StructTypes.StructType(::Type{BoundingBox}) = StructTypes.Struct()

function print_json(x)
x |> JSON3.write |> JSON3.read |> JSON3.pretty
end
8 changes: 4 additions & 4 deletions src/Collections.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import StructTypes

struct Collection
stac_version::String
stac_extensions::Vector{Any}
type::String
stac_extensions::Any
type::Union{String,Nothing}
id::String
title::String
title::Union{String,Nothing}
description::String
license::String
providers::Vector{Any}
providers::Any
extent::Any
links::Vector{Any}
end
Expand Down
130 changes: 107 additions & 23 deletions src/Connections.jl
Original file line number Diff line number Diff line change
@@ -1,23 +1,92 @@
using HTTP
using JSON3
import SHA
import Random
import Base64
import HTTP
import JSON3

"OpenID Connect device flow + PKCE"
function get_acces_token(discovery_url::String, client_id::String, scopes::AbstractVector)
oidc_config = HTTP.get(discovery_url).body |> JSON3.read

code_verifier = Random.randstring(128)
hash = SHA.sha256(code_verifier)
# see https://datatracker.ietf.org/doc/html/rfc7636#appendix-B
code_challenge = hash |> Base64.base64encode |> x -> strip(x, '=') |> x -> replace(x, "+" => "-", "/" => "_")

headers = ["content-type" => "application/x-www-form-urlencoded"]
args = Dict(
"client_id" => client_id,
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
"code_challenge_method" => "S256",
"code_challenge" => code_challenge,
"scope" => join(scopes, " ") # EDIT: Use space instead of ','
)
body = HTTP.URIs.escapeuri(args)
device_code_request = HTTP.post(oidc_config.device_authorization_endpoint, headers, body) |> x -> x.body |> String |> JSON3.read

@info "Please log in using any device at:\n" *
device_code_request.verification_uri_complete *
"\nWaiting until log in succeeded..."

while true
headers = ["content-type" => "application/x-www-form-urlencoded"]
args = Dict(
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
"device_code" => device_code_request.device_code,
"client_id" => client_id,
"code_verifier" => code_verifier
)
body = HTTP.URIs.escapeuri(args)

try
access_token = HTTP.post(oidc_config.token_endpoint, headers, body) |>
x -> x.body |> String |> JSON3.read |> x -> x.access_token
return access_token
catch e
sleep(device_code_request.interval)
end
end
end

abstract type AbstractConnection end
abstract type AuthorizedConnection <: AbstractConnection end

struct UnAuthorizedConnection <: AbstractConnection
host::String
version::String
end

struct BasicAuthConnection <: AuthorizedConnection
Base.show(io::IO, c::UnAuthorizedConnection) = print(io, "unauthorized openEO connection to https://$(c.host)/$(c.version)")

struct AuthorizedConnection <: AbstractConnection
host::String
version::String
access_token::Union{String,Nothing}
authorization::String
end

Base.show(io::IO, c::AuthorizedConnection) = print(io, "authorized openEO connection to https://$(c.host)/$(c.version)")

"HTTP basic authentification"
function AuthorizedConnection(host, version, username, password)
access_response = fetchApi("https://$(username):$(password)@$(host)/$(version)/credentials/basic")
access_token = access_response.access_token
access_token = get_access_token(host, version, username, password)
authorization = "Bearer basic//$access_token"
AuthorizedConnection(host, version, authorization)
end

struct OpenIDConnection <: AuthorizedConnection
"OpenID Connect device flow + PKCE authentification"
function AuthorizedConnection(host, version)
provider = fetchApi("https://$(host)/$(version)/credentials/oidc").providers[1]
discovery_url = "$(provider.issuer).well-known/openid-configuration"
client_id = provider.default_clients[1].id
scopes = provider.scopes
access_token = get_acces_token(discovery_url, client_id, scopes)
authorization = "Bearer oidc/$(provider.id)/$access_token"
AuthorizedConnection(host, version, authorization)
end

@enum AuthMethod no_auth basic_auth oidc_auth

const default_headers = [
"Accept" => "application/json",
"Content-Type" => "application/json"
Expand All @@ -31,14 +100,13 @@ function fetchApi(url; method="GET", headers=deepcopy(default_headers), output_t
response_type = Dict(response.headers)["Content-Type"]
if response_type == "application/json"
response_string = String(response.body)
response_converted = JSON3.read(response_string, output_type)
response_converted = output_type == Any ? JSON3.read(response_string) : JSON3.read(response_string, output_type)
return response_converted
else
return response
end
catch e
msg = e.response.body |> String |> JSON3.read |> x -> x.message
return ErrorException(msg)
@error e
end
end

Expand All @@ -50,44 +118,60 @@ end

function fetchApi(connection::AuthorizedConnection, path::String; headers=deepcopy(default_headers), kw...)
url = "https://$(connection.host)/$(connection.version)/$(path)"
append!(headers, ["Authorization" => "Bearer basic//$(connection.access_token)"])
append!(headers, ["Authorization" => connection.authorization])
response = fetchApi(url; headers=headers, kw...)
return response
end

function connect(host, version)
function connect(host, version, auth_method::AuthMethod=no_auth)
processes_code = get_processes_code(host, version)
global n_existing_connections += 1
module_str = """
module Connection$(n_existing_connections)
using OpenEOClient
const connection = OpenEOClient.UnAuthorizedConnection("$host", "$version")
const collections = OpenEOClient.list_collections(connection)
const processes = OpenEOClient.list_processes(connection)

$processes_code
if auth_method == no_auth
module_str = """
module Connection$(n_existing_connections)
using OpenEOClient
const connection = OpenEOClient.UnAuthorizedConnection("$host", "$version")
const collections = OpenEOClient.list_collections(connection)
const processes = OpenEOClient.list_processes(connection)

$processes_code
end
"""
elseif auth_method == oidc_auth
module_str = """
module Connection$(n_existing_connections)
using OpenEOClient
const connection = OpenEOClient.AuthorizedConnection("$host", "$version")
const collections = OpenEOClient.list_collections(connection)
const processes = OpenEOClient.list_processes(connection)
compute_result(p) = OpenEOClient.compute_result(connection, p)

$processes_code
end
"""
end
"""

global n_existing_connections += 1
eval(Meta.parse(module_str))
end

function connect(host, version::String, username::String, password::String)
access_response = fetchApi("https://$(username):$(password)@$(host)/$(version)/credentials/basic")
access_token = access_response["access_token"]

processes_code = get_processes_code(host, version)
global n_existing_connections += 1

module_str = """
module Connection$(n_existing_connections)
using OpenEOClient
const connection = OpenEOClient.BasicAuthConnection("$host", "$version", "$access_token")
const connection = OpenEOClient.AuthorizedConnection("$host", "$version", "Bearer basic//$access_token")
const collections = OpenEOClient.list_collections(connection)
const processes = OpenEOClient.list_processes(connection)
compute_result(p) = OpenEOClient.compute_result(connection, p)

$processes_code
end
"""

global n_existing_connections += 1
eval(Meta.parse(module_str))
end
2 changes: 1 addition & 1 deletion src/OpenEOClient.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export
ProcessNode,
Processes,
ProcessGraph,
Reducer,
ProcessGraph,
BoundingBox,
compute_result
end
Loading
Loading