citedhealth 0.2.0 → 0.4.1
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 +4 -4
- data/exe/citedhealth +7 -0
- data/lib/citedhealth/cli.rb +227 -0
- data/lib/citedhealth/client.rb +70 -1
- data/lib/citedhealth/types.rb +68 -4
- data/lib/citedhealth/version.rb +1 -1
- metadata +11 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4d8d98d5d0436b85f88bf33f0533b1f321dfb353fb447b04dc7ddf6257c942b
|
|
4
|
+
data.tar.gz: 4397209a7ce632b899cd4b22e1474bddb2ce329bf33e135c21cdb6ed2584d517
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e85ad6f1ed398cfc39fbb2c8221f9f64ed1f4cd2825e93e489cff2eabeba8ed29b894c1505f312f63dce45404efa02669951c458405130c17099533d0c65f39
|
|
7
|
+
data.tar.gz: 8475541e7d88b3134e0fc897c3fcd2523057078187d3a60977043dccceca983b511129731e098c9755d66cbf2a66fc3b3514bf961fdec5bb7227c76e9428fd17
|
data/exe/citedhealth
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module CitedHealth
|
|
7
|
+
# Command-line interface for the Cited Health API.
|
|
8
|
+
#
|
|
9
|
+
# Provides 5 subcommands to search ingredients, evidence, and papers
|
|
10
|
+
# from the terminal. Zero runtime dependencies beyond Ruby stdlib.
|
|
11
|
+
#
|
|
12
|
+
# citedhealth ingredients biotin
|
|
13
|
+
# citedhealth ingredient vitamin-d
|
|
14
|
+
# citedhealth evidence biotin hair-loss
|
|
15
|
+
# citedhealth papers --year 2024
|
|
16
|
+
# citedhealth paper 12345678
|
|
17
|
+
#
|
|
18
|
+
module CLI
|
|
19
|
+
USAGE = <<~HELP
|
|
20
|
+
Usage: citedhealth <command> [options]
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
ingredients [QUERY] [-c CATEGORY] Search ingredients
|
|
24
|
+
ingredient SLUG Get ingredient details
|
|
25
|
+
evidence INGREDIENT CONDITION Get evidence for a pair
|
|
26
|
+
papers [QUERY] [-y YEAR] Search research papers
|
|
27
|
+
paper PMID Get paper by PubMed ID
|
|
28
|
+
conditions [--featured] List health conditions
|
|
29
|
+
condition SLUG Get condition details
|
|
30
|
+
glossary [-c CATEGORY] List glossary terms
|
|
31
|
+
glossary-term SLUG Get glossary term details
|
|
32
|
+
guides [-c CATEGORY] List guides
|
|
33
|
+
guide SLUG Get guide details
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--json Compact JSON output
|
|
37
|
+
--version Show version
|
|
38
|
+
--help Show this help
|
|
39
|
+
HELP
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
def run(argv)
|
|
43
|
+
args = argv.dup
|
|
44
|
+
compact = extract_flag!(args, "--json")
|
|
45
|
+
|
|
46
|
+
if extract_flag!(args, "--version")
|
|
47
|
+
puts "citedhealth #{CitedHealth::VERSION}"
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if args.empty? || extract_flag!(args, "--help")
|
|
52
|
+
puts USAGE
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
command = args.shift
|
|
57
|
+
client = CitedHealth::Client.new
|
|
58
|
+
|
|
59
|
+
result = case command
|
|
60
|
+
when "ingredients" then cmd_ingredients(client, args)
|
|
61
|
+
when "ingredient" then cmd_ingredient(client, args)
|
|
62
|
+
when "evidence" then cmd_evidence(client, args)
|
|
63
|
+
when "papers" then cmd_papers(client, args)
|
|
64
|
+
when "paper" then cmd_paper(client, args)
|
|
65
|
+
when "conditions" then cmd_conditions(client, args)
|
|
66
|
+
when "condition" then cmd_condition(client, args)
|
|
67
|
+
when "glossary" then cmd_glossary(client, args)
|
|
68
|
+
when "glossary-term" then cmd_glossary_term(client, args)
|
|
69
|
+
when "guides" then cmd_guides(client, args)
|
|
70
|
+
when "guide" then cmd_guide(client, args)
|
|
71
|
+
else
|
|
72
|
+
warn "Unknown command: #{command}"
|
|
73
|
+
warn USAGE
|
|
74
|
+
exit 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
output(result, compact: compact)
|
|
78
|
+
rescue CitedHealth::NotFoundError => e
|
|
79
|
+
warn "Error: #{e.message}"
|
|
80
|
+
exit 1
|
|
81
|
+
rescue CitedHealth::RateLimitError => e
|
|
82
|
+
msg = "Error: Rate limit exceeded"
|
|
83
|
+
msg += " (retry after #{e.retry_after}s)" if e.retry_after
|
|
84
|
+
warn msg
|
|
85
|
+
exit 1
|
|
86
|
+
rescue CitedHealth::Error => e
|
|
87
|
+
warn "Error: #{e.message}"
|
|
88
|
+
exit 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def extract_flag!(args, flag)
|
|
94
|
+
if args.include?(flag)
|
|
95
|
+
args.delete(flag)
|
|
96
|
+
true
|
|
97
|
+
else
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cmd_ingredients(client, args)
|
|
103
|
+
category = extract_option!(args, "-c")
|
|
104
|
+
query = args.join(" ")
|
|
105
|
+
results = client.search_ingredients(query: query, category: category || "")
|
|
106
|
+
results.map { |i| to_hash(i) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def cmd_ingredient(client, args)
|
|
110
|
+
slug = args.shift
|
|
111
|
+
if slug.nil? || slug.empty?
|
|
112
|
+
warn "Usage: citedhealth ingredient SLUG"
|
|
113
|
+
exit 1
|
|
114
|
+
end
|
|
115
|
+
to_hash(client.get_ingredient(slug))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def cmd_evidence(client, args)
|
|
119
|
+
ingredient_slug = args.shift
|
|
120
|
+
condition_slug = args.shift
|
|
121
|
+
if ingredient_slug.nil? || condition_slug.nil?
|
|
122
|
+
warn "Usage: citedhealth evidence INGREDIENT CONDITION"
|
|
123
|
+
exit 1
|
|
124
|
+
end
|
|
125
|
+
to_hash(client.get_evidence(
|
|
126
|
+
ingredient_slug: ingredient_slug,
|
|
127
|
+
condition_slug: condition_slug
|
|
128
|
+
))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def cmd_papers(client, args)
|
|
132
|
+
year_str = extract_option!(args, "-y")
|
|
133
|
+
year = year_str&.to_i
|
|
134
|
+
query = args.join(" ")
|
|
135
|
+
results = client.search_papers(query: query, year: year)
|
|
136
|
+
results.map { |p| to_hash(p) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def cmd_paper(client, args)
|
|
140
|
+
pmid = args.shift
|
|
141
|
+
if pmid.nil? || pmid.empty?
|
|
142
|
+
warn "Usage: citedhealth paper PMID"
|
|
143
|
+
exit 1
|
|
144
|
+
end
|
|
145
|
+
to_hash(client.get_paper(pmid))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def cmd_conditions(client, args)
|
|
149
|
+
featured = extract_flag!(args, "--featured")
|
|
150
|
+
is_featured = featured ? true : nil
|
|
151
|
+
results = client.list_conditions(is_featured: is_featured)
|
|
152
|
+
results.map { |c| to_hash(c) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def cmd_condition(client, args)
|
|
156
|
+
slug = args.shift
|
|
157
|
+
if slug.nil? || slug.empty?
|
|
158
|
+
warn "Usage: citedhealth condition SLUG"
|
|
159
|
+
exit 1
|
|
160
|
+
end
|
|
161
|
+
to_hash(client.get_condition(slug))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def cmd_glossary(client, args)
|
|
165
|
+
category = extract_option!(args, "-c")
|
|
166
|
+
results = client.list_glossary(category: category)
|
|
167
|
+
results.map { |t| to_hash(t) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def cmd_glossary_term(client, args)
|
|
171
|
+
slug = args.shift
|
|
172
|
+
if slug.nil? || slug.empty?
|
|
173
|
+
warn "Usage: citedhealth glossary-term SLUG"
|
|
174
|
+
exit 1
|
|
175
|
+
end
|
|
176
|
+
to_hash(client.get_glossary_term(slug))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def cmd_guides(client, args)
|
|
180
|
+
category = extract_option!(args, "-c")
|
|
181
|
+
results = client.list_guides(category: category)
|
|
182
|
+
results.map { |g| to_hash(g) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def cmd_guide(client, args)
|
|
186
|
+
slug = args.shift
|
|
187
|
+
if slug.nil? || slug.empty?
|
|
188
|
+
warn "Usage: citedhealth guide SLUG"
|
|
189
|
+
exit 1
|
|
190
|
+
end
|
|
191
|
+
to_hash(client.get_guide(slug))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def extract_option!(args, flag)
|
|
195
|
+
idx = args.index(flag)
|
|
196
|
+
return nil unless idx
|
|
197
|
+
|
|
198
|
+
args.delete_at(idx) # remove flag
|
|
199
|
+
args.delete_at(idx) # remove value
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def to_hash(obj)
|
|
203
|
+
hash = {}
|
|
204
|
+
obj.instance_variables.each do |var|
|
|
205
|
+
key = var.to_s.delete_prefix("@")
|
|
206
|
+
value = obj.instance_variable_get(var)
|
|
207
|
+
hash[key] = case value
|
|
208
|
+
when CitedHealth::NestedIngredient, CitedHealth::Condition,
|
|
209
|
+
CitedHealth::GlossaryTerm, CitedHealth::Guide
|
|
210
|
+
to_hash(value)
|
|
211
|
+
else
|
|
212
|
+
value
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
hash
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def output(data, compact:)
|
|
219
|
+
if compact
|
|
220
|
+
puts JSON.generate(data)
|
|
221
|
+
else
|
|
222
|
+
puts JSON.pretty_generate(data)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
data/lib/citedhealth/client.rb
CHANGED
|
@@ -15,7 +15,7 @@ module CitedHealth
|
|
|
15
15
|
# puts ingredient.name # => "Vitamin D"
|
|
16
16
|
#
|
|
17
17
|
class Client
|
|
18
|
-
DEFAULT_BASE_URL = "https://
|
|
18
|
+
DEFAULT_BASE_URL = "https://haircited.com"
|
|
19
19
|
DEFAULT_TIMEOUT = 30
|
|
20
20
|
|
|
21
21
|
def initialize(base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT)
|
|
@@ -97,6 +97,75 @@ module CitedHealth
|
|
|
97
97
|
Paper.from_hash(data)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# List health conditions, optionally filtered by featured status.
|
|
101
|
+
#
|
|
102
|
+
# @param is_featured [Boolean, nil] filter by featured status
|
|
103
|
+
# @return [Array<Condition>] list of conditions
|
|
104
|
+
def list_conditions(is_featured: nil)
|
|
105
|
+
params = {}
|
|
106
|
+
params[:is_featured] = is_featured.to_s unless is_featured.nil?
|
|
107
|
+
|
|
108
|
+
data = get("/api/conditions/", params)
|
|
109
|
+
results = data["results"] || []
|
|
110
|
+
results.map { |h| Condition.from_hash(h) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get a single condition by slug.
|
|
114
|
+
#
|
|
115
|
+
# @param slug [String] condition slug (e.g. "hair-loss")
|
|
116
|
+
# @return [Condition]
|
|
117
|
+
# @raise [NotFoundError] if the condition does not exist
|
|
118
|
+
def get_condition(slug)
|
|
119
|
+
data = get("/api/conditions/#{slug}/")
|
|
120
|
+
Condition.from_hash(data)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# List glossary terms, optionally filtered by category.
|
|
124
|
+
#
|
|
125
|
+
# @param category [String, nil] filter by category
|
|
126
|
+
# @return [Array<GlossaryTerm>] list of glossary terms
|
|
127
|
+
def list_glossary(category: nil)
|
|
128
|
+
params = {}
|
|
129
|
+
params[:category] = category unless category.nil?
|
|
130
|
+
|
|
131
|
+
data = get("/api/glossary/", params)
|
|
132
|
+
results = data["results"] || []
|
|
133
|
+
results.map { |h| GlossaryTerm.from_hash(h) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Get a single glossary term by slug.
|
|
137
|
+
#
|
|
138
|
+
# @param slug [String] glossary term slug
|
|
139
|
+
# @return [GlossaryTerm]
|
|
140
|
+
# @raise [NotFoundError] if the glossary term does not exist
|
|
141
|
+
def get_glossary_term(slug)
|
|
142
|
+
data = get("/api/glossary/#{slug}/")
|
|
143
|
+
GlossaryTerm.from_hash(data)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# List guides, optionally filtered by category.
|
|
147
|
+
#
|
|
148
|
+
# @param category [String, nil] filter by category
|
|
149
|
+
# @return [Array<Guide>] list of guides
|
|
150
|
+
def list_guides(category: nil)
|
|
151
|
+
params = {}
|
|
152
|
+
params[:category] = category unless category.nil?
|
|
153
|
+
|
|
154
|
+
data = get("/api/guides/", params)
|
|
155
|
+
results = data["results"] || []
|
|
156
|
+
results.map { |h| Guide.from_hash(h) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Get a single guide by slug.
|
|
160
|
+
#
|
|
161
|
+
# @param slug [String] guide slug
|
|
162
|
+
# @return [Guide]
|
|
163
|
+
# @raise [NotFoundError] if the guide does not exist
|
|
164
|
+
def get_guide(slug)
|
|
165
|
+
data = get("/api/guides/#{slug}/")
|
|
166
|
+
Guide.from_hash(data)
|
|
167
|
+
end
|
|
168
|
+
|
|
100
169
|
private
|
|
101
170
|
|
|
102
171
|
def get(path, params = {})
|
data/lib/citedhealth/types.rb
CHANGED
|
@@ -32,19 +32,83 @@ module CitedHealth
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# A health condition referenced in evidence links.
|
|
35
|
+
# A health condition referenced in evidence links and the conditions API.
|
|
36
36
|
class Condition
|
|
37
|
-
attr_reader :slug, :name
|
|
37
|
+
attr_reader :slug, :name, :description, :meta_description,
|
|
38
|
+
:prevalence, :symptoms, :risk_factors, :is_featured
|
|
38
39
|
|
|
39
|
-
def initialize(slug:, name:
|
|
40
|
+
def initialize(slug:, name:, description: "", meta_description: "",
|
|
41
|
+
prevalence: "", symptoms: [], risk_factors: [], is_featured: false)
|
|
40
42
|
@slug = slug
|
|
41
43
|
@name = name
|
|
44
|
+
@description = description
|
|
45
|
+
@meta_description = meta_description
|
|
46
|
+
@prevalence = prevalence
|
|
47
|
+
@symptoms = symptoms
|
|
48
|
+
@risk_factors = risk_factors
|
|
49
|
+
@is_featured = is_featured
|
|
42
50
|
end
|
|
43
51
|
|
|
44
52
|
def self.from_hash(hash)
|
|
45
53
|
new(
|
|
46
54
|
slug: hash["slug"],
|
|
47
|
-
name: hash["name"]
|
|
55
|
+
name: hash["name"],
|
|
56
|
+
description: hash["description"] || "",
|
|
57
|
+
meta_description: hash["meta_description"] || "",
|
|
58
|
+
prevalence: hash["prevalence"] || "",
|
|
59
|
+
symptoms: hash["symptoms"] || [],
|
|
60
|
+
risk_factors: hash["risk_factors"] || [],
|
|
61
|
+
is_featured: hash["is_featured"] || false
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A glossary term explaining health and supplement terminology.
|
|
67
|
+
class GlossaryTerm
|
|
68
|
+
attr_reader :slug, :term, :short_definition, :definition,
|
|
69
|
+
:abbreviation, :category
|
|
70
|
+
|
|
71
|
+
def initialize(slug:, term:, short_definition: "", definition: "",
|
|
72
|
+
abbreviation: "", category: "")
|
|
73
|
+
@slug = slug
|
|
74
|
+
@term = term
|
|
75
|
+
@short_definition = short_definition
|
|
76
|
+
@definition = definition
|
|
77
|
+
@abbreviation = abbreviation
|
|
78
|
+
@category = category
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.from_hash(hash)
|
|
82
|
+
new(
|
|
83
|
+
slug: hash["slug"],
|
|
84
|
+
term: hash["term"],
|
|
85
|
+
short_definition: hash["short_definition"] || "",
|
|
86
|
+
definition: hash["definition"] || "",
|
|
87
|
+
abbreviation: hash["abbreviation"] || "",
|
|
88
|
+
category: hash["category"] || ""
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A curated guide on health topics and supplement usage.
|
|
94
|
+
class Guide
|
|
95
|
+
attr_reader :slug, :title, :content, :category, :meta_description
|
|
96
|
+
|
|
97
|
+
def initialize(slug:, title:, content: "", category: "", meta_description: "")
|
|
98
|
+
@slug = slug
|
|
99
|
+
@title = title
|
|
100
|
+
@content = content
|
|
101
|
+
@category = category
|
|
102
|
+
@meta_description = meta_description
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.from_hash(hash)
|
|
106
|
+
new(
|
|
107
|
+
slug: hash["slug"],
|
|
108
|
+
title: hash["title"],
|
|
109
|
+
content: hash["content"] || "",
|
|
110
|
+
category: hash["category"] || "",
|
|
111
|
+
meta_description: hash["meta_description"] || ""
|
|
48
112
|
)
|
|
49
113
|
end
|
|
50
114
|
end
|
data/lib/citedhealth/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: citedhealth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cited Health
|
|
8
|
-
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-03-30 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: minitest
|
|
@@ -55,11 +56,14 @@ description: API client for citedhealth.com. Search ingredients, evidence links,
|
|
|
55
56
|
research papers for evidence-based health supplement information. Zero dependencies.
|
|
56
57
|
email:
|
|
57
58
|
- hello@citedhealth.com
|
|
58
|
-
executables:
|
|
59
|
+
executables:
|
|
60
|
+
- citedhealth
|
|
59
61
|
extensions: []
|
|
60
62
|
extra_rdoc_files: []
|
|
61
63
|
files:
|
|
64
|
+
- exe/citedhealth
|
|
62
65
|
- lib/citedhealth.rb
|
|
66
|
+
- lib/citedhealth/cli.rb
|
|
63
67
|
- lib/citedhealth/client.rb
|
|
64
68
|
- lib/citedhealth/errors.rb
|
|
65
69
|
- lib/citedhealth/types.rb
|
|
@@ -73,6 +77,7 @@ metadata:
|
|
|
73
77
|
changelog_uri: https://github.com/citedhealth/citedhealth-rb/blob/main/CHANGELOG.md
|
|
74
78
|
documentation_uri: https://citedhealth.com/developers/
|
|
75
79
|
bug_tracker_uri: https://github.com/citedhealth/citedhealth-rb/issues
|
|
80
|
+
post_install_message:
|
|
76
81
|
rdoc_options: []
|
|
77
82
|
require_paths:
|
|
78
83
|
- lib
|
|
@@ -87,7 +92,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
92
|
- !ruby/object:Gem::Version
|
|
88
93
|
version: '0'
|
|
89
94
|
requirements: []
|
|
90
|
-
rubygems_version:
|
|
95
|
+
rubygems_version: 3.0.3.1
|
|
96
|
+
signing_key:
|
|
91
97
|
specification_version: 4
|
|
92
98
|
summary: Ruby client for the Cited Health evidence-based supplement API
|
|
93
99
|
test_files: []
|