Skip to content

Commit

Permalink
feat(phylopic): initial release
Browse files Browse the repository at this point in the history
* feat(phylopic): initial commit with an empty package

* ci(phylopic): setup testing

* dependencies(phylopic): add required packages

* feat(phylopic): add a non-exported function to ping

* feat(phylopic): autocomplete endpoint

* test(phylopic): ping

* dependencies(phylopic): add UUIDs

* feat(phylopic): search images

* refactor(phylopic): move functions to their own files

* feat(phylopic): get the images after a search

* doc(phylopic): comments in the top level file

* ci: add phylopic too the top level bootstrap

* license(phylopic): MIT

* doc(phylopic): README

* refactor(phylopic): use a more use-case inspired design

* test(phylopic): add test for names

* refactor(phylopic): move names to their own files

* refactor(phylopic): overload some methods for easier piping

* refactor(phylopic): replace images by thumbnail and vector

* dependencies(phylopic): import UUIDs

* doc(phylopic): update some comments in the code

* doc(phylopic): update the man pages

* doc(phylopic): manpage and vignette

* feat: reexport phylopic from top-level package

* doc(phylopic): README

* doc(phylopic): correct manpage path

* doc(phylopic): pick the first silhouette in the vignette

* doc(phylopic): correct index path

* bug: wrong module in the phylopic example

* doc(phylopic): fix axis for the figure

* doc(phylopic): some more context on how to plot

* doc(phylopic): use of the items argument

* feat(phylopic): restrict search by license terms

Closes #173

* feat(phylopic): rename names to imagesof

* doc(phylopic): update vignette use-case

* doc(phylopic): add docstrings in the manual section

* test(phylopic): name autocompletion

* bug(phylopic): wrong return when using items=1

* doc(phylopic): README

* feat(phylopic): twitter image

* feat(phylopic): add additional twitterimage methods

* refactor(phylopic): remove unused keyword arguments

* refactor(phylopic): get the link function outside of the data function

* feat(phylopic): get source image

* dependencies(phylopic): using Markdown

* feat(phylopic): attribution string

* bug(phylopic): wrong call to the function to get the links

* semver(phylopic): 0.0.1
  • Loading branch information
tpoisot committed Apr 25, 2023
1 parent 5571028 commit 12d567c
Show file tree
Hide file tree
Showing 22 changed files with 448 additions and 3 deletions.
26 changes: 25 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,30 @@ jobs:
with:
name: coverage
path: GBIF-lcov.info
Phylopic:
name: "Phylopic"
runs-on: "ubuntu-latest"
continue-on-error: true
steps:
- uses: actions/checkout@v3
- uses: julia-actions/setup-julia@v1
- name: "Bootstrap the package"
run: julia --project=./Phylopic Phylopic/bootstrap.jl
- uses: julia-actions/julia-buildpkg@latest
with:
project: Phylopic
- uses: julia-actions/julia-runtest@latest
with:
project: Phylopic
annotate: true
- uses: julia-actions/julia-processcoverage@v1
with:
directories: Phylopic/src
- run: mv lcov.info Phylopic-lcov.info
- uses: actions/upload-artifact@v3
with:
name: coverage
path: Phylopic-lcov.info
SimpleSDMLayers:
name: "SimpleSDMLayers"
runs-on: "ubuntu-latest"
Expand Down Expand Up @@ -107,7 +131,7 @@ jobs:
name: coverage
path: Fauxcurrences-lcov.info
SpeciesDistributionToolkit:
needs: [GBIF, Fauxcurrences, SimpleSDMLayers, SimpleSDMDatasets]
needs: [GBIF, Fauxcurrences, SimpleSDMLayers, SimpleSDMDatasets, Phylopic]
name: "SpeciesDistributionToolkit"
runs-on: "ubuntu-latest"
continue-on-error: true
Expand Down
22 changes: 22 additions & 0 deletions Phylopic/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The Phylopic.jl package is licensed under the MIT "Expat" License:

> Copyright (c) 2023: Timothée Poisot.
>
> Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions:
>
> The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
> SOFTWARE.
>
10 changes: 10 additions & 0 deletions Phylopic/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name = "Phylopic"
uuid = "c889285c-44aa-4473-b1e1-56f5d4e3ccf5"
authors = ["Timothée Poisot <timothee.poisot@umontreal.ca>"]
version = "0.0.1"

[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
18 changes: 18 additions & 0 deletions Phylopic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Phylopic.jl

This package is a *thin* wrapper around the Phylopic API, which is currently not covering
the entire API.

~~~julia
using Phylopic
import Downloads

# Get a series of UUIDs from a name
org_uuid = Phylopic.imagesof("chiroptera"; items=1)

# We can query the thumbnails for this UUID
thumb_url = Phylopic.thumbnail(org_uuid)

# We can download the thumbnail (to a temp file)
thumb_file = Downloads.download(first(thumb_url))
~~~
2 changes: 2 additions & 0 deletions Phylopic/bootstrap.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
main = () -> nothing
main()
33 changes: 33 additions & 0 deletions Phylopic/src/Phylopic.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module Phylopic

import HTTP
import JSON
import UUIDs
using Markdown

const api = "https://api.phylopic.org/"

# This file contains the ping and build functions, which are only really called when the
# package is initially loaded. Ping ensures that the API responds, and build gives the value
# of the current build by default, as it is required for most operations.
include("ping.jl")

# We do a first ping to fail ASAP if the API is not responsive
@assert isnothing(Phylopic.ping())

# We put the buildnumber in a const to avoid calling it multiple times -- this is a required
# parameter for a large number of queries (most of the queries, in fact), so it makes sense
# to get it as a const ASAP
const buildnumber = Phylopic.build()
# TODO: allow an environmental variable to specify a different build version for
# reproducibility

# The autocomplete endpoint is meant to give an overview of possible names starting from
# a stem - this is not necessarilly going to give all of the names, and I am not sure why
include("autocomplete.jl")

include("imagesof.jl")
include("images.jl")
include("attribution.jl")

end # module hylopic
16 changes: 16 additions & 0 deletions Phylopic/src/attribution.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
Phylopic.attribution(uuid::UUIDs.UUID)
Generates a string for the attribution of an image, as identified by its `uuid`. This string is markdown-formatted, and will include a link to the license.
"""
function attribution(uuid::UUIDs.UUID)
data = Phylopic.images_data(uuid)
contributorname = data["attribution"]
nodename = data["_links"]["specificNode"]["title"]
license = data["_links"]["license"]["href"]
attr_string = "Image of *$(nodename)* provided by [$(contributorname)]($license)"
return Markdown.parse(attr_string)
end

attribution(pair::Pair{String,UUIDs.UUID}; kwargs...) = attribution(pair.second; kwargs...)
attribution(dict::Dict{String,UUIDs.UUID}; kwargs...) = attribution.(collect(dict); kwargs...)
24 changes: 24 additions & 0 deletions Phylopic/src/autocomplete.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
Phylopic.autocomplete(query::AbstractString)
Performs an autocomplete query based on a string, which must be at least two characters in length.
This function will return an *array* of strings, which can be empty if there are no matches. In you want to do things depending on the values returned, check them with `isempty`, not `isnothing`.
The output of this function, when not empty, can be passed to either `Phylopic.nodes` or `Phylopic.images` using their `filter_name` keyword argument. Note that the `filter_name` argument accepts a *single* name, not an array of names.
"""
function autocomplete(query::AbstractString)
if length(query) < 2
throw(ArgumentError("The query ($(query)) must be at least two characters"))
end
req = HTTP.get(Phylopic.api * "autocomplete?query=$(query)")
if isequal(200)(req.status)
matches = JSON.parse(String(req.body))["matches"]
if ~isempty(matches)
return convert(Vector{String}, matches)
end
end
# WARNING: This probably should not return an empty list here, maybe a `nothing` would
# make more sense
return AbstractString[]
end
67 changes: 67 additions & 0 deletions Phylopic/src/images.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Phylopic.vector(uuid::UUIDs.UUID)
Returns the URL (if it exists) to the original vector image for the silhouette. Note that the image must be identified by its UUID, not by a string.
"""
function vector(uuid::UUIDs.UUID)
lnks = Phylopic.images_links(uuid)
return lnks["vectorFile"]["href"]
end

"""
Phylopic.twitterimage(uuid::UUIDs.UUID)
Returns the twitter image for a UUID.
"""
function twitterimage(uuid::UUIDs.UUID)
links = Phylopic.images_links(uuid)
return links["twitter:image"]["href"]
end

"""
Phylopic.source(uuid::UUIDs.UUID)
Returns the source image for a UUID.
"""
function source(uuid::UUIDs.UUID)
links = Phylopic.images_links(uuid)
return links["sourceFile"]["href"]
end

"""
Phylopic.thumbnail(uuid::UUIDs.UUID; resolution=192)
Returns the URL (if it exists) to the thumbnails for the silhouette. The thumbnail `resolution` can be `64`, `128`, or `192` (the default).
"""
function thumbnail(uuid::UUIDs.UUID; resolution=192)
@assert resolution in [64, 128, 192]
lnks = Phylopic.images_links(uuid)
required_size = "$(resolution)x$(resolution)"
img = filter(f -> f["sizes"] == required_size, lnks["thumbnailFiles"])
return only(img)["href"]
end

thumbnail(pair::Pair{String,UUIDs.UUID}; kwargs...) = thumbnail(pair.second; kwargs...)
thumbnail(dict::Dict{String,UUIDs.UUID}; kwargs...) = thumbnail.(collect(dict); kwargs...)
vector(pair::Pair{String,UUIDs.UUID}; kwargs...) = vector(pair.second; kwargs...)
vector(dict::Dict{String,UUIDs.UUID}; kwargs...) = vector.(collect(dict); kwargs...)
twitterimage(pair::Pair{String,UUIDs.UUID}; kwargs...) = twitterimage(pair.second; kwargs...)
twitterimage(dict::Dict{String,UUIDs.UUID}; kwargs...) = twitterimage.(collect(dict); kwargs...)
source(pair::Pair{String,UUIDs.UUID}; kwargs...) = source(pair.second; kwargs...)
source(dict::Dict{String,UUIDs.UUID}; kwargs...) = source.(collect(dict); kwargs...)

function images_data(uuid::UUIDs.UUID)
query = [
"build" => Phylopic.buildnumber,
]
req = HTTP.get(Phylopic.api * "images/$(string(uuid))", query=query)
if isequal(200)(req.status)
response = JSON.parse(String(req.body))
return response
end
return nothing
end

function images_links(uuid::UUIDs.UUID)
return images_data(uuid)["_links"]
end
69 changes: 69 additions & 0 deletions Phylopic/src/imagesof.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""
_get_uuids_at_page(query, page)
This function is an internal helped function to return an array of pairs, wherein each pair maps a name to a UUID, for a given query and page. These outpurs are collected in a dictionary by `Phylopic.names`.
"""
function _get_uuids_at_page(query, page)
page_query = push!(query, "page" => page - 1)
req = HTTP.get(Phylopic.api * "images", query=page_query)
if isequal(200)(req.status)
response = JSON.parse(String(req.body))
return [item["title"] => UUIDs.UUID(replace(item["href"], "/images/" => "", "?build=$(Phylopic.buildnumber)" => "")) for item in response["_links"]["items"]]
end
return nothing
end

"""
imagesof(name::AbstractString; items=1, attribution=false, sharealike=false, nocommercial=false)
Returns a mapping between names and UUIDs of images for a given text (see also `Phylopic.autocomplete` to find relevant names). By default, the search will return images that come without BY, SA, and NC clauses (*i.e.* public domain dedication), but this can be changed using the keyword arguments.
`items`
: Default to 1
: Specifies the number of items to return. When a single item is returned, it is return as a pair mapping the name to the UUID; when there are more than 1, they are returned as a dictionary
`attribution`
: Default to `false`
: Specifies whether the images returned require attribution to the creator
`sharealike`
: Default to `false`
: Specifies whether the images returned require sharing of derived products using a license with a SA clause
`nocommercial`
: Default to `false`
: Specifies whether the images returned are prevented from being used in commercial projects
"""
function imagesof(name::AbstractString; items=1, attribution=false, sharealike=false, nocommercial=false)
name = lowercase(replace(name, r"\s+" => " "))
query = [
"filter_name" => name,
"filter_license_by" => attribution,
"filter_license_nc" => nocommercial,
"filter_license_sa" => sharealike,
"build" => Phylopic.buildnumber
]
req = HTTP.get(Phylopic.api * "images", query=query)
if isequal(200)(req.status)
response = JSON.parse(String(req.body))
if iszero(response["totalItems"])
return nothing
end
if response["totalItems"] < items
@warn "Only $(response["totalItems"]) are available, you requested $(items)"
end
n_pages = ceil(items / response["itemsPerPage"])
uuids = vcat([_get_uuids_at_page(query, page) for page in n_pages]...)
if isone(length(uuids))
return only(uuids)
else
toreturn = min(items, response["totalItems"])
if isone(toreturn)
return first(uuids)
else
return Dict(uuids[1:toreturn])
end
end
end
return nothing
end
34 changes: 34 additions & 0 deletions Phylopic/src/ping.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import HTTP
import JSON

"""
Phylopic.ping()
This function will perform a simple ping of the API, and return `nothing` if it is responding, and throw and `ErrorException` (containing the string `"not responding"`) if the API does not returns a `204 No Content` success status.
"""
function ping()
req = HTTP.get(Phylopic.api * "ping")
if isequal(204)(req.status)
return nothing
else
throw(ErrorException("The API at $(Phylopic.api) is not responding"))
end
return nothing
end

"""
Phylopic.build()
Returns the current build to perform the queries
"""
function build()
req = HTTP.get(Phylopic.api)
if isequal(200)(req.status)
infostring = JSON.parse(String(req.body))
return infostring["build"]
else
throw(ErrorException("The API at $(Phylopic.api) is not responding"))
end
return nothing

end
2 changes: 2 additions & 0 deletions Phylopic/test/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[deps]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
8 changes: 8 additions & 0 deletions Phylopic/test/autocomplete.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module TestPhylopicAutocomplete

using Test
using Phylopic

@test ~isempty(Phylopic.autocomplete("chiro"))

end
9 changes: 9 additions & 0 deletions Phylopic/test/imagesof.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module TestPhylopicNames

using Phylopic
using Test

@test typeof(Phylopic.imagesof("chiroptera")) == Pair{String,Base.UUID}
@test typeof(Phylopic.imagesof("chiroptera"; items=2)) == Dict{String,Base.UUID}

end
8 changes: 8 additions & 0 deletions Phylopic/test/ping.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module TestPhylopicPing

using Phylopic
using Test

@test isnothing(Phylopic.ping())

end
Loading

2 comments on commit 12d567c

@tpoisot
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register subdir="Phylopic"

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/82334

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a Phylopic-v0.0.1 -m "<description of version>" 12d567c88546d7d44dee16a425c9c71330cca805
git push origin Phylopic-v0.0.1

Please sign in to comment.