nya_pr 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72276bc9090fe6feb1925b4d56948ca6521d3d6ddf218b8bde737010d29ed923
4
- data.tar.gz: b69b2671887a606a6cb09434f7eb242bb6b1c79075755d7492590a7adac8ce22
3
+ metadata.gz: fcee9fc7fd9c910f937d04b717a08558f1400f90029aca67ae4676a90db85031
4
+ data.tar.gz: 97e1630078e1fb9ebb0e01aafa61c2249dd1859cb48e611823e6105f7b422b5a
5
5
  SHA512:
6
- metadata.gz: 62623d0683a5c23143c219724f980194e2cf6e88d5b1b090c8d34d83494f2d4fb5340b1ee693d2c8464f7bfa8f13b925df714f52a6f0bdefe8b1f634bd8b2019
7
- data.tar.gz: 02fba8805b5d75b461fad1580e4fb70b71f709c91bc8d10e48d6fe3bdc11203ae48c051a57a67b68689947ca1f0613d7dc36b44873fa988aceb9d22e57c72754
6
+ metadata.gz: bde310b1529f25c20adf0e58031d028448f75b19a7d5e601fbb18432f768fb32ea22d1f7f869703977adf6ee22f40d0cc6a428fb0ca655937fae265d1069238b
7
+ data.tar.gz: 2932d0bba96e2ee071020fc21f0128d0d48b2d6b08f16966622067884d3a7e2ec91f769be0466b5b1e7c45e7f1f1f1b6539fb0012ba74c456f29f07a1f0170c5
data/README.md CHANGED
@@ -304,7 +304,9 @@ Logs are written to `stderr`, while actual pull request output goes to `stdout`.
304
304
  --skip-drafts Ignore draft pull requests
305
305
  --[no-]progress Show or hide progress bars
306
306
  --log-level LEVEL Logging level
307
+ -w, --workers Number of repository scan workers
307
308
  -v, --version Show NyaPr version
309
+ -c, --config Configuration file path
308
310
  ```
309
311
 
310
312
  Example with most options enabled:
@@ -321,6 +323,47 @@ ruby run.rb \
321
323
  --log-level debug
322
324
  ```
323
325
 
326
+ ## YAML configuration
327
+
328
+ NyaPr can load options from a YAML configuration file. By default, it looks for `.nya-pr.yml` in the current directory.
329
+
330
+ ```yaml
331
+ username: your-github-username
332
+
333
+ owners:
334
+ - organization-one
335
+ - organization-two
336
+
337
+ limit: 100
338
+ skip_archived: true
339
+ skip_drafts: true
340
+ progress: true
341
+ log_level: info
342
+ ```
343
+
344
+ You can also provide a custom configuration file:
345
+
346
+ ```bash
347
+ nya-pr --config path/to/config.yml
348
+ ```
349
+
350
+ Command-line options override values from the YAML file. Any options not defined in either place use NyaPr's built-in defaults.
351
+
352
+ For example:
353
+
354
+ ```yaml
355
+ limit: 200
356
+ progress: true
357
+ ```
358
+
359
+ ```bash
360
+ nya-pr --limit 50 --no-progress
361
+ ```
362
+
363
+ In this case, NyaPr uses `limit: 50` and disables progress output.
364
+
365
+ Unknown configuration options and invalid values are reported as errors.
366
+
324
367
  ## How repository discovery works
325
368
 
326
369
  For your own GitHub username, NyaPr collects repositories owned by your authenticated account.
@@ -5,6 +5,10 @@ module NyaPr
5
5
  class Command < Dry::CLI::Command
6
6
  desc 'Collect open GitHub pull requests'
7
7
 
8
+ option :config,
9
+ aliases: ['-c'],
10
+ desc: 'Configuration file path'
11
+
8
12
  option :username,
9
13
  aliases: ['-u', '--user'],
10
14
  desc: 'GitHub username'
@@ -12,7 +16,6 @@ module NyaPr
12
16
  option :owners,
13
17
  aliases: ['-o'],
14
18
  type: :array,
15
- default: [],
16
19
  desc: 'Repository owners'
17
20
 
18
21
  option :token,
@@ -26,47 +29,52 @@ module NyaPr
26
29
 
27
30
  option :limit,
28
31
  aliases: ['-l'],
29
- default: PullRequest::Config::DEFAULT_LIMIT,
30
32
  desc: 'Maximum number of pull requests'
31
33
 
32
34
  option :refresh,
33
35
  aliases: ['-r'],
34
36
  type: :boolean,
35
- default: false,
36
37
  desc: 'Refresh repositories from GitHub'
37
38
 
38
39
  option :skip_archived,
39
40
  type: :boolean,
40
- default: false,
41
41
  desc: 'Skip archived repositories'
42
42
 
43
43
  option :skip_drafts,
44
44
  type: :boolean,
45
- default: false,
46
45
  desc: 'Ignore draft pull requests'
47
46
 
48
47
  option :progress,
49
48
  type: :boolean,
50
- default: true,
51
49
  desc: 'Show progress bars'
52
50
 
53
51
  option :log_level,
54
- values: Config::LOG_LEVELS.keys,
55
- default: 'info',
52
+ values: Config::Global::LOG_LEVELS.keys,
56
53
  desc: 'Log level'
57
54
 
55
+ option :workers,
56
+ aliases: ['-w'],
57
+ desc: 'Number of repository scan workers'
58
+
58
59
  option :version,
59
60
  aliases: ['-v'],
60
61
  type: :flag,
61
62
  desc: 'Show version'
62
63
 
63
- def call(version: false, **options)
64
+ def call(version: false, config: nil, **options)
64
65
  if version
65
66
  puts NyaPr::VERSION
66
67
  return
67
68
  end
68
69
 
69
- Runner.new(Config.from_options(options)).run
70
+ file_options = Config::Loader.load(config)
71
+
72
+ Runner.new(
73
+ Config::Global.from_options(
74
+ options,
75
+ file_options: file_options
76
+ )
77
+ ).run
70
78
  end
71
79
  end
72
80
  end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NyaPr
4
+ module Config
5
+ class Global < Data.define(
6
+ :username,
7
+ :owners,
8
+ :token,
9
+ :repositories_csv,
10
+ :pull_requests_dir,
11
+ :limit,
12
+ :refresh,
13
+ :skip_archived,
14
+ :skip_drafts,
15
+ :progress,
16
+ :log_level,
17
+ :workers
18
+ )
19
+ LOG_LEVELS = {
20
+ 'debug' => Logger::DEBUG,
21
+ 'info' => Logger::INFO,
22
+ 'warn' => Logger::WARN,
23
+ 'error' => Logger::ERROR,
24
+ 'fatal' => Logger::FATAL
25
+ }.freeze
26
+
27
+ DEFAULTS = {
28
+ owners: [],
29
+ token: nil,
30
+ repositories_csv: nil,
31
+ pull_requests_dir: nil,
32
+ limit: PullRequest::Config::DEFAULT_LIMIT,
33
+ refresh: false,
34
+ skip_archived: false,
35
+ skip_drafts: false,
36
+ progress: true,
37
+ log_level: 'info',
38
+ workers: Repository::Config::DEFAULT_WORKERS
39
+ }.freeze
40
+
41
+ BOOLEAN_KEYS = %i[
42
+ refresh
43
+ skip_archived
44
+ skip_drafts
45
+ progress
46
+ ].freeze
47
+
48
+ class << self
49
+ def from_options(options, file_options: {})
50
+ validate_keys!(file_options)
51
+
52
+ attributes = DEFAULTS.
53
+ merge(file_options).
54
+ merge(options.compact)
55
+
56
+ build(attributes)
57
+ end
58
+
59
+ private
60
+
61
+ def build(attributes)
62
+ normalized = attributes.merge(
63
+ username: normalize_username(attributes[:username]),
64
+ owners: normalize_owners(attributes[:owners]),
65
+ limit: normalize_positive_integer(attributes[:limit], :limit),
66
+ workers: normalize_positive_integer(attributes[:workers], :workers),
67
+ log_level: normalize_log_level(attributes[:log_level]),
68
+ **normalize_booleans(attributes)
69
+ )
70
+
71
+ new(**normalized)
72
+ end
73
+
74
+ def normalize_username(value)
75
+ username = value.to_s.strip
76
+
77
+ validate_username!(username)
78
+
79
+ username
80
+ end
81
+
82
+ def normalize_booleans(attributes)
83
+ BOOLEAN_KEYS.to_h do |key|
84
+ [key, normalize_boolean(attributes[key], key)]
85
+ end
86
+ end
87
+
88
+ def validate_keys!(options)
89
+ unknown = options.keys - members
90
+
91
+ return if unknown.empty?
92
+
93
+ raise NyaPr::Error,
94
+ "Unknown config option(s): #{unknown.join(', ')}"
95
+ end
96
+
97
+ def normalize_owners(owners)
98
+ Array(owners).
99
+ flat_map { |owner| owner.to_s.split(',') }.
100
+ map(&:strip).
101
+ reject(&:empty?)
102
+ end
103
+
104
+ def normalize_positive_integer(value, name)
105
+ number = Integer(value)
106
+
107
+ return number if number.positive?
108
+
109
+ raise NyaPr::Error, "#{name} must be greater than 0"
110
+ rescue ArgumentError, TypeError
111
+ raise NyaPr::Error, "#{name} must be a positive integer"
112
+ end
113
+
114
+ def normalize_boolean(value, name)
115
+ return value if [true, false].include?(value)
116
+
117
+ raise NyaPr::Error, "#{name} must be true or false"
118
+ end
119
+
120
+ def normalize_log_level(value)
121
+ LOG_LEVELS.fetch(value.to_s) do
122
+ raise NyaPr::Error,
123
+ "Invalid log level: #{value}"
124
+ end
125
+ end
126
+
127
+ def validate_username!(username)
128
+ return unless username.empty?
129
+
130
+ raise NyaPr::Error, 'GitHub username is required'
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module NyaPr
6
+ module Config
7
+ class Loader
8
+ DEFAULT_PATH = '.nya-pr.yml'
9
+
10
+ def self.load(path = nil)
11
+ new(path).load
12
+ end
13
+
14
+ attr_reader :path
15
+
16
+ def initialize(path = nil)
17
+ @explicit_path = !path.nil?
18
+ @path = Pathname.new(path || DEFAULT_PATH)
19
+ end
20
+
21
+ def load
22
+ unless path.exist?
23
+ return {} unless explicit_path?
24
+
25
+ raise NyaPr::Error, "Config file not found: #{path}"
26
+ end
27
+
28
+ parse
29
+ rescue Psych::SyntaxError => e
30
+ raise NyaPr::Error,
31
+ "Invalid YAML in #{path}: #{e.problem}"
32
+ rescue Errno::EACCES, Errno::ENOENT => e
33
+ raise NyaPr::Error,
34
+ "Cannot read config file #{path}: #{e.message}"
35
+ end
36
+
37
+ private
38
+
39
+ def explicit_path?
40
+ @explicit_path
41
+ end
42
+
43
+ def parse
44
+ data = YAML.safe_load_file(
45
+ path.to_s,
46
+ aliases: false
47
+ )
48
+
49
+ return {} if data.nil?
50
+
51
+ unless data.is_a?(Hash)
52
+ raise NyaPr::Error,
53
+ "Config file must contain a mapping: #{path}"
54
+ end
55
+
56
+ symbolize_keys(data)
57
+ end
58
+
59
+ def symbolize_keys(data)
60
+ data.to_h do |key, value|
61
+ unless key.is_a?(String) || key.is_a?(Symbol)
62
+ raise NyaPr::Error,
63
+ "Invalid config key: #{key.inspect}"
64
+ end
65
+
66
+ [key.to_sym, value]
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NyaPr
4
+ module Github
5
+ class Client
6
+ attr_reader :token, :options
7
+
8
+ def initialize(token, **options)
9
+ @token = token
10
+ @options = options
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/gzip'
5
+
6
+ module NyaPr
7
+ module Github
8
+ module Connection
9
+ API_URL = 'https://api.github.com'
10
+ API_VERSION = '2026-03-10'
11
+
12
+ def connection(client)
13
+ Faraday.new(connection_options(client)) do |faraday|
14
+ faraday.request :gzip
15
+ faraday.adapter Faraday.default_adapter
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def connection_options(client)
22
+ {
23
+ url: API_URL,
24
+ headers: {
25
+ accept: 'application/vnd.github+json',
26
+ user_agent: "nya_pr gem/#{NyaPr::VERSION}",
27
+ authorization: "Bearer #{client.token}",
28
+ 'X-GitHub-Api-Version': client.options.fetch(:api_version, API_VERSION)
29
+ },
30
+ request: {
31
+ timeout: client.options.fetch(:timeout, 60),
32
+ open_timeout: client.options.fetch(:open_timeout, 10)
33
+ }
34
+ }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+
5
+ module NyaPr
6
+ module Github
7
+ module Request
8
+ include Connection
9
+
10
+ MAX_PAGES = 1_000
11
+ PER_PAGE = 100
12
+
13
+ def get(path, params = {})
14
+ _response, body = do_get(path, params)
15
+
16
+ body
17
+ end
18
+
19
+ def paginate(path, params = {})
20
+ params = params.merge(per_page: params.fetch(:per_page, PER_PAGE))
21
+
22
+ page = 1
23
+ result = []
24
+
25
+ loop do
26
+ raise NyaPr::Error, 'Pagination limit exceeded' if page > MAX_PAGES
27
+
28
+ NyaPr.logger.debug("Fetching page #{page} for #{path}")
29
+ response, body = do_get(path, params.merge(page: page))
30
+
31
+ raise NyaPr::Error, 'Expected paginated response to be an array' unless body.is_a?(Array)
32
+
33
+ result.concat(body)
34
+
35
+ break if body.empty?
36
+ break unless next_page?(response)
37
+
38
+ page += 1
39
+ end
40
+
41
+ result
42
+ end
43
+
44
+ private
45
+
46
+ def do_get(path, params = {})
47
+ prepared_path = prepare(path)
48
+
49
+ NyaPr.logger.debug("GET #{prepared_path} #{params.inspect}")
50
+
51
+ response = connection(client).get(prepared_path, params)
52
+ body = parse_body(response)
53
+
54
+ NyaPr.logger.debug(
55
+ "GET #{prepared_path} -> HTTP #{response.status}"
56
+ )
57
+
58
+ raise_on_error!(response, body, prepared_path)
59
+
60
+ [response, body]
61
+ end
62
+
63
+ def parse_body(response)
64
+ return nil if response.body.nil? || response.body.empty?
65
+
66
+ Oj.load(response.body)
67
+ rescue Oj::ParseError
68
+ raise NyaPr::Error, "Failed to parse response: #{response.body}"
69
+ end
70
+
71
+ def raise_on_error!(response, body, path)
72
+ return if response.success?
73
+
74
+ message =
75
+ if body.is_a?(Hash)
76
+ body['message'] || body
77
+ else
78
+ body
79
+ end
80
+
81
+ raise NyaPr::Error.new(
82
+ "GET #{path}: GitHub API error #{response.status}: #{message}",
83
+ status: response.status
84
+ )
85
+ end
86
+
87
+ def next_page?(response)
88
+ response.headers['link']&.include?('rel="next"')
89
+ end
90
+
91
+ def prepare(path)
92
+ path.
93
+ delete_prefix('/').
94
+ gsub(%r{//}, '/').
95
+ gsub(%r{/+\z}, '')
96
+ end
97
+ end
98
+ end
99
+ end
@@ -19,7 +19,14 @@ module NyaPr
19
19
  @output = output
20
20
  @enabled = enabled
21
21
  @current = 0
22
+
23
+ # Use a monotonic clock so elapsed time is not affected by system
24
+ # clock changes while the progress bar is running.
22
25
  @started_at = monotonic_time
26
+
27
+ # Progress may be updated by multiple worker threads. The mutex keeps
28
+ # the counter update and terminal rendering atomic relative to each
29
+ # other, preventing lost increments and interleaved output.
23
30
  @mutex = Mutex.new
24
31
 
25
32
  render if enabled?
@@ -29,6 +36,7 @@ module NyaPr
29
36
  return unless enabled?
30
37
 
31
38
  mutex.synchronize do
39
+ # Never allow the displayed counter to exceed the expected total.
32
40
  @current = [@current + step, total].min
33
41
  render
34
42
  end
@@ -38,6 +46,8 @@ module NyaPr
38
46
  return unless enabled?
39
47
 
40
48
  mutex.synchronize do
49
+ # Render the final known state, then move subsequent output
50
+ # to a new terminal line.
41
51
  render
42
52
  output.puts
43
53
  output.flush
@@ -49,11 +59,13 @@ module NyaPr
49
59
  attr_reader :current, :started_at, :mutex
50
60
 
51
61
  def enabled?
62
+ # A zero-sized task does not need a progress bar.
52
63
  @enabled && total.positive?
53
64
  end
54
65
 
55
66
  def render
56
67
  output.print(
68
+ # Return to the beginning of the line and clear the previous render.
57
69
  "\r\e[2K",
58
70
  title,
59
71
  ' [',
@@ -63,7 +75,6 @@ module NyaPr
63
75
  "#{percentage}% ",
64
76
  "ETA #{eta}"
65
77
  )
66
-
67
78
  output.flush
68
79
  end
69
80
 
@@ -99,8 +110,11 @@ module NyaPr
99
110
  return "#{seconds}s" if seconds < 60
100
111
 
101
112
  minutes, seconds = seconds.divmod(60)
113
+ return "#{minutes}m #{seconds.to_s.rjust(2, '0')}s" if minutes < 60
114
+
115
+ hours, minutes = minutes.divmod(60)
102
116
 
103
- "#{minutes}m #{seconds.to_s.rjust(2, '0')}s"
117
+ "#{hours}h #{minutes.to_s.rjust(2, '0')}m"
104
118
  end
105
119
 
106
120
  def monotonic_time
@@ -5,7 +5,8 @@ module NyaPr
5
5
  class Config < Data.define(
6
6
  :limit,
7
7
  :skip_drafts,
8
- :progress_enabled
8
+ :progress_enabled,
9
+ :pull_requests_dir
9
10
  )
10
11
  DEFAULT_LIMIT = 100
11
12
 
@@ -13,7 +14,8 @@ module NyaPr
13
14
  new(
14
15
  limit: config.limit,
15
16
  skip_drafts: config.skip_drafts,
16
- progress_enabled: config.progress
17
+ progress_enabled: config.progress,
18
+ pull_requests_dir: config.pull_requests_dir
17
19
  )
18
20
  end
19
21
  end
@@ -3,7 +3,7 @@
3
3
  module NyaPr
4
4
  module PullRequest
5
5
  class Finder
6
- include NyaPr::Request
6
+ include NyaPr::Github::Request
7
7
 
8
8
  MAX_PER_PAGE = 100
9
9
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module NyaPr
6
+ module PullRequest
7
+ class Paths
8
+ DEFAULT_DIR = 'data'
9
+ FILENAME = 'pull_requests.csv'
10
+
11
+ attr_reader :config
12
+
13
+ def initialize(config, timestamp: Time.now)
14
+ @config = config
15
+ @timestamp = timestamp
16
+ end
17
+
18
+ def csv
19
+ path = run_dir.join(FILENAME)
20
+
21
+ FileUtils.mkdir_p(path.dirname)
22
+
23
+ path.to_s
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :timestamp
29
+
30
+ def run_dir
31
+ @run_dir ||= Pathname.
32
+ new(config.pull_requests_dir || DEFAULT_DIR).
33
+ join("pull_requests_#{formatted_timestamp}")
34
+ end
35
+
36
+ def formatted_timestamp
37
+ timestamp.strftime('%Y%m%d_%H%M%S')
38
+ end
39
+ end
40
+ end
41
+ end
@@ -8,7 +8,8 @@ module NyaPr
8
8
  :refresh,
9
9
  :skip_archived,
10
10
  :progress_enabled,
11
- :workers
11
+ :workers,
12
+ :repositories_csv
12
13
  )
13
14
  DEFAULT_WORKERS = 10
14
15
 
@@ -19,7 +20,8 @@ module NyaPr
19
20
  refresh: config.refresh,
20
21
  skip_archived: config.skip_archived,
21
22
  progress_enabled: config.progress,
22
- workers: DEFAULT_WORKERS
23
+ workers: config.workers,
24
+ repositories_csv: config.repositories_csv
23
25
  )
24
26
  end
25
27
  end
@@ -3,7 +3,7 @@
3
3
  module NyaPr
4
4
  module Repository
5
5
  class Filter
6
- include NyaPr::Request
6
+ include NyaPr::Github::Request
7
7
 
8
8
  attr_reader :client, :config
9
9
 
@@ -66,6 +66,8 @@ module NyaPr
66
66
  def build_workers(queue, result, progress_bar)
67
67
  Array.new(workers) do
68
68
  Thread.new do
69
+ Thread.current.report_on_exception = false
70
+
69
71
  process_queue(queue, result, progress_bar)
70
72
  end
71
73
  end
@@ -3,7 +3,7 @@
3
3
  module NyaPr
4
4
  module Repository
5
5
  class Finder
6
- include NyaPr::Request
6
+ include NyaPr::Github::Request
7
7
 
8
8
  attr_reader :client, :config
9
9
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module NyaPr
6
+ module Repository
7
+ class Paths
8
+ DEFAULT_PATH = 'data/repositories.csv'
9
+
10
+ attr_reader :config
11
+
12
+ def initialize(config)
13
+ @config = config
14
+ end
15
+
16
+ def csv
17
+ path = Pathname.new(
18
+ config.repositories_csv || DEFAULT_PATH
19
+ )
20
+
21
+ FileUtils.mkdir_p(path.dirname)
22
+
23
+ path.to_s
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/nya_pr/runner.rb CHANGED
@@ -15,19 +15,13 @@ module NyaPr
15
15
  configure_logger
16
16
 
17
17
  pull_requests = find_pull_requests
18
+
18
19
  pull_request_printer.print(pull_requests)
19
20
  save_pull_requests(pull_requests)
20
21
  end
21
22
 
22
23
  private
23
24
 
24
- def paths
25
- @paths ||= Paths.new(
26
- repositories_path: config.repositories_csv,
27
- pull_requests_dir: config.pull_requests_dir
28
- )
29
- end
30
-
31
25
  def configure_logger
32
26
  NyaPr.logger.level = config.log_level
33
27
  end
@@ -36,23 +30,43 @@ module NyaPr
36
30
  repository_collector.collect
37
31
  end
38
32
 
39
- def repository_collector
40
- @repository_collector ||= Repository::Collector.new(
41
- client,
42
- repository_store,
33
+ def repository_config
34
+ @repository_config ||= Repository::Config.from_app(config)
35
+ end
36
+
37
+ def pull_request_config
38
+ @pull_request_config ||= PullRequest::Config.from_app(config)
39
+ end
40
+
41
+ def repository_paths
42
+ @repository_paths ||= Repository::Paths.new(
43
43
  repository_config
44
44
  )
45
45
  end
46
46
 
47
- def repository_config
48
- @repository_config ||= Repository::Config.from_app(config)
47
+ def pull_request_paths
48
+ @pull_request_paths ||= PullRequest::Paths.new(
49
+ pull_request_config
50
+ )
49
51
  end
50
52
 
51
- def save_pull_requests(pull_requests)
52
- pull_request_store.write(pull_requests)
53
+ def repository_store
54
+ @repository_store ||= Repository::CsvStore.new(
55
+ repository_paths.csv
56
+ )
57
+ end
53
58
 
54
- NyaPr.logger.info(
55
- "Saved #{pull_requests.size} pull requests to #{pull_request_store.path}"
59
+ def pull_request_store
60
+ @pull_request_store ||= PullRequest::CsvStore.new(
61
+ pull_request_paths.csv
62
+ )
63
+ end
64
+
65
+ def repository_collector
66
+ @repository_collector ||= Repository::Collector.new(
67
+ client,
68
+ repository_store,
69
+ repository_config
56
70
  )
57
71
  end
58
72
 
@@ -66,16 +80,12 @@ module NyaPr
66
80
  find
67
81
  end
68
82
 
69
- def pull_request_config
70
- @pull_request_config ||= PullRequest::Config.from_app(config)
71
- end
72
-
73
- def repository_store
74
- @repository_store ||= Repository::CsvStore.new(paths.repositories_csv)
75
- end
83
+ def save_pull_requests(pull_requests)
84
+ pull_request_store.write(pull_requests)
76
85
 
77
- def pull_request_store
78
- @pull_request_store ||= PullRequest::CsvStore.new(paths.pull_requests_csv)
86
+ NyaPr.logger.info(
87
+ "Saved #{pull_requests.size} pull requests to #{pull_request_store.path}"
88
+ )
79
89
  end
80
90
 
81
91
  def pull_request_printer
@@ -83,7 +93,7 @@ module NyaPr
83
93
  end
84
94
 
85
95
  def client
86
- @client ||= Client.new(token)
96
+ @client ||= Github::Client.new(token)
87
97
  end
88
98
 
89
99
  def token
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NyaPr
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nya_pr
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elijah S. Krukowski
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '2.14'
68
+ - !ruby/object:Gem::Dependency
69
+ name: faraday-gzip
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: oj
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -108,23 +122,25 @@ files:
108
122
  - exe/nya-pr
109
123
  - lib/nya_pr.rb
110
124
  - lib/nya_pr/cli/command.rb
111
- - lib/nya_pr/client.rb
112
- - lib/nya_pr/config.rb
113
- - lib/nya_pr/connection.rb
125
+ - lib/nya_pr/config/global.rb
126
+ - lib/nya_pr/config/loader.rb
114
127
  - lib/nya_pr/error.rb
115
- - lib/nya_pr/paths.rb
128
+ - lib/nya_pr/github/client.rb
129
+ - lib/nya_pr/github/connection.rb
130
+ - lib/nya_pr/github/request.rb
116
131
  - lib/nya_pr/progress.rb
117
132
  - lib/nya_pr/pull_request/config.rb
118
133
  - lib/nya_pr/pull_request/csv_store.rb
119
134
  - lib/nya_pr/pull_request/filter.rb
120
135
  - lib/nya_pr/pull_request/finder.rb
136
+ - lib/nya_pr/pull_request/paths.rb
121
137
  - lib/nya_pr/pull_request/printer.rb
122
138
  - lib/nya_pr/repository/collector.rb
123
139
  - lib/nya_pr/repository/config.rb
124
140
  - lib/nya_pr/repository/csv_store.rb
125
141
  - lib/nya_pr/repository/filter.rb
126
142
  - lib/nya_pr/repository/finder.rb
127
- - lib/nya_pr/request.rb
143
+ - lib/nya_pr/repository/paths.rb
128
144
  - lib/nya_pr/runner.rb
129
145
  - lib/nya_pr/version.rb
130
146
  homepage: https://github.com/bodrovis/nya_pr
data/lib/nya_pr/client.rb DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NyaPr
4
- class Client
5
- attr_reader :token, :options
6
-
7
- def initialize(token, **options)
8
- @token = token
9
- @options = options
10
- end
11
- end
12
- end
data/lib/nya_pr/config.rb DELETED
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NyaPr
4
- class Config < Data.define(
5
- :username,
6
- :owners,
7
- :token,
8
- :repositories_csv,
9
- :pull_requests_dir,
10
- :limit,
11
- :refresh,
12
- :skip_archived,
13
- :skip_drafts,
14
- :progress,
15
- :log_level
16
- )
17
- LOG_LEVELS = {
18
- 'debug' => Logger::DEBUG,
19
- 'info' => Logger::INFO,
20
- 'warn' => Logger::WARN,
21
- 'error' => Logger::ERROR,
22
- 'fatal' => Logger::FATAL
23
- }.freeze
24
-
25
- class << self
26
- def from_options(options)
27
- username = options[:username].to_s
28
- validate_username!(username)
29
-
30
- new(
31
- username: username,
32
- owners: normalize_owners(options[:owners]),
33
- token: options[:token],
34
- repositories_csv: options[:repositories_csv],
35
- pull_requests_dir: options[:pull_requests_dir],
36
- limit: normalize_limit(options[:limit]),
37
- refresh: options[:refresh],
38
- skip_archived: options[:skip_archived],
39
- skip_drafts: options[:skip_drafts],
40
- progress: options[:progress],
41
- log_level: LOG_LEVELS.fetch(options[:log_level])
42
- )
43
- end
44
-
45
- private
46
-
47
- def normalize_owners(owners)
48
- Array(owners).
49
- flat_map { |owner| owner.split(',') }.
50
- map(&:strip).
51
- reject(&:empty?)
52
- end
53
-
54
- def normalize_limit(value)
55
- limit = Integer(value)
56
-
57
- return limit if limit.positive?
58
-
59
- raise NyaPr::Error, 'limit must be greater than 0'
60
- rescue ArgumentError, TypeError
61
- raise NyaPr::Error, 'limit must be a positive integer'
62
- end
63
-
64
- def validate_username!(username)
65
- return unless username.empty?
66
-
67
- raise NyaPr::Error, 'GitHub username is required'
68
- end
69
- end
70
- end
71
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'faraday'
4
-
5
- module NyaPr
6
- module Connection
7
- API_URL = 'https://api.github.com'
8
- API_VERSION = '2026-03-10'
9
-
10
- def connection(client)
11
- Faraday.new(connection_options(client)) do |faraday|
12
- faraday.adapter Faraday.default_adapter
13
- end
14
- end
15
-
16
- private
17
-
18
- def connection_options(client)
19
- {
20
- url: API_URL,
21
- headers: {
22
- accept: 'application/vnd.github+json',
23
- user_agent: "nya_pr gem/#{NyaPr::VERSION}",
24
- authorization: "Bearer #{client.token}",
25
- 'X-GitHub-Api-Version': client.options.fetch(:api_version, API_VERSION)
26
- },
27
- request: {
28
- timeout: client.options.fetch(:timeout, 60),
29
- open_timeout: client.options.fetch(:open_timeout, 10)
30
- }
31
- }
32
- end
33
- end
34
- end
data/lib/nya_pr/paths.rb DELETED
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
-
5
- module NyaPr
6
- class Paths
7
- DEFAULT_DATA_DIR = 'data'
8
- REPOSITORIES_FILENAME = 'repositories.csv'
9
- PULL_REQUESTS_FILENAME = 'pull_requests.csv'
10
-
11
- attr_reader :repositories_path, :pull_requests_dir
12
-
13
- def initialize(
14
- repositories_path: nil,
15
- pull_requests_dir: nil,
16
- timestamp: Time.now
17
- )
18
- @repositories_path = repositories_path
19
- @pull_requests_dir = pull_requests_dir
20
- @timestamp = timestamp
21
- end
22
-
23
- def repositories_csv
24
- path = Pathname.new(
25
- repositories_path || File.join(DEFAULT_DATA_DIR, REPOSITORIES_FILENAME)
26
- )
27
-
28
- prepare_parent(path)
29
- path.to_s
30
- end
31
-
32
- def pull_requests_csv
33
- path = pull_requests_run_dir.join(PULL_REQUESTS_FILENAME)
34
-
35
- FileUtils.mkdir_p(path.dirname)
36
-
37
- path.to_s
38
- end
39
-
40
- private
41
-
42
- attr_reader :timestamp
43
-
44
- def pull_requests_run_dir
45
- @pull_requests_run_dir ||= begin
46
- base = Pathname.new(pull_requests_dir || DEFAULT_DATA_DIR)
47
-
48
- base.join("pull_requests_#{formatted_timestamp}")
49
- end
50
- end
51
-
52
- def formatted_timestamp
53
- timestamp.strftime('%Y%m%d_%H%M%S')
54
- end
55
-
56
- def prepare_parent(path)
57
- FileUtils.mkdir_p(path.dirname)
58
- end
59
- end
60
- end
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'oj'
4
-
5
- module NyaPr
6
- module Request
7
- include NyaPr::Connection
8
-
9
- MAX_PAGES = 1_000
10
- PER_PAGE = 100
11
-
12
- def get(path, params = {})
13
- _response, body = do_get(path, params)
14
-
15
- body
16
- end
17
-
18
- def paginate(path, params = {})
19
- params = params.merge(per_page: params.fetch(:per_page, PER_PAGE))
20
-
21
- page = 1
22
- result = []
23
-
24
- loop do
25
- raise NyaPr::Error, 'Pagination limit exceeded' if page > MAX_PAGES
26
-
27
- NyaPr.logger.debug("Fetching page #{page} for #{path}")
28
- response, body = do_get(path, params.merge(page: page))
29
-
30
- raise NyaPr::Error, 'Expected paginated response to be an array' unless body.is_a?(Array)
31
-
32
- result.concat(body)
33
-
34
- break if body.empty?
35
- break unless next_page?(response)
36
-
37
- page += 1
38
- end
39
-
40
- result
41
- end
42
-
43
- private
44
-
45
- def do_get(path, params = {})
46
- prepared_path = prepare(path)
47
-
48
- NyaPr.logger.debug("GET #{prepared_path} #{params.inspect}")
49
-
50
- response = connection(client).get(prepared_path, params)
51
- body = parse_body(response)
52
-
53
- NyaPr.logger.debug(
54
- "GET #{prepared_path} -> HTTP #{response.status}"
55
- )
56
-
57
- raise_on_error!(response, body, prepared_path)
58
-
59
- [response, body]
60
- end
61
-
62
- def parse_body(response)
63
- return nil if response.body.nil? || response.body.empty?
64
-
65
- Oj.load(response.body)
66
- rescue Oj::ParseError
67
- raise NyaPr::Error, "Failed to parse response: #{response.body}"
68
- end
69
-
70
- def raise_on_error!(response, body, path)
71
- return if response.success?
72
-
73
- message =
74
- if body.is_a?(Hash)
75
- body['message'] || body
76
- else
77
- body
78
- end
79
-
80
- raise NyaPr::Error.new(
81
- "GET #{path}: GitHub API error #{response.status}: #{message}",
82
- status: response.status
83
- )
84
- end
85
-
86
- def next_page?(response)
87
- response.headers['link']&.include?('rel="next"')
88
- end
89
-
90
- def prepare(path)
91
- path.
92
- delete_prefix('/').
93
- gsub(%r{//}, '/').
94
- gsub(%r{/+\z}, '')
95
- end
96
- end
97
- end