Skip to content

Commit e3458cf

Browse files
authored
Merge branch 'main' into kr/taginoptions
2 parents 29b83fa + 09fdfd7 commit e3458cf

File tree

7 files changed

+257
-25
lines changed

7 files changed

+257
-25
lines changed

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ const PAGES_REFERENCE = [
9292
"reference/datasets.md",
9393
"reference/projects.md",
9494
"reference/exceptions.md",
95+
"reference/experimental.md",
9596
]
9697
Mocking.apply(mocking_patch) do
9798
makedocs(;

docs/src/reference/experimental.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Experimental APIs
2+
3+
> 🐉 Hic Sunt Dracones
4+
5+
The [`JuliaHub.Experimental`](@ref) module contains various experimental APIs.
6+
7+
```@docs
8+
JuliaHub.Experimental
9+
```
10+
11+
## Reference
12+
13+
```@docs
14+
JuliaHub.Experimental.Registry
15+
JuliaHub.Experimental.registries
16+
JuliaHub.Experimental.register_package
17+
```

src/JuliaHub.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const _LOCAL_TZ = Ref{Dates.TimeZone}()
2222
include("utils.jl")
2323
include("authentication.jl")
2424
include("restapi.jl")
25+
include("experimental.jl")
2526
include("userinfo.jl")
2627
include("applications.jl")
2728
include("batchimages.jl")
@@ -34,6 +35,7 @@ include("jobs/request.jl")
3435
include("jobs/logging.jl")
3536
include("jobs/logging-kafka.jl")
3637
include("jobs/logging-legacy.jl")
38+
include("packages.jl")
3739
include("projects.jl")
3840

3941
# JuliaHub.jl follows the convention that all private names are
@@ -42,7 +44,8 @@ function _find_public_names()
4244
return filter(names(@__MODULE__; all=true)) do s
4345
# We don't need to check or mark public the main module itself
4446
(s == :JuliaHub) && return false
45-
startswith(string(s), "_") && return false
47+
# The Experimental module (or anything within it) is not public.
48+
(s == :Experimental) && return false
4649
# Internal functions and types, prefixed by _
4750
startswith(string(s), "_") && return false
4851
# Internal macros, prefixed by _

src/experimental.jl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
module Experimental
3+
4+
Home for experimental JuliaHub.jl APIs.
5+
6+
!!! warning "Unstable APIs"
7+
8+
These APIs are considered highly unstable.
9+
Both JuliaHub platform version changes, and also JuliaHub.jl package changes may break these APIs at any time.
10+
Depend on them at your own peril.
11+
"""
12+
module Experimental
13+
14+
using UUIDs: UUIDs
15+
16+
const _DOCS_EXPERIMENTAL_API = """
17+
!!! warning "Unstable API"
18+
This API is not part of the public API and does not adhere to semantic versioning.
19+
20+
This APIs is considered highly unstable.
21+
Both JuliaHub platform version changes, and also JuliaHub.jl package changes may break it at any time.
22+
Depend on it at your own peril.
23+
"""
24+
25+
"""
26+
struct Registry
27+
28+
Represents a Julia package registry on JuliaHub.
29+
30+
$(_DOCS_EXPERIMENTAL_API)
31+
"""
32+
struct Registry
33+
uuid::UUIDs.UUID
34+
name::String
35+
end
36+
37+
function registries end
38+
function register_package end
39+
40+
end

src/packages.jl

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
function _parse_registry(registry_dict::Dict)
2+
name, uuid = try
3+
registry_dict["name"], tryparse(UUIDs.UUID, registry_dict["uuid"])
4+
catch e
5+
@error "Invalid registry value in API response" exception = (e, catch_backtrace())
6+
return nothing
7+
end
8+
return Experimental.Registry(uuid, name)
9+
end
10+
11+
"""
12+
JuliaHub.Experimental.registries() -> Vector{Experimental.Registry}
13+
14+
Return the list of registries configured on the JuliaHub instance.
15+
16+
$(Experimental._DOCS_EXPERIMENTAL_API)
17+
"""
18+
function Experimental.registries(auth::Authentication)
19+
# NOTE: this API endpoint is not considered stable as of now
20+
r = _restcall(auth, :GET, ("app", "packages", "registries"), nothing)
21+
if r.status != 200 || !r.json["success"]
22+
throw(JuliaHubError("Invalid response from JuliaHub (code $(r.status))\n$(r.body)"))
23+
end
24+
_parse_registry.(r.json["registries"])
25+
end
26+
27+
"""
28+
JuliaHub.Experimental.register_package(
29+
auth::Authentication,
30+
registry::Union{AbstractString, Registry},
31+
repository_url::AbstractString;
32+
# Optional keyword arguments:
33+
[notes::AbstractString,]
34+
[branch::AbstractString,]
35+
[subdirectory::AbstractString,]
36+
[git_server_type::AbstractString]
37+
) -> String | Nothing
38+
39+
Initiates a registration PR of the package at `repository_url` in
40+
Returns the URL of the registry PR, or `nothing` if the registration failed.
41+
42+
# Example
43+
44+
```
45+
using JuliaHub
46+
auth = JuliaHub.authenticate("juliahub.com")
47+
JuliaHub._registries(auth)
48+
49+
r = JuliaHub.Experimental.register_package(
50+
auth,
51+
"MyInternalRegistry",
52+
"https://github.com/MyUser/MyPackage.jl";
53+
notes = "This was initiated via JuliaHub.jl",
54+
)
55+
```
56+
57+
$(Experimental._DOCS_EXPERIMENTAL_API)
58+
"""
59+
function Experimental.register_package(
60+
auth::Authentication,
61+
registry::Union{AbstractString, Experimental.Registry},
62+
repository_url::AbstractString;
63+
notes::Union{AbstractString, Nothing}=nothing,
64+
branch::Union{AbstractString, Nothing}=nothing,
65+
subdirectory::AbstractString="",
66+
git_server_type::Union{AbstractString, Nothing}=nothing,
67+
)
68+
if !isnothing(branch) && isempty(branch)
69+
throw(ArgumentError("branch can not be an empty string"))
70+
end
71+
git_server_type = if isnothing(git_server_type)
72+
if startswith(repository_url, "https://github.com")
73+
"github"
74+
else
75+
throw(
76+
ArgumentError(
77+
"Unable to determine git_server_type for repository: $(repository_url)"
78+
),
79+
)
80+
end
81+
else
82+
git_server_type
83+
end
84+
# Interpret the registry argument
85+
registry_name::String = if isa(registry, Experimental.Registry)
86+
registry.name
87+
else
88+
String(registry)
89+
end
90+
# Do the package registration POST request.
91+
# NOTE: this API endpoint is not considered stable as of now
92+
body = Dict(
93+
"requests" => [
94+
Dict(
95+
"registry_name" => registry_name,
96+
"repo_url" => repository_url,
97+
"branch" => something(branch, ""),
98+
"notes" => something(notes, ""),
99+
"subdir" => subdirectory,
100+
"git_server_type" => git_server_type,
101+
),
102+
],
103+
)
104+
r = _restcall(
105+
auth,
106+
:POST,
107+
("app", "registrator", "register"),
108+
JSON.json(body);
109+
headers=["Content-Type" => "application/json"],
110+
)
111+
if r.status != 200
112+
throw(JuliaHubError("Invalid response from JuliaHub (code $(r.status))\n$(r.body)"))
113+
elseif !r.json["success"]
114+
error_message = get(get(r.json, "message", Dict()), "error", nothing)
115+
if isnothing(error_message)
116+
throw(JuliaHubError("Invalid response from JuliaHub (code $(r.status))\n$(r.body)"))
117+
end
118+
throw(InvalidRequestError(error_message))
119+
end
120+
id, message = r.json["id"], r.json["message"]
121+
@info "Initiated registration in $(registry_name)" id message repository_url
122+
sleep(1) # registration won't go through right away anyway
123+
status = _registration_status(auth, id)
124+
δt = 2
125+
while status.state == "pending"
126+
sleep(δt)
127+
δt = min(δt * 2, 10) # double the sleep time, to a max of 10s
128+
status = _registration_status(auth, id)
129+
if status.state == "pending"
130+
@info ".. waiting for registration to succeed" status.message
131+
end
132+
end
133+
if status.state != "success"
134+
@error "Registration failed ($id)" status.state status.message
135+
return nothing
136+
end
137+
return status.message
138+
end
139+
140+
struct _RegistrationStatus
141+
state::String
142+
message::String
143+
end
144+
145+
function _registration_status(auth::Authentication, id::AbstractString)
146+
# NOTE: this API endpoint is not considered stable as of now
147+
r = _restcall(
148+
auth,
149+
:POST,
150+
("app", "registrator", "status"),
151+
JSON.json(Dict(
152+
"id" => id
153+
));
154+
headers=["Content-Type" => "application/json"],
155+
)
156+
if r.status != 200 || !r.json["success"]
157+
throw(JuliaHubError("Invalid response from JuliaHub (code $(r.status))\n$(r.body)"))
158+
end
159+
return _RegistrationStatus(r.json["state"], r.json["message"])
160+
end

test/datasets-live.jl

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
11
import HTTP, JSON, JuliaHub
2-
function _get_user_groups_rest(auth::JuliaHub.Authentication)
3-
r = HTTP.get(
4-
JuliaHub._url(auth, "user", "groups"),
5-
JuliaHub._authheaders(auth),
6-
)
7-
r.status == 200 && return String.(JSON.parse(String(r.body)))
8-
JuliaHub._throw_invalidresponse(r)
9-
end
10-
function _get_user_groups_gql(auth::JuliaHub.Authentication)
2+
function _get_user_groups(auth::JuliaHub.Authentication)::Vector{String}
113
# Note: this query is newer than the one we use in src/userinfo.jl, and works
124
# with newer JuliaHub versions, whereas the other one specifically works with
13-
# older versions.
5+
# older versions. This specific query has been tested with JuliaHub 6.8+
146
userinfo_gql = read(joinpath(@__DIR__, "userInfo.gql"), String)
15-
r = JuliaHub._gql_request(auth, userinfo_gql)
16-
r.status == 200 || error("Invalid response from GQL ($(r.status))\n$(r.body)")
17-
user = only(r.json["data"]["users"])
18-
String[g["group"]["name"] for g in user["groups"]]
19-
end
20-
function _get_user_groups(auth::JuliaHub.Authentication)::Vector{String}
21-
rest_exception = try
22-
return _get_user_groups_rest(auth)
23-
catch e
24-
@debug "Failed to fetch user groups via REST API" exception = (e, catch_backtrace())
25-
e, catch_backtrace()
26-
end
277
try
28-
return _get_user_groups_gql(auth)
8+
r = JuliaHub._gql_request(auth, userinfo_gql)
9+
r.status == 200 || error("Invalid response from GQL ($(r.status))\n$(r.body)")
10+
user = only(r.json["data"]["users"])
11+
return String[g["group"]["name"] for g in user["groups"]]
2912
catch e
3013
@error "Unable to determine valid user groups"
3114
@error "> REST API failure" exception = rest_exception

test/userInfo.gql

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# # LIMITATIONS ON EXTERNAL USE: external users are only allowed to depend
2+
# # on certain fields exposed in this query. No backwards compatibility
3+
# # guarantees are made for the fields marked "DISALLOWED".
4+
#
5+
# external_query = true
6+
# roles = ["default"]
7+
18
query UserInfo {
29
users(limit: 1) {
310
id
@@ -6,7 +13,19 @@ query UserInfo {
613
emails {
714
email
815
}
9-
groups: user_groups {
16+
# DISALLOWED: .features is disallowed for external users
17+
# No guarantees are made about the contents or validity of this field.
18+
features: user_feature_maps {
19+
feature {
20+
name
21+
id
22+
product_id
23+
}
24+
}
25+
# DISALLOWED: .get_started_viewed is disallowed for external users
26+
# No guarantees are made about the contents or validity of this field.
27+
get_started_viewed
28+
groups: user_groups(where: { _not: { is_deleted: { _eq: true } } }) {
1029
id: group_id
1130
group {
1231
name
@@ -22,6 +41,15 @@ query UserInfo {
2241
}
2342
}
2443
accepted_tos
44+
# DISALLOWED: .accepted_tos_time is disallowed for external users
45+
# No guarantees are made about the contents or validity of this field.
46+
accepted_tos_time
2547
survey_submitted_time
2648
}
49+
# DISALLOWED: .features is disallowed for external users
50+
# No guarantees are made about the contents or validity of this field.
51+
features(where: { public: { _eq: true } }) {
52+
id
53+
name
54+
}
2755
}

0 commit comments

Comments
 (0)