|
| 1 | +module PlotlyV2 |
| 2 | + |
| 3 | +# ------------- # |
| 4 | +# Imports/Setup # |
| 5 | +# ------------- # |
| 6 | + |
| 7 | +using Plotly, JSON, Requests |
| 8 | + |
| 9 | +const API_ROOT = "https://api.plot.ly/v2/" |
| 10 | +const VERSION = string(Pkg.installed("Plotly")) |
| 11 | + |
| 12 | +const METHOD_MAP = Dict( |
| 13 | + :get => Requests.get, |
| 14 | + :post => Requests.post, |
| 15 | + :put => Requests.put, |
| 16 | + :delete => Requests.delete, |
| 17 | + :patch => Requests.patch, |
| 18 | +) |
| 19 | + |
| 20 | +# import HTTP |
| 21 | +# const METHOD_MAP = Dict( |
| 22 | +# :get => HTTP.get, |
| 23 | +# :post => HTTP.post, |
| 24 | +# :put => HTTP.put, |
| 25 | +# :delete => HTTP.delete, |
| 26 | +# :patch => HTTP.patch, |
| 27 | +# ) |
| 28 | +# function validate_response(res::HTTP.Response) |
| 29 | +# if res.status > 204 |
| 30 | +# uri = get(res.request).uri |
| 31 | +# throw(PlotlyAPIError("Request $uri failed with code $(res.status)", res)) |
| 32 | +# end |
| 33 | +# end |
| 34 | +# get_json_data(res::HTTP.Response) = JSON.parse(deepcopy(res.body)) |
| 35 | + |
| 36 | +# --------------- # |
| 37 | +# Tools/Utilities # |
| 38 | +# --------------- # |
| 39 | + |
| 40 | +function get_method(method::Symbol) |
| 41 | + if haskey(METHOD_MAP, method) |
| 42 | + return METHOD_MAP[method] |
| 43 | + else |
| 44 | + error("Unkown method type $method requested") |
| 45 | + end |
| 46 | +end |
| 47 | +get_method(s::String) = get_method(Symbol(s)) |
| 48 | + |
| 49 | +struct PlotlyAPIError <: Exception |
| 50 | + msg |
| 51 | + res |
| 52 | +end |
| 53 | + |
| 54 | +function validate_response(res::Requests.Response) |
| 55 | + code = Requests.statuscode(res) |
| 56 | + if code > 204 |
| 57 | + uri = Requests.requestfor(res).uri |
| 58 | + # TODO: provide meaningful error message based on request url + status |
| 59 | + throw(PlotlyAPIError("Request $uri failed with code $code", res)) |
| 60 | + end |
| 61 | +end |
| 62 | + |
| 63 | +get_json_data(res::Requests.Response) = Requests.json(res) |
| 64 | + |
| 65 | +function basic_auth(username, password) |
| 66 | + # ref https://github.com/plotly/plotly.py/blob/master/plotly/api/utils.py |
| 67 | + return string("Basic ", base64encode(string(username, ":", password))) |
| 68 | +end |
| 69 | + |
| 70 | +function get_headers(method::Symbol=:get) |
| 71 | + creds = Plotly.get_credentials() |
| 72 | + return Dict{Any,Any}( |
| 73 | + "Plotly-Client-Platform" => "Julia $(VERSION)", |
| 74 | + "Content-Type" => "application/json", |
| 75 | + "content-type" => "application/json", |
| 76 | + "Accept" => "application/json", # TODO: for some reason I had to do this to get it to work??? |
| 77 | + "authorization" => basic_auth(creds.username, creds.api_key), |
| 78 | + ) |
| 79 | +end |
| 80 | +get_headers(s::String) = get_headers(Symbol(s)) |
| 81 | + |
| 82 | +function get_json(;kwargs...) |
| 83 | + out = Dict() |
| 84 | + for (k, v) in kwargs |
| 85 | + if v !=nothing |
| 86 | + out[k] = v |
| 87 | + end |
| 88 | + end |
| 89 | + out |
| 90 | +end |
| 91 | + |
| 92 | +function _get_uid(uids) |
| 93 | + length(uids) == 0 && error("Must supply at least one uid") |
| 94 | + uid = join(map(string, uids), ",") |
| 95 | +end |
| 96 | + |
| 97 | +function api_url(endpoint; fid=nothing, route=nothing) |
| 98 | + extra = [] |
| 99 | + fid !== nothing && push!(extra, fid) |
| 100 | + route !== nothing && push!(extra, route) |
| 101 | + out = string(API_ROOT, endpoint) |
| 102 | + if length(extra) > 0 |
| 103 | + return out * "/" * join(extra, "/") |
| 104 | + else |
| 105 | + return out |
| 106 | + end |
| 107 | +end |
| 108 | + |
| 109 | +function request(method, endpoint; fid=nothing, route=nothing, json=nothing, kwargs...) |
| 110 | + url = api_url(endpoint; fid=fid, route=route) |
| 111 | + method_func = get_method(method) |
| 112 | + query_params = Dict() |
| 113 | + for (k, v) in kwargs |
| 114 | + if v !== nothing |
| 115 | + query_params[k] = v |
| 116 | + end |
| 117 | + end |
| 118 | + if Symbol(method) in (:post, :patch, :put) && json !== nothing |
| 119 | + # here I am! |
| 120 | + res = method_func(url, headers=get_headers(method), query=query_params, json=json) |
| 121 | + else |
| 122 | + res = method_func(url, headers=get_headers(method), query=query_params) |
| 123 | + end |
| 124 | + validate_response(res) |
| 125 | + res |
| 126 | +end |
| 127 | + |
| 128 | +function request_data(method, endpoint; kwargs...) |
| 129 | + res = request(method, endpoint; kwargs...) |
| 130 | + content_type = get(Requests.headers(res), "Content-Type", "application/json") |
| 131 | + if startswith(content_type, "application/json") |
| 132 | + return get_json_data(res) |
| 133 | + else |
| 134 | + return res |
| 135 | + end |
| 136 | +end |
| 137 | + |
| 138 | +# ---------------- # |
| 139 | +# # API wrappers # # |
| 140 | +# ---------------- # |
| 141 | + |
| 142 | +struct ApiCall |
| 143 | + funname::Symbol |
| 144 | + method::Symbol |
| 145 | + endpoint::Symbol |
| 146 | + fid::Bool |
| 147 | + route::Union{Void,Symbol} |
| 148 | + required::Vector{Symbol} |
| 149 | + optional::Vector{Symbol} |
| 150 | + json::Vector{Symbol} |
| 151 | + required_json::Vector{Symbol} |
| 152 | + uids::Bool |
| 153 | + data_out::Bool |
| 154 | +end |
| 155 | + |
| 156 | +function ApiCall( |
| 157 | + funname::Symbol, method::Symbol, endpoint::Symbol, fid::Bool=false, |
| 158 | + route=nothing; required=Symbol[], optional=Symbol[], json=Symbol[], |
| 159 | + required_json=Symbol[], uids::Bool=false, data_out::Bool=true |
| 160 | + ) |
| 161 | + ApiCall( |
| 162 | + funname, method, endpoint, fid, route, required, optional, json, |
| 163 | + required_json, uids, data_out |
| 164 | + ) |
| 165 | +end |
| 166 | + |
| 167 | +function make_method(api::ApiCall) |
| 168 | + request_fun = api.data_out ? :request_data : :request |
| 169 | + request_kwargs = [] |
| 170 | + |
| 171 | + all_kw = vcat(api.json, api.optional) |
| 172 | + sig = Expr(:call, |
| 173 | + api.funname, |
| 174 | + Expr(:parameters, [Expr(:kw, name, nothing) for name in all_kw]...), |
| 175 | + ) |
| 176 | + if api.fid |
| 177 | + push!(sig.args, :_fid) |
| 178 | + push!(request_kwargs, Expr(:kw, :fid, :_fid)) |
| 179 | + end |
| 180 | + |
| 181 | + # add rest of required |
| 182 | + append!(sig.args, api.required) |
| 183 | + append!(sig.args, api.required_json) |
| 184 | + if api.uids |
| 185 | + push!(sig.args, Expr(:(...), :uids)) |
| 186 | + push!(request_kwargs, Expr(:kw, :uid, :(_get_uid(uids)))) |
| 187 | + end |
| 188 | + |
| 189 | + if api.route != nothing |
| 190 | + push!(request_kwargs, Expr(:kw, :route, string(api.route))) |
| 191 | + end |
| 192 | + |
| 193 | + all_json = vcat(api.json, api.required_json) |
| 194 | + if length(all_json) > 0 |
| 195 | + call_get_json = Expr(:call, :get_json, [Expr(:kw, name, name) for name in all_json]...) |
| 196 | + push!(request_kwargs, Expr(:kw, :json, call_get_json)) |
| 197 | + elseif api.method in (:put, :post, :patch, :delete) |
| 198 | + # need to add empty json argument on put these request methods when |
| 199 | + # no json data is needed. |
| 200 | + push!(request_kwargs, Expr(:kw, :json, :(Dict()))) |
| 201 | + end |
| 202 | + |
| 203 | + # add rest of kwargs |
| 204 | + append!(request_kwargs, [Expr(:kw, name, name) for name in api.optional]) |
| 205 | + append!(request_kwargs, [Expr(:kw, name, name) for name in api.required]) |
| 206 | + |
| 207 | + body = Expr(:block, |
| 208 | + Expr(:call, |
| 209 | + request_fun, |
| 210 | + string(api.method), |
| 211 | + string(api.endpoint), |
| 212 | + request_kwargs..., |
| 213 | + ) |
| 214 | + ) |
| 215 | + |
| 216 | + if api.data_out |
| 217 | + raw_sig = deepcopy(sig) |
| 218 | + raw_sig.args[1] = Symbol(api.funname, "_", "raw") |
| 219 | + |
| 220 | + raw_body = deepcopy(body) |
| 221 | + raw_body.args[1].args[1] = :request |
| 222 | + return Expr(:block, Expr(:function, sig, body), Expr(:function, raw_sig, raw_body)) |
| 223 | + |
| 224 | + else |
| 225 | + return Expr(:function, sig, body) |
| 226 | + end |
| 227 | +end |
| 228 | + |
| 229 | +file_writeable_metadata = [ |
| 230 | + :parent_path, :filename, :parent, :share_key_enabled, :world_readable |
| 231 | +] |
| 232 | +grid_writable_metadata = vcat(file_writeable_metadata, [:]) |
| 233 | +for _api in [ |
| 234 | + # search |
| 235 | + ApiCall(:search_list, :get, :search, false, required=[:q]) |
| 236 | + |
| 237 | + # files |
| 238 | + ApiCall(:file_retrieve, :get, :files, true) |
| 239 | + ApiCall(:file_content, :get, :files, true, :content) # failing |
| 240 | + ApiCall(:file_update, :put, :files, true, json=file_writeable_metadata) |
| 241 | + ApiCall(:file_partial_update, :patch, :files, true, json=file_writeable_metadata) |
| 242 | + ApiCall(:file_image, :get, :files, true, :image) |
| 243 | + ApiCall(:file_copy, :get, :files, true, :copy, optional=[:deep_copy]) # failing |
| 244 | + ApiCall(:file_path, :get, :files, true, :path) |
| 245 | + ApiCall(:file_drop_reference, :post, :files, true, :drop_reference, json=[:fid]) |
| 246 | + ApiCall(:file_trash, :post, :files, true, :trash) |
| 247 | + ApiCall(:file_restore, :post, :files, true, :restore) |
| 248 | + ApiCall(:file_permanent_delete, :post, :files, true, :permanent_delete, data_out=false) |
| 249 | + ApiCall(:file_lookup, :get, :files, false, :lookup, required=[:path], optional=[:parent, :user, :exists]) |
| 250 | + ApiCall(:file_star, :post, :files, true, :star) |
| 251 | + ApiCall(:file_remove_star, :delete, :files, true, :star, data_out=false) |
| 252 | + ApiCall(:file_sources, :get, :files, true, :sources) |
| 253 | + |
| 254 | + # grids |
| 255 | + ApiCall(:grid_create, :post, :grids, false, required_json=[:data], json=file_writeable_metadata) |
| 256 | + # ApiCall(:grid_upload) # failing |
| 257 | + ApiCall(:grid_row, :post, :grids, true, :row, required_json=[:rows], data_out=false) |
| 258 | + ApiCall(:grid_get_col, :get, :grids, true, :col, uids=true) |
| 259 | + ApiCall(:grid_put_col, :put, :grids, true, :col, uids=true, required_json=[:cols]) # failing |
| 260 | + ApiCall(:grid_post_col, :post, :grids, true, :col, required_json=[:cols]) # failing |
| 261 | + ApiCall(:grid_retrieve, :get, :grids, true) |
| 262 | + ApiCall(:grid_content, :get, :grids, true, :content) |
| 263 | + ApiCall(:grid_destroy, :delete, :grids, true, data_out=false) |
| 264 | + ApiCall(:grid_partial_update, :patch, :grids, true, json=file_writeable_metadata) |
| 265 | + ApiCall(:grid_update, :put, :grids, true, json=file_writeable_metadata) |
| 266 | + ApiCall(:grid_drop_reference, :post, :grids, true, :drop_reference, json=[:fid]) |
| 267 | + ApiCall(:grid_trash, :post, :grids, true, :trash) |
| 268 | + ApiCall(:grid_restore, :post, :grids, true, :restore) |
| 269 | + ApiCall(:grid_permanent_delete, :post, :grids, true, :permanent_delete, data_out=false) |
| 270 | + ApiCall(:grid_lookup, :get, :grids, false, :lookup, required=[:path], optional=[:parent, :user, :exists]) |
| 271 | + |
| 272 | + # plots |
| 273 | + ApiCall(:plot_list, :get, :plots, false, optional=[:order_by, :min_quality, :max_quality]) |
| 274 | + ApiCall(:plot_feed, :get, :plots, false, :feed) |
| 275 | + ApiCall(:plot_create, :post, :plots, false; required_json=[:figure], json=file_writeable_metadata) |
| 276 | + ApiCall(:plot_detail, :get, :plots, true) |
| 277 | + ApiCall(:plot_content, :get, :plots, true, :content, optional=[:inline_data, :map_data]) |
| 278 | + ApiCall(:plot_update, :put, :plots, true, json=file_writeable_metadata) |
| 279 | + ApiCall(:plot_partial_update, :patch, :plots, true, json=file_writeable_metadata) |
| 280 | + |
| 281 | + # extras |
| 282 | + ApiCall(:extra_create, :post, :extras, false, required_json=[:referencers], json=[:filename, :content]) |
| 283 | + ApiCall(:extra_content, :post, :extras, true, :content) |
| 284 | + ApiCall(:extra_partial_update, :patch, :extras, true, json=[:filename, :content]) |
| 285 | + ApiCall(:extra_delete, :delete, :extras, true, data_out=false) |
| 286 | + ApiCall(:extra_detail, :get, :extras, true) |
| 287 | + |
| 288 | + # folders |
| 289 | + ApiCall(:folder_create, :post, :folders, false, required_json=[:path], json=[:parent]) |
| 290 | + ApiCall(:folder_detail, :get, :folders, true) |
| 291 | + ApiCall(:folder_home, :get, :folders, false, :home, optional=[:user]) |
| 292 | + ApiCall(:folder_shared, :get, :folders, false, :shared) |
| 293 | + ApiCall(:folder_starred, :get, :folders, false, :starred) |
| 294 | + ApiCall(:folder_trashed, :get, :folders, false, :trashed) |
| 295 | + ApiCall(:folder_all, :get, :folders, false, :all, optional=[:user, :filetype, :order_by]) |
| 296 | + ApiCall(:folder_trash, :post, :folders, true, :trash) |
| 297 | + ApiCall(:folder_restore, :post, :folders, true, :restore) |
| 298 | + ApiCall(:folder_permanent_delete, :post, :folders, true, :permanent_delete) |
| 299 | + |
| 300 | + # images |
| 301 | + ApiCall(:image_generate, :post, :images, false, required_json=[:figure], json=[:width, :height, :format, :scale, :encoded]) |
| 302 | + |
| 303 | + # comments |
| 304 | + ApiCall(:comment_create, :post, :comments, false, required_json=[:fid, :comment]) |
| 305 | + ApiCall(:comment_delete, :delete, :comments, true) |
| 306 | + |
| 307 | + # plot-schema |
| 308 | + ApiCall(:plot_schema_get, :get, Symbol("plot-schema"), required=[:sha1]) |
| 309 | + ] |
| 310 | + eval(current_module(), make_method(_api)) |
| 311 | +end |
| 312 | + |
| 313 | +# --------------------- # |
| 314 | +# Convenience functions # |
| 315 | +# --------------------- # |
| 316 | + |
| 317 | +end # module |
0 commit comments