Skip to content

Commit 6b1a932

Browse files
authored
feat(exp): add experimental package registration API (#96)
1 parent a8d6a9e commit 6b1a932

File tree

4 files changed

+221
-1
lines changed

4 files changed

+221
-1
lines changed

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

0 commit comments

Comments
 (0)