From e29b2308b28690f7604f9134c087ccd9788d4fbf Mon Sep 17 00:00:00 2001 From: Dale Black Date: Thu, 2 Jan 2025 11:13:11 -0800 Subject: [PATCH] rework package into simpler script --- Project.toml | 11 +- src/OrthancTools.jl | 174 ++++++++++++++++++++++++++++- src/downloader.jl | 264 -------------------------------------------- test/runtests.jl | 103 ++++++++++++++++- 4 files changed, 280 insertions(+), 272 deletions(-) delete mode 100644 src/downloader.jl diff --git a/Project.toml b/Project.toml index caadaa7..c81fc0e 100644 --- a/Project.toml +++ b/Project.toml @@ -6,16 +6,15 @@ version = "0.1.0" [deps] Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" -InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" -Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" [compat] -julia = "1.8" +"HTTP" = "1.10" +"JSON" = "0.21" +"OrderedCollections" = "1.7" +"Downloads" = "1.6" +julia = "1.10, 1.11" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/src/OrthancTools.jl b/src/OrthancTools.jl index f202cfe..7bd4fdc 100644 --- a/src/OrthancTools.jl +++ b/src/OrthancTools.jl @@ -1,5 +1,177 @@ module OrthancTools -include("downloader.jl") +using OrderedCollections: OrderedDict +using Downloads: download +import HTTP +import JSON + +export get_all_studies, get_all_series, get_all_instances, download_instances + +""" + get_all_studies(ip_address::String="localhost"; show_warnings=false) + +Get all studies from an Orthanc server. + +# Arguments +- `ip_address`: IP address corresponding to the Orthanc server +- `show_warnings`: Whether to show warnings for studies without accession numbers + +# Returns +- `studies_dict`: An `OrderedDict` of every study name with its corresponding accession number + +# Example +```julia +studies_dict = get_all_studies("128.000.00.00") +# OrderedDict("CTP006" => ["e44217cc-498e394b-dc380909-a742a65f-51530d58"], ...) +``` +""" +function get_all_studies(ip_address::String="localhost"; show_warnings=false) + url_studies = HTTP.URI(scheme="http", host=ip_address, port="8042", path="/studies") + studies = JSON.parse(String(HTTP.get(url_studies).body)) + + studies_dict = OrderedDict{String,Vector}() + for study in studies + s = JSON.parse(String(HTTP.get(string(url_studies, "/", study)).body)) + try + accession_num = s["MainDicomTags"]["AccessionNumber"] + if !haskey(studies_dict, accession_num) + push!(studies_dict, accession_num => [study]) + else + push!(studies_dict[accession_num], study) + end + catch + if show_warnings + @warn "No accession number for $study" + end + end + end + + return studies_dict +end + +""" + get_all_series(studies_dict::OrderedDict, accession_num::String, ip_address::String="localhost") + +Get all series for a specific study from an Orthanc server. + +# Arguments +- `studies_dict`: A dictionary of all the studies in this server (see `get_all_studies`) +- `accession_num`: The accession number for the study of interest +- `ip_address`: IP address corresponding to the Orthanc server + +# Returns +- `series_dict`: An `OrderedDict` of every series name with its corresponding series number + +# Example +```julia +series_dict = get_all_series(studies_dict, "2475", "128.000.00.00") +# OrderedDict("1" => ["776d9eb6-34ff4a40-e252e29c-3d3b9806-db6a4d8b"], ...) +``` +""" +function get_all_series( + studies_dict::OrderedDict, + accession_num::String, + ip_address::String="localhost") + + url_study = HTTP.URI(scheme="http", host=ip_address, port="8042", path="/studies/$(studies_dict[accession_num]...)") + + series = JSON.parse(String(HTTP.get(url_study).body)) + series_dict = OrderedDict{String,Vector}() + for ser in series["Series"] + url_series = HTTP.URI( + scheme="http", host=ip_address, port="8042", path=string("/series/", ser) + ) + s = JSON.parse(String(HTTP.get(url_series).body)) + try + series_num = s["MainDicomTags"]["SeriesNumber"] + if !haskey(series_dict, series_num) + push!(series_dict, series_num => [ser]) + else + push!(series_dict[series_num], ser) + end + catch + @warn "No series number for $ser" + end + end + + return series_dict +end + +""" + get_all_instances(series_dict::OrderedDict, series_num::String, ip_address::String="localhost") + +Get all instances for a specific series from an Orthanc server. + +# Arguments +- `series_dict`: A dictionary of all the series in this server (see `get_all_series`) +- `series_num`: The series number for the study of interest +- `ip_address`: IP address corresponding to the Orthanc server + +# Returns +- `instances_dict`: An `OrderedDict` of every instance name with its corresponding series number + +# Example +```julia +instances_dict = get_all_instances(series_dict, "4", "128.000.00.00") +# OrderedDict("4" => [["ddbcf7d1-eab7dfc6-1f507224-7a92be0c-4c08d055"], ...]) +``` +""" +function get_all_instances( + series_dict::OrderedDict, + series_num::String, + ip_address::String="localhost") + + instances_dict = OrderedDict{String,Vector}() + for ser in series_dict[series_num] + url_series = HTTP.URI( + scheme="http", host=ip_address, port="8042", path=string("/series/", ser) + ) + series = JSON.parse(String(HTTP.get(url_series).body)) + instances = series["Instances"] + if !haskey(instances_dict, series_num) + push!(instances_dict, series_num => [instances]) + else + push!(instances_dict[series_num], instances) + end + end + return instances_dict +end + +""" + download_instances(instances_dict::OrderedDict, instance_num::Number, output_directory::String, ip_address::String="localhost") + +Download specific instances from an Orthanc server. + +# Arguments +- `instances_dict`: A dictionary of all the instances in this series (see `get_all_series`) +- `instance_num`: The instance number of the series that you want to download +- `output_directory`: The folder you want to save the DICOM files +- `ip_address`: IP address corresponding to the Orthanc server + +# Returns +Nothing, but saves DICOM files to the specified output directory + +# Example +```julia +output_dir = mktempdir() +download_instances(instances_dict, 1, output_dir, "128.000.00.00") +``` +""" +function download_instances( + instances_dict::OrderedDict, + instance_num::Number, + output_directory::String, + ip_address::String="localhost") + + for (key, value) in instances_dict + for i in value[instance_num] + url_instance = string("http://", ip_address, ":8042", string("/instances/", i)) + instance = JSON.parse(String(HTTP.get(url_instance).body)) + idx = instance["IndexInSeries"] + download(string(url_instance, "/", "file"), joinpath(output_directory, "$(idx).dcm")) + end + end + return nothing +end end diff --git a/src/downloader.jl b/src/downloader.jl deleted file mode 100644 index d277464..0000000 --- a/src/downloader.jl +++ /dev/null @@ -1,264 +0,0 @@ -### A Pluto.jl notebook ### -# v0.19.22 - -using Markdown -using InteractiveUtils - -# ╔═╡ d8578dac-9845-11ed-324c-856950ed40c3 -# ╠═╡ show_logs = false -begin - using Pkg - Pkg.activate("..") - using Revise, PlutoUI, HTTP, JSON, Downloads, OrderedCollections - using Downloads: download -end - -# ╔═╡ 0771ec75-e37b-4fa8-906d-a6b1f3f51d6f -TableOfContents() - -# ╔═╡ a8d7f303-4b86-4172-9425-da1c5c0daa66 -ip_address = "128.200.49.26" - -# ╔═╡ 93bc7b5c-97dc-4166-89e8-490e5366c7cb -md""" -## Get Studies -""" - -# ╔═╡ 8e780f7b-53df-4e0b-bf3d-b4c1094b7b24 -""" -```julia -get_all_studies(ip_address::String="localhost") -``` - -#### Arguments -- `ip_address`: IP address corresponding to the Orthanc server - -#### Returns -- `studies_dict`: An `OrderedDict` of every study name with its corresponding accession number - -#### Example -```julia-repl -julia> studies_dict = get_all_studies("128.000.00.00") - -studies_dict = OrderedDict("CTP006" => ["e44217cc-498e394b-dc380909-a742a65f-51530d58"], "2890" => ["30c04e76-4965d9e3-9ffd4fdc-b0e84651-325c9074", "30c04e76-4965d9e3-9ffd4fdc-b0e84651-895s9231"], more...) -``` -""" -function get_all_studies(ip_address::String="localhost"; show_warnings=false) - url_studies = HTTP.URI(scheme="http", host=ip_address, port="8042", path="/studies") - studies = JSON.parse(String(HTTP.get(url_studies).body)) - - studies_dict = OrderedDict{String,Vector}() - for study in studies - s = JSON.parse(String(HTTP.get(string(url_studies, "/", study)).body)) - try - accession_num = s["MainDicomTags"]["AccessionNumber"] - if !haskey(studies_dict, accession_num) - push!(studies_dict, accession_num => [study]) - else - push!(studies_dict[accession_num], study) - end - catch - if show_warnings - @warn "No accession number for $study" - end - end - end - - return studies_dict -end - -# ╔═╡ c17095fb-fe4d-458e-91f3-f94c48a785ec -export get_all_studies - -# ╔═╡ 3540e706-ad58-4af2-b7c0-f83ae19edcd3 -md""" -## Get Series -""" - -# ╔═╡ 2c04e17b-9c6d-4a36-985c-c256a4397a8e -""" -```julia -get_all_series( - studies_dict::OrderedDict, - accession_num::String, - ip_address::String="localhost") -``` - -#### Arguments -- `studies_dict`: A dictionary of all the studies in this server (see `get_all_studies`) -- `accession_num`: The accession number for the study of interest -- `ip_address`: IP address corresponding to the Orthanc server - -#### Return -- `series_dict`: An `OrderedDict` of every series name with its corresponding series number - -#### Example -```julia-repl -julia> series_dict = get_all_series(studies_dict, "2475", "128.000.00.00") - -series_dict = OrderedDict("1" => ["776d9eb6-34ff4a40-e252e29c-3d3b9806-db6a4d8b"], "2" => ["725fdc65-76d49a59-22f280ef-5c49f76e-9528dbb4", "bacca487-87933763-8d4b8253-aba41c21-cd57cad5"], more...) -``` -""" -function get_all_series( - studies_dict::OrderedDict, - accession_num::String, - ip_address::String="localhost") - - url_study = HTTP.URI(scheme="http", host=ip_address, port="8042", path="/studies/$(studies_dict[accession_num]...)") - - series = JSON.parse(String(HTTP.get(url_study).body)) - series_dict = OrderedDict{String,Vector}() - for ser in series["Series"] - url_series = HTTP.URI( - scheme="http", host=ip_address, port="8042", path=string("/series/", ser) - ) - s = JSON.parse(String(HTTP.get(url_series).body)) - try - series_num = s["MainDicomTags"]["SeriesNumber"] - if !haskey(series_dict, series_num) - push!(series_dict, series_num => [ser]) - else - push!(series_dict[series_num], ser) - end - catch - @warn "No series number for $ser" - end - end - - return series_dict -end - -# ╔═╡ 44b8bcae-9f64-4c1a-a98d-11d61a9fd747 -export get_all_series - -# ╔═╡ 8044f98d-889f-4e33-98ed-94fe446b3642 -accession_num = "2582" - -# ╔═╡ 2b291e5c-dafd-4914-aac7-212c4f8546c4 -md""" -## Get Instances -""" - -# ╔═╡ 9d7bc7f9-2970-4636-a74a-e6b761f8e215 -""" -```julia -get_all_instances( - series_dict::OrderedDict, - series_num::String, - ip_address::String="localhost") -``` - -#### Arguments -- `series_dict`: A dictionary of all the series in this server (see `get_all_series`) -- `series_num`: The series number for the study of interest -- `ip_address`: IP address corresponding to the Orthanc server - -#### Return -- `instances_dict`: An `OrderedDict` of every instance name with its corresponding series number (see `get_all_series`) - -#### Example -```julia-repl -julia> instances_dict = get_all_instances(series_dict, "4", "128.000.00.00") - -series_dict = OrderedDict("4" => ["ddbcf7d1-eab7dfc6-1f507224-7a92be0c-4c08d055", "0b643ae7-ecb881f6-c62055fb-3573fda4-b9c2abd2", more...], [, more...]) -``` -""" -function get_all_instances( - series_dict::OrderedDict, - series_num::String, - ip_address::String="localhost") - - url = HTTP.URI(scheme="http", host=ip_address, port="8042") - instances_dict = OrderedDict{String,Vector}() - for ser in series_dict[series_num] - url_series = HTTP.URI( - scheme="http", host=ip_address, port="8042", path=string("/series/", ser) - ) - series = JSON.parse(String(HTTP.get(url_series).body)) - instances = series["Instances"] - if !haskey(instances_dict, series_num) - push!(instances_dict, series_num => [instances]) - else - push!(instances_dict[series_num], instances) - end - end - return instances_dict -end - -# ╔═╡ 4e6ee284-3ee8-4af9-a7b4-cdfdabf65732 -export get_all_instances - -# ╔═╡ 34e8bb07-2e7a-4f71-9b8e-1c35372c4203 -series_num = "5" - -# ╔═╡ 8c2c8651-c413-48ab-8ed4-b71c97e83c04 -md""" -## Download Instances -""" - -# ╔═╡ a864c9f7-50e2-4105-8494-48cf9ba1a50b -""" -```julia -download_instances( - instances_dict::OrderedDict, - instance_num::Number, - output_directory::String, - ip_address::String="localhost") - -``` - -#### Arguments -- `instances_dict`: A dictionary of all the instances in this series (see `get_all_series`) -- `instance_num`: The instance number of the series that you want to download -- `output_directory`: The folder you want to save the DICOM files -- `ip_address`: IP address corresponding to the Orthanc server - -#### Return -- `output_directory`: The location where the DICOM file(s) will be saved - -#### Example -```julia-repl -julia> output_dir = mktempdir() -julia> download_instances(instances_dict, 1, output_dir, "128.000.00.00") - -Files located at /var/folders/t3/_k26tgtj7cv96l4vy3pxk5nw0000gn/T/jl_Hsbrwd -``` -""" -function download_instances( - instances_dict::OrderedDict, - instance_num::Number, - output_directory::String, - ip_address::String="localhost") - - for (key, value) in instances_dict - for i in value[instance_num] - url_instance = string("http://", ip_address, ":8042", string("/instances/", i)) - instance = JSON.parse(String(HTTP.get(url_instance).body)) - idx = instance["IndexInSeries"] - download(string(url_instance, "/", "file"), joinpath(output_directory, "$(idx).dcm")) - end - end - @info "Files located at $(output_directory)" -end - -# ╔═╡ 75b6606c-2e33-454b-98b7-a673f265b7ef -export download_instances - -# ╔═╡ Cell order: -# ╠═d8578dac-9845-11ed-324c-856950ed40c3 -# ╠═0771ec75-e37b-4fa8-906d-a6b1f3f51d6f -# ╠═a8d7f303-4b86-4172-9425-da1c5c0daa66 -# ╟─93bc7b5c-97dc-4166-89e8-490e5366c7cb -# ╠═8e780f7b-53df-4e0b-bf3d-b4c1094b7b24 -# ╠═c17095fb-fe4d-458e-91f3-f94c48a785ec -# ╟─3540e706-ad58-4af2-b7c0-f83ae19edcd3 -# ╠═2c04e17b-9c6d-4a36-985c-c256a4397a8e -# ╠═44b8bcae-9f64-4c1a-a98d-11d61a9fd747 -# ╠═8044f98d-889f-4e33-98ed-94fe446b3642 -# ╟─2b291e5c-dafd-4914-aac7-212c4f8546c4 -# ╠═9d7bc7f9-2970-4636-a74a-e6b761f8e215 -# ╠═4e6ee284-3ee8-4af9-a7b4-cdfdabf65732 -# ╠═34e8bb07-2e7a-4f71-9b8e-1c35372c4203 -# ╟─8c2c8651-c413-48ab-8ed4-b71c97e83c04 -# ╠═a864c9f7-50e2-4105-8494-48cf9ba1a50b -# ╠═75b6606c-2e33-454b-98b7-a673f265b7ef diff --git a/test/runtests.jl b/test/runtests.jl index 85ae388..9503e66 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,107 @@ using OrthancTools using Test +using OrderedCollections +using HTTP +using JSON + +#= +To run the tests locally with the Orthanc server, use: +``` +julia --project -e 'using Pkg; Pkg.test("OrthancTools", test_args=["local"])' +``` + +Else, run the tests in CI with: +``` +julia --project -e 'using Pkg; Pkg.test("OrthancTools")' +``` +Or simply: +``` +(OrthancTools) pkg> test +``` +=# + +# Check if we're running local tests +TEST_MODE = filter(x -> x in ["local"], ARGS) +if isempty(TEST_MODE) + @info "Running CI tests (no server connection)" +else + @info "Running local tests with Orthanc server" +end + +# Constants for local testing +const TEST_IP = "128.200.49.26" +const TEST_ACCESSION = "2581" +const TEST_SERIES = "1" @testset "OrthancTools.jl" begin - @test 1 == 1 # dummy test + if "local" in TEST_MODE + @testset "Local tests with real server" begin + # Test getting studies + studies_dict = get_all_studies(TEST_IP) + @test studies_dict isa OrderedDict + @test !isempty(studies_dict) + + # Test getting series + series_dict = get_all_series(studies_dict, TEST_ACCESSION, TEST_IP) + @test series_dict isa OrderedDict + @test !isempty(series_dict) + + # Test getting instances + instances_dict = get_all_instances(series_dict, TEST_SERIES, TEST_IP) + @test instances_dict isa OrderedDict + @test !isempty(instances_dict) + + # Test downloading instances + temp_dir = mktempdir() + try + download_instances(instances_dict, 1, temp_dir, TEST_IP) + # Check if any DICOM files were downloaded + @test !isempty(readdir(temp_dir)) + @test any(endswith.(readdir(temp_dir), ".dcm")) + finally + rm(temp_dir, recursive=true) + end + end + else + @testset "CI verification tests" begin + # Test type stability and basic functionality + @testset "get_all_studies" begin + # Test that it throws the right error for non-existent server + @test_throws HTTP.ConnectError get_all_studies("localhost", show_warnings=true) + + # Test return type with mock data + studies_dict = OrderedDict{String,Vector}() + @test studies_dict isa OrderedDict{String,Vector} + end + + @testset "get_all_series" begin + # Test that it throws the right error for non-existent server + studies_dict = OrderedDict("ACC001" => ["study1"]) + @test_throws HTTP.ConnectError get_all_series(studies_dict, "ACC001", "localhost") + + # Test input validation + @test_throws KeyError get_all_series(OrderedDict{String,Vector}(), "nonexistent", "localhost") + end + + @testset "get_all_instances" begin + # Test that it throws the right error for non-existent server + series_dict = OrderedDict("1" => ["series1"]) + @test_throws HTTP.ConnectError get_all_instances(series_dict, "1", "localhost") + + # Test input validation + @test_throws KeyError get_all_instances(OrderedDict{String,Vector}(), "nonexistent", "localhost") + end + + @testset "download_instances" begin + temp_dir = mktempdir() + try + instances_dict = OrderedDict("1" => [["instance1"]]) + # Test that it throws the right error for non-existent server + @test_throws HTTP.ConnectError download_instances(instances_dict, 1, temp_dir, "localhost") + finally + rm(temp_dir, recursive=true) + end + end + end + end end