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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1412f03c7f37f1c6e05d1cf651bd2185e9fe0a3697793bb352387199f907acf
4
- data.tar.gz: 3237403eebbb4a5710df421e9462e43353a9c9eb8f772f1b99617e08695aee36
3
+ metadata.gz: e4d8d98d5d0436b85f88bf33f0533b1f321dfb353fb447b04dc7ddf6257c942b
4
+ data.tar.gz: 4397209a7ce632b899cd4b22e1474bddb2ce329bf33e135c21cdb6ed2584d517
5
5
  SHA512:
6
- metadata.gz: 7fa44b8d9936beb53096330f88563dfcde9a17b4d45e8383da02ba4bd0e6ee937fbcade18f5eeae66d2989f7d9f7baec811c2850d7b88062265593648045ea1c
7
- data.tar.gz: ded37c88e38fe195e3efe64a7e6383dbd10105621b1d6c68e8e915a41cfed52288caf78bd9441863e1e6526c001287a74aeaa4dc50284b69b1b215757ba8fc3c
6
+ metadata.gz: 1e85ad6f1ed398cfc39fbb2c8221f9f64ed1f4cd2825e93e489cff2eabeba8ed29b894c1505f312f63dce45404efa02669951c458405130c17099533d0c65f39
7
+ data.tar.gz: 8475541e7d88b3134e0fc897c3fcd2523057078187d3a60977043dccceca983b511129731e098c9755d66cbf2a66fc3b3514bf961fdec5bb7227c76e9428fd17
data/exe/citedhealth ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "citedhealth"
5
+ require "citedhealth/cli"
6
+
7
+ CitedHealth::CLI.run(ARGV)
@@ -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
@@ -15,7 +15,7 @@ module CitedHealth
15
15
  # puts ingredient.name # => "Vitamin D"
16
16
  #
17
17
  class Client
18
- DEFAULT_BASE_URL = "https://citedhealth.com"
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 = {})
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CitedHealth
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.1"
5
5
  end
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.2.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cited Health
8
- bindir: bin
8
+ autorequire:
9
+ bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
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: 4.0.3
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: []