folding_at_home_client 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0b5eed2c78d8ff38ba693988b34c4a0858ae2e3816569ad5fdd672a8fed80f12
4
+ data.tar.gz: f412f9ce32d81ad8008f0241e33bb18a80e991fbb7af3f29c142d80a0053095f
5
+ SHA512:
6
+ metadata.gz: 0f4a86bcdc2a9ae852fc465c82f7c8c8481b28a84c1b44f59e89eb1bb1200ef3ff2d6d65f49765523b76f68bef9eb4d4f32cac81b79b639f6e6d9210537f4929
7
+ data.tar.gz: 478940d0851f6de9d5d48ff909e5dd7c11c0f7e3f923c713d39dd0c3e17ba7fe3c7c6f7c390e3645113b4fb631dcb9cc37541dbfe55e3e6347113e12d8f9fd66
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,42 @@
1
+ # Docs: https://docs.rubocop.org/rubocop/configuration
2
+
3
+ AllCops:
4
+ NewCops: enable
5
+ TargetRubyVersion: 3.1
6
+
7
+ Layout/ArgumentAlignment:
8
+ EnforcedStyle: 'with_fixed_indentation'
9
+ Enabled: true
10
+
11
+ Metrics/MethodLength:
12
+ Enabled: false
13
+
14
+ Metrics/AbcSize:
15
+ Enabled: false
16
+
17
+ Metrics/BlockLength:
18
+ Enabled: false
19
+
20
+ Metrics/ClassLength:
21
+ Enabled: false
22
+
23
+ Metrics/CyclomaticComplexity:
24
+ Enabled: false
25
+
26
+ Metrics/ParameterLists:
27
+ Enabled: false
28
+
29
+ Metrics/PerceivedComplexity:
30
+ Enabled: false
31
+
32
+ Style/Documentation:
33
+ Enabled: false
34
+
35
+ Style/HashConversion:
36
+ Enabled: false
37
+
38
+ Style/TrailingCommaInHashLiteral:
39
+ EnforcedStyleForMultiline: 'consistent_comma'
40
+
41
+ Style/TrailingCommaInArrayLiteral:
42
+ EnforcedStyleForMultiline: 'consistent_comma'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Blake Gearin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # folding_at_home_client
2
+
3
+ Ruby client for the [Folding@home API](https://api.foldingathome.org)
4
+
5
+ Need more historical data? Try out [`extreme_overclocking_client`](https://github.com/blakegearin/folding_at_home_client)
6
+
7
+ ## Getting Started
8
+
9
+ Install and add to Gemfile:
10
+
11
+ ```bash
12
+ bundle add folding_at_home_client
13
+ ```
14
+
15
+ Install without bundler:
16
+
17
+ ```bash
18
+ gem install folding_at_home_client
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ - [Users](#users)
24
+ - [User](#user)
25
+ - [Teams](#teams)
26
+ - [Team](#team)
27
+ - [Projects](#projects)
28
+ - [Project](#project)
29
+ - [Descriptions](#descriptions)
30
+ - [Description](#description)
31
+ - [Managers](#managers)
32
+ - [Manager](#manager)
33
+ - [Causes](#causes)
34
+ - [GPUs](#gpus)
35
+ - [GPU](#gpu)
36
+
37
+ ### Users
38
+
39
+ ```ruby
40
+ # Fetch count of users
41
+ FoldingAtHomeClient::Users.count
42
+
43
+ # Fetch top users of all-time
44
+ FoldingAtHomeClient::Users.top
45
+
46
+ # Fetch top users from a specific month
47
+ FoldingAtHomeClient::Users.top(month: 1, year: 2018)
48
+
49
+ # Fetch daily users (unique based on name and team_id, not user id)
50
+ # Caches the TXT file and limits fetching to every 3 hours
51
+ # Optional: filepath, limit, name, order, page, per_page, position, sort_by, team_id
52
+ FoldingAtHomeClient::Users.daily
53
+ FoldingAtHomeClient::Users.daily(sort_by: :name, order: :desc)
54
+ FoldingAtHomeClient::Users.daily(limit: 5)
55
+ FoldingAtHomeClient::Users.daily(page: 1, per_page: 5)
56
+ FoldingAtHomeClient::Users.daily(name: 'Anonymous')
57
+ FoldingAtHomeClient::Users.daily(team_id: 0)
58
+ FoldingAtHomeClient::Users.daily(name: 'Anonymous', team_id: 0)
59
+ FoldingAtHomeClient::Users.daily(position: 10)
60
+ FoldingAtHomeClient::Users.daily(filepath: 'my_daily_user_summary.txt')
61
+ ```
62
+
63
+ ### User
64
+
65
+ ```ruby
66
+ id = 2
67
+ name = "name"
68
+ passkey = "passkey"
69
+ team_id = 0
70
+
71
+ # Create a user
72
+ # Required: id or name
73
+ user = FoldingAtHomeClient::User.new(id: id)
74
+ user = FoldingAtHomeClient::User.new(name: name)
75
+ user = FoldingAtHomeClient::User.new(id: id, name: name)
76
+
77
+ # Fetch a user's stats, including teams
78
+ # Required: id or name
79
+ # Optional: passkey, team_id
80
+ user = user.find_by(id: id)
81
+ user = user.find_by(name: name)
82
+ user = user.find_by(id: id, name: name)
83
+ user = user.find_by(id: id, passkey: passkey)
84
+ user = user.find_by(id: id, team_id: team_id)
85
+ user = user.find_by(id: id, passkey: passkey, team_id: team_id)
86
+
87
+ # Fetch a user's list of teams
88
+ # Note: Suffixed with "_lookup" since teams is a class attribute
89
+ # Required: id or name
90
+ # Optional: passkey
91
+ teams = user.teams_lookup
92
+ teams = user.teams_lookup(passkey: passkey)
93
+
94
+ # Fetch a user's list of contributed projects
95
+ # Required: name
96
+ projects = user.projects
97
+
98
+ # Fetch a user's bonus stats
99
+ # Required: name
100
+ # Optional: passkey
101
+ bonuses = user.bonuses
102
+ bonuses = user.bonuses(passkey: passkey)
103
+ ```
104
+
105
+ ### Teams
106
+
107
+ ```ruby
108
+ # Fetch count of teams
109
+ FoldingAtHomeClient::Teams.count
110
+
111
+ # Fetch top teams from a specific month
112
+ FoldingAtHomeClient::Teams.top(month: 1, year: 2018)
113
+ ```
114
+
115
+ ### Team
116
+
117
+ ```ruby
118
+ id = 1
119
+
120
+ # Fetch a team
121
+ # Required: id or name
122
+ team = FoldingAtHomeClient::Team.find_by(id: id)
123
+
124
+ # Fetch a teams's members
125
+ # Required: id
126
+ members = FoldingAtHomeClient::Team.new(id: id).members
127
+ ```
128
+
129
+ ### Projects
130
+
131
+ ```ruby
132
+ # Fetch all projects
133
+ FoldingAtHomeClient::Projects.all
134
+ ```
135
+
136
+ ### Project
137
+
138
+ ```ruby
139
+ id = 2968
140
+
141
+ project = FoldingAtHomeClient::Project.new(id: id)
142
+
143
+ # Fetch a project
144
+ # Required: id
145
+ project = project.lookup
146
+
147
+ # Fetch a project's contributors
148
+ # Required: id
149
+ contributors = project.contributors
150
+
151
+ # Fetch a project's description
152
+ # Required: description_id
153
+ description = project.description
154
+ ```
155
+
156
+ ### Descriptions
157
+
158
+ ```ruby
159
+ # Fetch all descriptions
160
+ descriptions = FoldingAtHomeClient::Descriptions.all
161
+ ```
162
+
163
+ ### Description
164
+
165
+ ```ruby
166
+ id = 195
167
+
168
+ # Fetch a description
169
+ # Required: id
170
+ description = FoldingAtHomeClient::Description.new(id: id).lookup
171
+ ```
172
+
173
+ ### Managers
174
+
175
+ ```ruby
176
+ # Fetch all managers
177
+ managers = FoldingAtHomeClient::Managers.all
178
+ ```
179
+
180
+ ### Manager
181
+
182
+ ```ruby
183
+ id = 326
184
+
185
+ # Fetch a manager
186
+ # Required: id
187
+ manager = FoldingAtHomeClient::Manager.new(id: id).lookup
188
+ ```
189
+
190
+ ### Causes
191
+
192
+ ```ruby
193
+ # Fetch all causes
194
+ causes = FoldingAtHomeClient::Causes.all
195
+ ```
196
+
197
+ ### GPUs
198
+
199
+ ```ruby
200
+ # Fetch all GPUs
201
+ gpus = FoldingAtHomeClient::GPUs.all
202
+ ```
203
+
204
+ ### GPU
205
+
206
+ ```ruby
207
+ vendor = 4318
208
+ device = 5
209
+
210
+ # Fetch a GPUs
211
+ # Required: device, vendor
212
+ gpu = FoldingAtHomeClient::GPU.find_by(vendor: vendor, device: device)
213
+ ```
214
+
215
+ ### Notes
216
+
217
+ Currently only `GET` endpoints are supported. It's not exhaustive because some endpoints aren't particularly useful in terms of what they return compared to other endpoints.
218
+
219
+ ## Development
220
+
221
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
222
+
223
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
224
+
225
+ ## Contributing
226
+
227
+ Bug reports, feature requests, and pull requests are welcome.
228
+
229
+ ## Links
230
+
231
+ - [Folding@home](https://foldingathome.org)
232
+
233
+ - [Folding@home Download](https://foldingathome.org/start-folding)
234
+
235
+ - [Folding@home Stats](https://stats.foldingathome.org)
236
+
237
+ - [EXTREME Overclocking (EOC) Stats](https://folding.extremeoverclocking.com/aggregate_summary.php)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/folding_at_home_client/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'folding_at_home_client'
7
+ spec.version = FoldingAtHomeClient::VERSION
8
+
9
+ spec.authors = ['Blake Gearin']
10
+ spec.email = 'hello@blakeg.me'
11
+
12
+ spec.summary = 'Ruby client for Folding@Home'
13
+ spec.description = 'Ruby client for Folding@Home'
14
+ spec.homepage = 'https://github.com/blakegearin/folding_at_home_client'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.1.0'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/blakegearin/folding_at_home_client'
20
+ spec.metadata['github_repo'] = 'ssh://github.com/blakegearin/folding_at_home_client'
21
+
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (File.expand_path(f) == __FILE__) ||
26
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
27
+ end
28
+ end
29
+
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ spec.add_dependency 'faraday', '~> 2.9'
35
+ spec.metadata['rubygems_mfa_required'] = 'true'
36
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ module Causes
5
+ extend Request
6
+
7
+ def self.all
8
+ endpoint = '/project/cause'
9
+
10
+ request(endpoint:)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ class Description
5
+ include Request
6
+
7
+ attr_reader :id,
8
+ :manager,
9
+ :body,
10
+ :updated_at,
11
+ :error
12
+
13
+ def initialize(
14
+ id:,
15
+ description: nil,
16
+ manager: nil,
17
+ projects: nil,
18
+ modified: nil
19
+ )
20
+ @id = id
21
+
22
+ @body = description if description
23
+ @manager = manager if manager
24
+ @projects = projects if projects
25
+ @updated_at = modified if modified
26
+ end
27
+
28
+ def lookup
29
+ endpoint = "/project/description/#{id}"
30
+ description_hash = request(endpoint:).first
31
+
32
+ error = description_hash[:error]
33
+
34
+ if error
35
+ @error = error
36
+ return self
37
+ end
38
+
39
+ @body = description_hash[:description]
40
+ @manager = description_hash[:manager]
41
+ @updated_at = description_hash[:modified]
42
+
43
+ self
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ module Descriptions
5
+ extend Request
6
+
7
+ def self.all
8
+ endpoint = '/project/description'
9
+
10
+ request_and_instantiate_objects(endpoint:, object_class: Description)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ class GPU
5
+ extend Request
6
+
7
+ attr_reader :vendor,
8
+ :device,
9
+ :type,
10
+ :species,
11
+ :description,
12
+ :error
13
+
14
+ def initialize(
15
+ vendor: nil,
16
+ device: nil,
17
+ type: nil,
18
+ species: nil,
19
+ description: nil,
20
+ error: nil
21
+ )
22
+ @vendor = vendor
23
+ @device = device
24
+ @type = type if type
25
+ @species = species if species
26
+ @description = description if description
27
+
28
+ @error = error if error
29
+ end
30
+
31
+ def self.find_by(vendor:, device:)
32
+ endpoint = "/gpus/#{vendor}/#{device}"
33
+ gpu_hash = request(endpoint:).first
34
+
35
+ gpu_hash[:vendor] = vendor
36
+ gpu_hash[:device] = device
37
+
38
+ error = gpu_hash[:error]
39
+ gpu_hash.delete(:status) if error
40
+
41
+ new_gpu = allocate
42
+ new_gpu.send(:initialize, **gpu_hash)
43
+
44
+ new_gpu
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ module GPUs
5
+ extend Request
6
+
7
+ def self.all
8
+ endpoint = '/gpus'
9
+
10
+ request_and_instantiate_objects(endpoint:, object_class: GPU)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ class Manager
5
+ include Request
6
+
7
+ attr_reader :id,
8
+ :name,
9
+ :description,
10
+ :thumb,
11
+ :url,
12
+ :institution,
13
+ :error
14
+
15
+ def initialize(
16
+ id: nil,
17
+ name: nil,
18
+ description: nil,
19
+ thumb: nil,
20
+ url: nil,
21
+ institution: nil
22
+ )
23
+ @id = id if id
24
+
25
+ @name = name if name
26
+ @description = description if description
27
+ @thumb = thumb if thumb && !thumb.empty?
28
+ @url = url if url && !url.empty?
29
+ @institution = institution if institution && !institution.empty?
30
+ end
31
+
32
+ def lookup
33
+ endpoint = "/project/manager/#{id}"
34
+ manager_hash = request(endpoint:).first
35
+
36
+ error = manager_hash[:error]
37
+
38
+ if error
39
+ @error = error
40
+ return self
41
+ end
42
+
43
+ @name = manager_hash[:name]
44
+ @description = manager_hash[:description]
45
+
46
+ thumb = manager_hash[:thumb]
47
+ @thumb = thumb if thumb && !thumb.empty?
48
+
49
+ url = manager_hash[:url]
50
+ @url = url if url && !url.empty?
51
+
52
+ institution = manager_hash[:institution]
53
+ @institution = institution if institution && !institution.empty?
54
+
55
+ self
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ module Managers
5
+ extend Request
6
+
7
+ def self.all
8
+ endpoint = '/project/manager'
9
+
10
+ request_and_instantiate_objects(endpoint:, object_class: Manager)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ class Project
5
+ include Request
6
+
7
+ attr_reader :id,
8
+ :description_id,
9
+ :manager,
10
+ :cause,
11
+ :thumb,
12
+ :url,
13
+ :institution,
14
+ :updated_at,
15
+ :error
16
+
17
+ def initialize(
18
+ id:,
19
+ description: nil,
20
+ manager: nil,
21
+ cause: nil,
22
+ modified: nil
23
+ )
24
+ @id = id
25
+
26
+ @description_id = description if description
27
+ @manager = Manager.new(name: manager) if manager
28
+ @cause = cause if cause
29
+ @updated_at = modified if modified
30
+ end
31
+
32
+ def lookup
33
+ endpoint = "/project/#{@id}"
34
+ project_hash = request(endpoint:).first
35
+
36
+ error = project_hash[:error]
37
+
38
+ if error
39
+ @error = error
40
+ return self
41
+ end
42
+
43
+ @body = project_hash[:description]
44
+ @manager = Manager.new(
45
+ name: project_hash[:manager],
46
+ thumb: project_hash[:mthumb],
47
+ description: project_hash[:mdescription]
48
+ )
49
+ @cause = project_hash[:cause]
50
+ @updated_at = project_hash[:modified]
51
+
52
+ self
53
+ end
54
+
55
+ def contributors
56
+ endpoint = '/project/contributors'
57
+ params = { projects: @id }
58
+
59
+ request(endpoint:, params:).map do |name|
60
+ User.new(name:)
61
+ end
62
+ end
63
+
64
+ def description
65
+ Description.new(id: @description_id).lookup
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ module Projects
5
+ extend Request
6
+
7
+ def self.all
8
+ endpoint = '/project'
9
+
10
+ request_and_instantiate_objects(endpoint:, object_class: Project)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: false
2
+
3
+ class DoNotEncoder
4
+ def self.encode(params)
5
+ buffer = ''
6
+
7
+ params.each do |key, value|
8
+ buffer << "#{key}=#{value}&"
9
+ end
10
+
11
+ buffer.chop
12
+ end
13
+
14
+ def self.decode(params)
15
+ Faraday::FlatParamsEncoder.decode(params)
16
+ end
17
+ end
18
+
19
+ module FoldingAtHomeClient
20
+ module Request
21
+ API_URL = 'https://api.foldingathome.org'.freeze
22
+ HEADERS = {
23
+ 'Accept' => 'application/json',
24
+ }.freeze
25
+
26
+ def connection(base_url: API_URL)
27
+ Faraday.new(url: base_url) do |faraday|
28
+ faraday.request :url_encoded
29
+ faraday.adapter Faraday.default_adapter
30
+
31
+ faraday.options.params_encoder = DoNotEncoder
32
+ end
33
+ end
34
+
35
+ def format_response(response)
36
+ parsed_response = JSON.parse(response.body, symbolize_names: true)
37
+ parsed_response.is_a?(Array) ? parsed_response : [parsed_response]
38
+ end
39
+
40
+ def request_unencoded(endpoint_and_params:, base_url: API_URL, format_response: true)
41
+ response = connection(base_url:).get(endpoint_and_params)
42
+
43
+ return format_response(response) if format_response
44
+
45
+ response
46
+ end
47
+
48
+ def request(endpoint:, base_url: API_URL, format_response: true, params: {})
49
+ full_url = base_url + endpoint
50
+ response = Faraday.get(full_url, params, HEADERS)
51
+
52
+ return format_response(response) if format_response
53
+
54
+ response
55
+ end
56
+
57
+ def request_and_instantiate_objects(endpoint:, object_class:, params: {})
58
+ request(endpoint:, params:).map do |hash|
59
+ # require 'pry'
60
+ # binding.pry
61
+ # puts 'end of pry'
62
+ object_class.new(**hash)
63
+ end
64
+ end
65
+
66
+ # def request_and_instantiate_objects(endpoint:, object_class:, params: {})
67
+ # request(endpoint:, params:).map do |hash|
68
+ # object_class.new(*hash.values_at(*object_class.members))
69
+ # end
70
+ # end
71
+ end
72
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ class Team
5
+ include Request
6
+ extend Request
7
+
8
+ attr_accessor :id, :name
9
+ attr_reader :rank,
10
+ :score,
11
+ :wus,
12
+ :founder,
13
+ :url,
14
+ :logo,
15
+ :user_score,
16
+ :user_wus,
17
+ :error
18
+
19
+ # rubocop:disable Lint/UnusedMethodArgument
20
+ def initialize(
21
+ id: nil,
22
+ team: nil,
23
+ name: nil,
24
+ wus: nil,
25
+ rank: nil,
26
+ trank: nil,
27
+ credit: nil,
28
+ tscore: nil,
29
+ twus: nil,
30
+ founder: nil,
31
+ score: nil,
32
+ url: nil,
33
+ logo: nil,
34
+ last: nil,
35
+ active_7: nil,
36
+ active_50: nil,
37
+ error: nil
38
+ )
39
+ @id = id || team.to_i if id || team
40
+ @name = name if name
41
+
42
+ @rank = trank || rank if trank || rank
43
+
44
+ @score = tscore || credit if tscore || credit
45
+ @wus = twus || wus if twus || wus
46
+
47
+ @founder = founder if founder
48
+ @url = url if url
49
+ @logo = logo if logo
50
+
51
+ @user_score = score if !tscore.nil? && score
52
+ @user_wus = wus if !twus.nil? && wus
53
+
54
+ @error = error if error
55
+ end
56
+ # rubocop:enable Lint/UnusedMethodArgument
57
+
58
+ def self.find_by(id: nil, name: nil)
59
+ team = allocate
60
+
61
+ team.id ||= id if id
62
+ team.name ||= name if name
63
+
64
+ endpoint_and_params = '/team'
65
+
66
+ if team.id && !team.id.to_s.empty?
67
+ endpoint_and_params += "/#{team.id}"
68
+ elsif team.name && !team.name.empty?
69
+ endpoint_and_params += "/find?name=#{escape_param(team.name)}"
70
+ else
71
+ raise ArgumentError, 'Required: id or name of team'
72
+ end
73
+
74
+ team_hash = nil
75
+
76
+ begin
77
+ team_hash = request_unencoded(endpoint_and_params:).first
78
+ raise StandardError if team_hash[:error]
79
+ rescue StandardError
80
+ if team.name
81
+ query_endpoint_and_params = "/team?q=#{escape_param(team.name)}"
82
+ query_team_hash = request_unencoded(endpoint_and_params: query_endpoint_and_params).first
83
+
84
+ team.id = query_team_hash&.fetch(:id, nil)
85
+ endpoint = "/#{team.id}"
86
+
87
+ team_hash = request(endpoint:).first if team.id
88
+ end
89
+ end
90
+
91
+ error = team_hash[:error]
92
+ team_hash.delete(:status) if error
93
+
94
+ team.send(:initialize, **team_hash)
95
+
96
+ team
97
+ end
98
+
99
+ def members
100
+ endpoint = "/team/#{@id}/members"
101
+
102
+ members_array = request(endpoint:)
103
+ keys = members_array.shift.map(&:to_sym)
104
+
105
+ members_array.map do |member_array|
106
+ member_hash = Hash[keys.zip(member_array)]
107
+
108
+ User.new(**member_hash)
109
+ end
110
+ end
111
+
112
+ def self.escape_param(name)
113
+ name.gsub(' ', '%20').gsub('&', '%26').gsub(',', '%2C')
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ module Teams
5
+ extend Request
6
+
7
+ def self.count
8
+ endpoint = '/team/count'
9
+
10
+ request(endpoint:).first
11
+ end
12
+
13
+ def self.top(month: nil, year: nil)
14
+ endpoint = '/team/monthly'
15
+ params = {
16
+ month:,
17
+ year:,
18
+ }
19
+
20
+ request_and_instantiate_objects(
21
+ endpoint:,
22
+ params:,
23
+ object_class: Team
24
+ )
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module FoldingAtHomeClient
6
+ class User
7
+ include Request
8
+ extend Request
9
+
10
+ attr_accessor :id, :name
11
+ attr_reader :work_units,
12
+ :wus_daily,
13
+ :active7,
14
+ :active50,
15
+ :credit,
16
+ :last,
17
+ :rank,
18
+ :score,
19
+ :teams,
20
+ :error
21
+
22
+ # rubocop:disable Lint/UnusedMethodArgument, Naming/MethodParameterName
23
+ def initialize(
24
+ id: nil,
25
+ name: nil,
26
+ work_unit: nil,
27
+ work_units: nil,
28
+ wu: nil,
29
+ active7: nil,
30
+ active50: nil,
31
+ credit: nil,
32
+ last: nil,
33
+ rank: nil,
34
+ score: nil,
35
+ team: nil,
36
+ teams: nil,
37
+ users: nil,
38
+ error: nil
39
+ )
40
+ @id = id if id
41
+ @name = name if name
42
+
43
+ @work_units = work_units&.to_i || wu&.to_i unless work_units.nil? && wu.nil?
44
+
45
+ @active7 = active7 if active7
46
+ @active50 = active50 if active50
47
+ @credit = credit if credit
48
+ @last = last if last
49
+ @rank = rank if rank
50
+ @score = score.to_i if score
51
+
52
+ teams = [team] if teams.nil? && team
53
+
54
+ teams = teams&.map do |team_thing|
55
+ if team_thing.is_a?(Hash)
56
+ Team.new(**team_thing) unless team_thing[:score]&.zero?
57
+ elsif team_thing.is_a?(String)
58
+ Team.new(id: team_thing.to_i)
59
+ else
60
+ team_thing
61
+ end
62
+ end&.compact
63
+
64
+ @teams = teams if teams
65
+
66
+ @error = error if error
67
+ end
68
+ # rubocop:enable Lint/UnusedMethodArgument, Naming/MethodParameterName
69
+
70
+ def lookup(passkey: nil, team_id: nil)
71
+ endpoint = user_endpoint(id: @id, name: @name)
72
+
73
+ params = {}
74
+ params[:passkey] = passkey if passkey
75
+ params[:team] = team_id if team_id
76
+
77
+ user_hash = nil
78
+
79
+ begin
80
+ user_hash = request(endpoint:, params:).first
81
+ rescue JSON::ParserError
82
+ if @name
83
+ query_endpoint = '/search/user'
84
+ query_params = { query: @name }
85
+
86
+ query_user_hash = request(endpoint: query_endpoint, params: query_params).first
87
+ @id = query_user_hash&.fetch(:id, nil)
88
+ user_hash = request(endpoint: user_endpoint, params:).first if @id
89
+ end
90
+ end
91
+
92
+ error = user_hash[:error]
93
+
94
+ if error
95
+ @error = error
96
+ return self
97
+ end
98
+
99
+ @id = user_hash[:id]
100
+ @name = user_hash[:name]
101
+
102
+ @work_units_daily = @work_units if user_hash[:wus] && !@work_units.nil?
103
+ @work_units = user_hash[:wus]
104
+
105
+ @active7 = user_hash[:active7]
106
+ @active50 = user_hash[:active50]
107
+ @credit = user_hash[:credit] if user_hash[:credit]
108
+ @last = user_hash[:last]
109
+ @rank = user_hash[:rank]
110
+ @score = user_hash[:score]
111
+
112
+ teams = user_hash[:teams]&.map do |team_hash|
113
+ Team.new(**team_hash) unless team_hash[:score]&.zero?
114
+ end&.compact
115
+
116
+ @teams = teams if teams
117
+
118
+ self
119
+ end
120
+
121
+ def self.find_by(id: nil, name: nil, passkey: nil, team_id: nil)
122
+ user = allocate
123
+
124
+ user.id ||= id if id
125
+ user.name ||= name if name
126
+
127
+ endpoint = user_endpoint(id: user.id, name: user.name)
128
+
129
+ params = {}
130
+ params[:passkey] = passkey if passkey
131
+ params[:team] = team_id if team_id
132
+
133
+ user_hash = nil
134
+
135
+ begin
136
+ user_hash = request(endpoint:, params:).first
137
+ rescue JSON::ParserError
138
+ if user.name
139
+ query_endpoint = '/search/user'
140
+ query_params = { query: user.name }
141
+
142
+ query_user_hash = request(endpoint: query_endpoint, params: query_params).first
143
+ @id = query_user_hash&.fetch(:id, nil)
144
+ user_hash = request(endpoint: user_endpoint, params:).first if @id
145
+ end
146
+ end
147
+
148
+ error = user_hash[:error]
149
+
150
+ if error
151
+ user_hash.delete(:status)
152
+
153
+ user.send(:initialize, **user_hash)
154
+ return user
155
+ end
156
+
157
+ @id = user_hash[:id]
158
+ @name = user_hash[:name]
159
+ @work_units = user_hash[:wus]
160
+ @active7 = user_hash[:active7]
161
+ @active50 = user_hash[:active50]
162
+ @credit = user_hash[:credit]
163
+ @last = user_hash[:last]
164
+ @rank = user_hash[:rank]
165
+ @score = user_hash[:score]
166
+
167
+ teams = user_hash[:teams]&.map do |team_hash|
168
+ Team.new(**team_hash) unless team_hash[:score]&.zero?
169
+ end&.compact
170
+
171
+ user_hash[:teams] = teams if teams
172
+
173
+ user.send(:initialize, **user_hash)
174
+
175
+ user
176
+ end
177
+
178
+ def teams_lookup(passkey: nil)
179
+ endpoint = user_endpoint(name_required: true)
180
+ endpoint += '/teams'
181
+
182
+ params = {}
183
+ params[:passkey] = passkey if passkey
184
+
185
+ request_and_instantiate_objects(
186
+ endpoint:,
187
+ params:,
188
+ object_class: Team
189
+ )
190
+ end
191
+
192
+ def projects
193
+ endpoint = user_endpoint(name_required: true)
194
+ endpoint += '/projects'
195
+
196
+ request(endpoint:)
197
+ end
198
+
199
+ def bonuses(passkey: nil)
200
+ raise ArgumentError, 'Required: name of user' unless @name
201
+
202
+ endpoint = '/bonus'
203
+ params = {
204
+ user: @name,
205
+ }
206
+ params[:passkey] = passkey if passkey
207
+
208
+ request(endpoint:, params:)
209
+ end
210
+
211
+ class << self
212
+ private
213
+
214
+ def user_endpoint(id: nil, name: nil)
215
+ if id && !id.to_s.empty?
216
+ "/uid/#{id}"
217
+ elsif name && !name.empty?
218
+ "/user/#{CGI.escape(name)}"
219
+ elsif name_required
220
+ raise ArgumentError, 'Required: name of user'
221
+ else
222
+ raise ArgumentError, 'Required: id or name of user'
223
+ end
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ # def user_endpoint(name_required: false)
230
+ # if @id && !@id.to_s.empty? && !name_required
231
+ # "/uid/#{@id}"
232
+ # elsif @name && !@name.empty?
233
+ # "/user/#{CGI.escape(@name)}"
234
+ # elsif name_required
235
+ # raise ArgumentError, 'Required: name of user'
236
+ # else
237
+ # raise ArgumentError, 'Required: id or name of user'
238
+ # end
239
+ # end
240
+
241
+ def user_endpoint(id: @id, name: @name, name_required: false)
242
+ if id && !id.to_s.empty? && !name_required
243
+ "/uid/#{id}"
244
+ elsif name && !name.empty?
245
+ "/user/#{CGI.escape(name)}"
246
+ elsif name_required
247
+ raise ArgumentError, 'Required: name of user'
248
+ else
249
+ raise ArgumentError, 'Required: id or name of user'
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'open-uri'
5
+
6
+ module FoldingAtHomeClient
7
+ module Users
8
+ extend Request
9
+
10
+ DAILY_FILE = 'daily_user_summary.txt'
11
+
12
+ def self.count
13
+ endpoint = '/user-count'
14
+
15
+ request(endpoint:).first
16
+ end
17
+
18
+ def self.top(month: nil, year: nil)
19
+ endpoint = '/user'
20
+ params = {}
21
+
22
+ if month && year
23
+ endpoint += '/monthly'
24
+ params = {
25
+ month:,
26
+ year:,
27
+ }
28
+ end
29
+
30
+ request_and_instantiate_objects(
31
+ endpoint:,
32
+ params:,
33
+ object_class: User
34
+ )
35
+ end
36
+
37
+ def self.update_daily_file(filepath: DAILY_FILE, force: false)
38
+ last_timestamp = File.open(filepath, &:readline).chomp
39
+ time_difference_in_hours = (Time.now.utc - Time.parse(last_timestamp)) / 3600
40
+
41
+ return unless time_difference_in_hours >= 3.0 || force
42
+
43
+ IO.copy_stream(
44
+ URI.open('https://apps.foldingathome.org/daily_user_summary.txt'),
45
+ filepath
46
+ )
47
+ end
48
+
49
+ def self.daily(params = {})
50
+ filepath = params[:filepath] || DAILY_FILE
51
+ update_daily_file(filepath:)
52
+
53
+ header_lines = 2
54
+
55
+ position = params[:position]&.to_i || false
56
+
57
+ unless position
58
+ page = params[:page]&.to_i
59
+ per_page = params[:per_page]&.to_i
60
+ paginating = !page.nil? && !per_page.nil?
61
+
62
+ skip_line_index = paginating ? ((page - 1) * per_page) : false
63
+
64
+ unless paginating
65
+ limit = params[:limit]&.to_i || 10
66
+
67
+ name = params[:name] || false
68
+ team_id = params[:team_id]&.to_i || false
69
+ end
70
+ end
71
+
72
+ keys = []
73
+ users = []
74
+
75
+ File.foreach(filepath).each_with_index do |line, index|
76
+ next if index.zero?
77
+
78
+ line_array = line_to_array(line)
79
+
80
+ if index == 1
81
+ keys = line_array.map(&:to_sym)
82
+ next
83
+ elsif paginating && (index < (skip_line_index + header_lines))
84
+ next
85
+ end
86
+
87
+ user_hash = Hash[keys.zip(line_array)]
88
+
89
+ if position
90
+ next unless (position + header_lines) == index
91
+
92
+ return User.new(**user_hash)
93
+ elsif name || team_id
94
+ name_match = name ? name == user_hash[:name] : true
95
+ team_match = team_id ? team_id == user_hash[:team].to_i : true
96
+
97
+ users.push(User.new(**user_hash)) if name_match && team_match
98
+ else
99
+ users.push(User.new(**user_hash))
100
+
101
+ break if paginating && users.length >= per_page
102
+ end
103
+
104
+ break if users.length >= limit
105
+ end
106
+
107
+ sort_by = params[:sort_by] || false
108
+ users.sort_by! { |user| user.send(sort_by) } if sort_by
109
+
110
+ order = params[:order] || false
111
+ users.reverse! if order && order.to_s.downcase == 'desc'
112
+
113
+ users
114
+ end
115
+
116
+ def self.line_to_array(line)
117
+ line.chomp.split("\t")
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FoldingAtHomeClient
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'folding_at_home_client/version'
4
+
5
+ require 'faraday'
6
+ require 'json'
7
+
8
+ # Helper
9
+ require_relative 'folding_at_home_client/request'
10
+
11
+ # Classes
12
+ require_relative 'folding_at_home_client/causes'
13
+ require_relative 'folding_at_home_client/description'
14
+ require_relative 'folding_at_home_client/gpu'
15
+ require_relative 'folding_at_home_client/manager'
16
+ require_relative 'folding_at_home_client/project'
17
+ require_relative 'folding_at_home_client/team'
18
+ require_relative 'folding_at_home_client/user'
19
+
20
+ # Modules
21
+ require_relative 'folding_at_home_client/descriptions'
22
+ require_relative 'folding_at_home_client/gpus'
23
+ require_relative 'folding_at_home_client/managers'
24
+ require_relative 'folding_at_home_client/projects'
25
+ require_relative 'folding_at_home_client/teams'
26
+ require_relative 'folding_at_home_client/users'
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: folding_at_home_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Blake Gearin
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-08-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.9'
27
+ description: Ruby client for Folding@Home
28
+ email: hello@blakeg.me
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".rspec"
34
+ - ".rubocop.yml"
35
+ - LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - folding_at_home_client.gemspec
39
+ - lib/folding_at_home_client.rb
40
+ - lib/folding_at_home_client/causes.rb
41
+ - lib/folding_at_home_client/description.rb
42
+ - lib/folding_at_home_client/descriptions.rb
43
+ - lib/folding_at_home_client/gpu.rb
44
+ - lib/folding_at_home_client/gpus.rb
45
+ - lib/folding_at_home_client/manager.rb
46
+ - lib/folding_at_home_client/managers.rb
47
+ - lib/folding_at_home_client/project.rb
48
+ - lib/folding_at_home_client/projects.rb
49
+ - lib/folding_at_home_client/request.rb
50
+ - lib/folding_at_home_client/team.rb
51
+ - lib/folding_at_home_client/teams.rb
52
+ - lib/folding_at_home_client/user.rb
53
+ - lib/folding_at_home_client/users.rb
54
+ - lib/folding_at_home_client/version.rb
55
+ homepage: https://github.com/blakegearin/folding_at_home_client
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ homepage_uri: https://github.com/blakegearin/folding_at_home_client
60
+ source_code_uri: https://github.com/blakegearin/folding_at_home_client
61
+ github_repo: ssh://github.com/blakegearin/folding_at_home_client
62
+ rubygems_mfa_required: 'true'
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 3.1.0
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.3.26
79
+ signing_key:
80
+ specification_version: 4
81
+ summary: Ruby client for Folding@Home
82
+ test_files: []