citedhealth 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/citedhealth/client.rb +130 -0
- data/lib/citedhealth/errors.rb +20 -0
- data/lib/citedhealth/types.rb +139 -0
- data/lib/citedhealth/version.rb +5 -0
- data/lib/citedhealth.rb +6 -0
- metadata +93 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d1412f03c7f37f1c6e05d1cf651bd2185e9fe0a3697793bb352387199f907acf
|
|
4
|
+
data.tar.gz: 3237403eebbb4a5710df421e9462e43353a9c9eb8f772f1b99617e08695aee36
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7fa44b8d9936beb53096330f88563dfcde9a17b4d45e8383da02ba4bd0e6ee937fbcade18f5eeae66d2989f7d9f7baec811c2850d7b88062265593648045ea1c
|
|
7
|
+
data.tar.gz: ded37c88e38fe195e3efe64a7e6383dbd10105621b1d6c68e8e915a41cfed52288caf78bd9441863e1e6526c001287a74aeaa4dc50284b69b1b215757ba8fc3c
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module CitedHealth
|
|
8
|
+
# HTTP client for the Cited Health REST API.
|
|
9
|
+
#
|
|
10
|
+
# All methods return typed Ruby objects (Ingredient, Paper, EvidenceLink).
|
|
11
|
+
# Zero runtime dependencies — uses only Ruby stdlib (net/http, json, uri).
|
|
12
|
+
#
|
|
13
|
+
# client = CitedHealth::Client.new
|
|
14
|
+
# ingredient = client.get_ingredient("vitamin-d")
|
|
15
|
+
# puts ingredient.name # => "Vitamin D"
|
|
16
|
+
#
|
|
17
|
+
class Client
|
|
18
|
+
DEFAULT_BASE_URL = "https://citedhealth.com"
|
|
19
|
+
DEFAULT_TIMEOUT = 30
|
|
20
|
+
|
|
21
|
+
def initialize(base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
22
|
+
@base_url = base_url.chomp("/")
|
|
23
|
+
@timeout = timeout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Search ingredients by query and/or category.
|
|
27
|
+
#
|
|
28
|
+
# @param query [String] search term (default: "")
|
|
29
|
+
# @param category [String] filter by category (default: "")
|
|
30
|
+
# @return [Array<Ingredient>] list of matching ingredients
|
|
31
|
+
def search_ingredients(query: "", category: "")
|
|
32
|
+
params = {}
|
|
33
|
+
params[:q] = query unless query.empty?
|
|
34
|
+
params[:category] = category unless category.empty?
|
|
35
|
+
|
|
36
|
+
data = get("/api/ingredients/", params)
|
|
37
|
+
results = data["results"] || []
|
|
38
|
+
results.map { |h| Ingredient.from_hash(h) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Get a single ingredient by slug.
|
|
42
|
+
#
|
|
43
|
+
# @param slug [String] ingredient slug (e.g. "vitamin-d")
|
|
44
|
+
# @return [Ingredient]
|
|
45
|
+
# @raise [NotFoundError] if the ingredient does not exist
|
|
46
|
+
def get_ingredient(slug)
|
|
47
|
+
data = get("/api/ingredients/#{slug}/")
|
|
48
|
+
Ingredient.from_hash(data)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get evidence linking an ingredient to a condition.
|
|
52
|
+
#
|
|
53
|
+
# @param ingredient_slug [String] ingredient slug
|
|
54
|
+
# @param condition_slug [String] condition slug
|
|
55
|
+
# @return [EvidenceLink] the first matching evidence link
|
|
56
|
+
# @raise [NotFoundError] if no evidence exists for the pair
|
|
57
|
+
def get_evidence(ingredient_slug:, condition_slug:)
|
|
58
|
+
data = get("/api/evidence/", ingredient: ingredient_slug, condition: condition_slug)
|
|
59
|
+
results = data["results"] || []
|
|
60
|
+
raise NotFoundError, "No evidence found: #{ingredient_slug} × #{condition_slug}" if results.empty?
|
|
61
|
+
|
|
62
|
+
EvidenceLink.from_hash(results.first)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get a single evidence link by ID.
|
|
66
|
+
#
|
|
67
|
+
# @param id [Integer] evidence link ID
|
|
68
|
+
# @return [EvidenceLink]
|
|
69
|
+
# @raise [NotFoundError] if the evidence link does not exist
|
|
70
|
+
def get_evidence_by_id(id)
|
|
71
|
+
data = get("/api/evidence/#{id}/")
|
|
72
|
+
EvidenceLink.from_hash(data)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Search research papers by query and/or publication year.
|
|
76
|
+
#
|
|
77
|
+
# @param query [String] search term (default: "")
|
|
78
|
+
# @param year [Integer, nil] filter by publication year
|
|
79
|
+
# @return [Array<Paper>] list of matching papers
|
|
80
|
+
def search_papers(query: "", year: nil)
|
|
81
|
+
params = {}
|
|
82
|
+
params[:q] = query unless query.empty?
|
|
83
|
+
params[:year] = year.to_s unless year.nil?
|
|
84
|
+
|
|
85
|
+
data = get("/api/papers/", params)
|
|
86
|
+
results = data["results"] || []
|
|
87
|
+
results.map { |h| Paper.from_hash(h) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Get a single paper by PubMed ID.
|
|
91
|
+
#
|
|
92
|
+
# @param pmid [String] PubMed ID
|
|
93
|
+
# @return [Paper]
|
|
94
|
+
# @raise [NotFoundError] if the paper does not exist
|
|
95
|
+
def get_paper(pmid)
|
|
96
|
+
data = get("/api/papers/#{pmid}/")
|
|
97
|
+
Paper.from_hash(data)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def get(path, params = {})
|
|
103
|
+
uri = URI("#{@base_url}#{path}")
|
|
104
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
105
|
+
|
|
106
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
107
|
+
http.use_ssl = uri.scheme == "https"
|
|
108
|
+
http.open_timeout = @timeout
|
|
109
|
+
http.read_timeout = @timeout
|
|
110
|
+
|
|
111
|
+
request = Net::HTTP::Get.new(uri)
|
|
112
|
+
request["Accept"] = "application/json"
|
|
113
|
+
request["User-Agent"] = "citedhealth-rb/#{VERSION}"
|
|
114
|
+
|
|
115
|
+
response = http.request(request)
|
|
116
|
+
|
|
117
|
+
case response
|
|
118
|
+
when Net::HTTPSuccess
|
|
119
|
+
JSON.parse(response.body)
|
|
120
|
+
when Net::HTTPNotFound
|
|
121
|
+
raise NotFoundError, "Not found: #{path}"
|
|
122
|
+
when Net::HTTPTooManyRequests
|
|
123
|
+
retry_after = response["Retry-After"]&.to_i
|
|
124
|
+
raise RateLimitError.new("Rate limit exceeded", retry_after: retry_after)
|
|
125
|
+
else
|
|
126
|
+
raise Error, "HTTP #{response.code}: #{response.body}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CitedHealth
|
|
4
|
+
# General API error.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when the server returns HTTP 404.
|
|
8
|
+
class NotFoundError < Error; end
|
|
9
|
+
|
|
10
|
+
# Raised when the server returns HTTP 429.
|
|
11
|
+
# Exposes the Retry-After header value when present.
|
|
12
|
+
class RateLimitError < Error
|
|
13
|
+
attr_reader :retry_after
|
|
14
|
+
|
|
15
|
+
def initialize(message = "Rate limit exceeded", retry_after: nil)
|
|
16
|
+
@retry_after = retry_after
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CitedHealth
|
|
4
|
+
# A health ingredient with dosage and form information.
|
|
5
|
+
class Ingredient
|
|
6
|
+
attr_reader :id, :name, :slug, :category, :mechanism,
|
|
7
|
+
:recommended_dosage, :forms, :is_featured
|
|
8
|
+
|
|
9
|
+
def initialize(id:, name:, slug:, category: "", mechanism: "",
|
|
10
|
+
recommended_dosage: {}, forms: [], is_featured: false)
|
|
11
|
+
@id = id
|
|
12
|
+
@name = name
|
|
13
|
+
@slug = slug
|
|
14
|
+
@category = category
|
|
15
|
+
@mechanism = mechanism
|
|
16
|
+
@recommended_dosage = recommended_dosage
|
|
17
|
+
@forms = forms
|
|
18
|
+
@is_featured = is_featured
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.from_hash(hash)
|
|
22
|
+
new(
|
|
23
|
+
id: hash["id"],
|
|
24
|
+
name: hash["name"],
|
|
25
|
+
slug: hash["slug"],
|
|
26
|
+
category: hash["category"] || "",
|
|
27
|
+
mechanism: hash["mechanism"] || "",
|
|
28
|
+
recommended_dosage: hash["recommended_dosage"] || {},
|
|
29
|
+
forms: hash["forms"] || [],
|
|
30
|
+
is_featured: hash["is_featured"] || false
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A health condition referenced in evidence links.
|
|
36
|
+
class Condition
|
|
37
|
+
attr_reader :slug, :name
|
|
38
|
+
|
|
39
|
+
def initialize(slug:, name:)
|
|
40
|
+
@slug = slug
|
|
41
|
+
@name = name
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.from_hash(hash)
|
|
45
|
+
new(
|
|
46
|
+
slug: hash["slug"],
|
|
47
|
+
name: hash["name"]
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A research paper from PubMed.
|
|
53
|
+
class Paper
|
|
54
|
+
attr_reader :id, :pmid, :title, :journal, :publication_year,
|
|
55
|
+
:study_type, :citation_count, :is_open_access, :pubmed_link
|
|
56
|
+
|
|
57
|
+
def initialize(id:, pmid:, title:, journal: "", publication_year: nil,
|
|
58
|
+
study_type: "", citation_count: 0, is_open_access: false,
|
|
59
|
+
pubmed_link: "")
|
|
60
|
+
@id = id
|
|
61
|
+
@pmid = pmid
|
|
62
|
+
@title = title
|
|
63
|
+
@journal = journal
|
|
64
|
+
@publication_year = publication_year
|
|
65
|
+
@study_type = study_type
|
|
66
|
+
@citation_count = citation_count
|
|
67
|
+
@is_open_access = is_open_access
|
|
68
|
+
@pubmed_link = pubmed_link
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.from_hash(hash)
|
|
72
|
+
new(
|
|
73
|
+
id: hash["id"],
|
|
74
|
+
pmid: hash["pmid"],
|
|
75
|
+
title: hash["title"],
|
|
76
|
+
journal: hash["journal"] || "",
|
|
77
|
+
publication_year: hash["publication_year"],
|
|
78
|
+
study_type: hash["study_type"] || "",
|
|
79
|
+
citation_count: hash["citation_count"] || 0,
|
|
80
|
+
is_open_access: hash["is_open_access"] || false,
|
|
81
|
+
pubmed_link: hash["pubmed_link"] || ""
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Nested ingredient reference within an evidence link.
|
|
87
|
+
class NestedIngredient
|
|
88
|
+
attr_reader :slug, :name
|
|
89
|
+
|
|
90
|
+
def initialize(slug:, name:)
|
|
91
|
+
@slug = slug
|
|
92
|
+
@name = name
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.from_hash(hash)
|
|
96
|
+
new(
|
|
97
|
+
slug: hash["slug"],
|
|
98
|
+
name: hash["name"]
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# An evidence link connecting an ingredient to a condition with a grade.
|
|
104
|
+
class EvidenceLink
|
|
105
|
+
attr_reader :id, :ingredient, :condition, :grade, :grade_label,
|
|
106
|
+
:summary, :direction, :total_studies, :total_participants
|
|
107
|
+
|
|
108
|
+
def initialize(id:, ingredient:, condition:, grade: "", grade_label: "",
|
|
109
|
+
summary: "", direction: "", total_studies: 0,
|
|
110
|
+
total_participants: 0)
|
|
111
|
+
@id = id
|
|
112
|
+
@ingredient = ingredient
|
|
113
|
+
@condition = condition
|
|
114
|
+
@grade = grade
|
|
115
|
+
@grade_label = grade_label
|
|
116
|
+
@summary = summary
|
|
117
|
+
@direction = direction
|
|
118
|
+
@total_studies = total_studies
|
|
119
|
+
@total_participants = total_participants
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.from_hash(hash)
|
|
123
|
+
ingredient_data = hash["ingredient"] || {}
|
|
124
|
+
condition_data = hash["condition"] || {}
|
|
125
|
+
|
|
126
|
+
new(
|
|
127
|
+
id: hash["id"],
|
|
128
|
+
ingredient: NestedIngredient.from_hash(ingredient_data),
|
|
129
|
+
condition: Condition.from_hash(condition_data),
|
|
130
|
+
grade: hash["grade"] || "",
|
|
131
|
+
grade_label: hash["grade_label"] || "",
|
|
132
|
+
summary: hash["summary"] || "",
|
|
133
|
+
direction: hash["direction"] || "",
|
|
134
|
+
total_studies: hash["total_studies"] || 0,
|
|
135
|
+
total_participants: hash["total_participants"] || 0
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/citedhealth.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: citedhealth
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Cited Health
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: webmock
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
description: API client for citedhealth.com. Search ingredients, evidence links, and
|
|
55
|
+
research papers for evidence-based health supplement information. Zero dependencies.
|
|
56
|
+
email:
|
|
57
|
+
- hello@citedhealth.com
|
|
58
|
+
executables: []
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- lib/citedhealth.rb
|
|
63
|
+
- lib/citedhealth/client.rb
|
|
64
|
+
- lib/citedhealth/errors.rb
|
|
65
|
+
- lib/citedhealth/types.rb
|
|
66
|
+
- lib/citedhealth/version.rb
|
|
67
|
+
homepage: https://citedhealth.com
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
homepage_uri: https://citedhealth.com
|
|
72
|
+
source_code_uri: https://github.com/citedhealth/citedhealth-rb
|
|
73
|
+
changelog_uri: https://github.com/citedhealth/citedhealth-rb/blob/main/CHANGELOG.md
|
|
74
|
+
documentation_uri: https://citedhealth.com/developers/
|
|
75
|
+
bug_tracker_uri: https://github.com/citedhealth/citedhealth-rb/issues
|
|
76
|
+
rdoc_options: []
|
|
77
|
+
require_paths:
|
|
78
|
+
- lib
|
|
79
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '3.0'
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 4.0.3
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Ruby client for the Cited Health evidence-based supplement API
|
|
93
|
+
test_files: []
|