Skip to content

Commit 2166413

Browse files
committed
WIP: started wrapping the v2 api endpoints. still need to replace current code
1 parent fb52f4f commit 2166413

File tree

1 file changed

+317
-0
lines changed

1 file changed

+317
-0
lines changed

src/v2.jl

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
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

Comments
 (0)