arxiv-dl 0.3.0 → 0.3.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: c919d2e16ae72397375aa3aa5f71bd08fe4f3d0687d83358bf652081ee994596
4
- data.tar.gz: 35a55cb7152642e19816a76b4a00fe52b06f74d5a52cb587d2e2b6897e23ac61
3
+ metadata.gz: 14d23483457b1dbb20c8aa9fa1384e52caf7144cab5ed357450f235dbcd29b80
4
+ data.tar.gz: 4d7255130c208f959bb0bb7249a33b3a076a362da7894dc370e480f2511109b7
5
5
  SHA512:
6
- metadata.gz: 67f1ea3177544e5c4e5472d2b1e81d894a42346313ae4666f53f9de89571b36a55d50ee247dae0b8af2dce869cec03ea72890009817ee613b7cab37b93bddbfe
7
- data.tar.gz: 5bcc8fc0ab51a75032ed0a3d8d2e20dad830f9b6bb92f75841be0fa23e05f60d66e76cb7e23c5a8b17fffba0634213d3b4299fde0c86314858f396135d2a9a84
6
+ metadata.gz: '0572297cdb2a74bb5cb46cdcd8449e2e41611a9649493e8ba5bc22b2fcb84d264e5982f114d2cff640cfd76e1761de592c8a472815ee6a8116b70f35807a11a0'
7
+ data.tar.gz: 8fd1844b84d8fc2044b8d92078ad2dae136713c93768269b64fe9a83b169a9da37466661dbaf61bb12f81d2ba827276f9c56e00291b978e8bdf7524de4a858ad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [0.3.1]
2
+
3
+ - Built on [dl-core](https://github.com/xoengineering/dl-core) 0.1: the HTTP client, errors, `Author`, `Slug`, sidecar writers, and CLI now come from it instead of copies. No change in behavior or output. `Arxiv::Downloader::Client`, `HTTPError`, `Author`, `Slug`, and `Metadata::YAML`/`JSON` still work; they are now `dl-core`'s classes. `Arxiv::Downloader::Error` now subclasses `DL::Core::Error`.
4
+ - Direct dependencies on `http`, `stringex`, and `ostruct` replaced by `dl-core`.
5
+
1
6
  ## [0.3.0]
2
7
 
3
8
  Less folder nesting for the common case: most papers only ever have one version.
@@ -1,11 +1,5 @@
1
1
  module Arxiv
2
2
  module Downloader
3
- Author = Data.define :name, :affiliations do
4
- def initialize name:, affiliations: []
5
- super
6
- end
7
-
8
- def to_s = name
9
- end
3
+ Author = DL::Core::Author
10
4
  end
11
5
  end
@@ -1,106 +1,24 @@
1
- require 'optparse'
2
-
3
1
  module Arxiv
4
2
  module Downloader
5
- class CLI
6
- DEFAULT_DOWNLOAD_PATH = File.join Dir.home, 'Downloads', 'ArXiv_Papers'
7
- USAGE = 'Usage: arxiv-dl [options] <ARXIV_ID_OR_URL> [<ARXIV_ID_OR_URL>...]'.freeze
8
-
9
- def initialize argv, stderr: $stderr, stdin: $stdin, stdout: $stdout
10
- @argv = argv
11
- @stderr = stderr
12
- @stdin = stdin
13
- @stdout = stdout
14
- end
15
-
16
- def run
17
- options = parse
18
- return options.fetch(:exit_status) if options.key? :exit_status
19
-
20
- return error_with USAGE if options[:targets].empty?
21
- return error_with conflict if options[:verbose] && options[:quiet]
22
-
23
- failures = download_each options
24
- failures.zero? ? 0 : 1
25
- end
26
-
27
- private
28
-
29
- def conflict
30
- '-v and -q are mutually exclusive'
31
- end
32
-
33
- def parse
34
- options = { targets: [], verbose: false, quiet: false }
35
- parser = build_parser options
36
-
37
- begin
38
- parser.parse! @argv
39
- options[:targets] = @argv + input_targets(options[:input])
40
- rescue OptionParser::ParseError, SystemCallError => e
41
- @stderr.puts e.message
42
- return { exit_status: 1 }
43
- end
44
-
45
- options[:path] ||= ENV['ARXIV_DOWNLOAD_PATH'] || DEFAULT_DOWNLOAD_PATH
46
- options[:rate_limit] ||= (ENV['ARXIV_RATE_LIMIT'] || Client::DEFAULT_RATE_LIMIT).to_i
47
- options
48
- end
3
+ class CLI < DL::Core::CLI
4
+ def program = 'arxiv-dl'
49
5
 
50
- def build_parser options
51
- OptionParser.new do |parser|
52
- parser.banner = USAGE
53
- parser.on('-i FILE', '--input FILE') { |value| options[:input] = value }
54
- parser.on('-p PATH', '--path PATH') { |value| options[:path] = value }
55
- parser.on('--rate-limit SECONDS', Integer) { |value| options[:rate_limit] = value }
56
- parser.on('-v', '--verbose') { options[:verbose] = true }
57
- parser.on('-q', '--quiet') { options[:quiet] = true }
58
- parser.on('--version') do
59
- @stdout.puts VERSION
60
- options[:exit_status] = 0
61
- end
62
- parser.on('-h', '--help') do
63
- @stdout.puts parser.help
64
- options[:exit_status] = 0
65
- end
66
- end
67
- end
6
+ def target_name = 'ARXIV_ID_OR_URL'
68
7
 
69
- # one target per line from FILE, or stdin for "-"; blank lines and # comments skipped
70
- def input_targets input
71
- return [] if input.nil?
8
+ # ARXIV_DOWNLOAD_PATH, ARXIV_RATE_LIMIT
9
+ def env_prefix = 'ARXIV'
72
10
 
73
- text = input == '-' ? @stdin.read : File.read(input)
74
- text.lines.map(&:strip).reject { it.empty? || it.start_with?('#') }
75
- end
11
+ def default_path = File.join(Dir.home, 'Downloads', 'ArXiv_Papers')
76
12
 
77
- def error_with message
78
- @stderr.puts message
79
- 1
80
- end
13
+ def version = VERSION
81
14
 
82
- def download_each options
83
- client = Client.new rate_limit: options[:rate_limit], log: (options[:verbose] ? @stdout : nil)
15
+ def user_agent = Client::USER_AGENT
84
16
 
85
- failures = 0
86
- options[:targets].each do |target|
87
- failures += 1 unless download_one(target, client:, options:)
88
- end
89
- failures
90
- end
17
+ def client_for(rate_limit:, log:) = Client.new(rate_limit:, log:)
91
18
 
92
- # true on success; reports the failure and returns false otherwise
93
- def download_one target, client:, options:
94
- identifier = Identifier.new target
95
- @stdout.puts "==> Downloading #{identifier.id}" if options[:verbose]
19
+ def identifier_for(target) = Identifier.new(target)
96
20
 
97
- path = Archive.new(identifier, root: options[:path], client: client).run
98
- @stdout.puts path unless options[:quiet]
99
- true
100
- rescue Error, HTTP::Error => e
101
- @stderr.puts "#{target}: #{e.message}"
102
- false
103
- end
21
+ def archive_for(identifier, root:, client:) = Archive.new(identifier, root:, client:)
104
22
  end
105
23
  end
106
24
  end
@@ -1,85 +1,12 @@
1
- require 'http'
2
-
3
1
  module Arxiv
4
2
  module Downloader
5
- class Client
6
- SOURCE_URL = 'https://github.com/xoengineering/arxiv-dl'.freeze
7
- DEFAULT_RATE_LIMIT = 3
8
- TIMEOUTS = { connect: 10, read: 60, write: 10 }.freeze # seconds, per operation
9
- MAX_RETRIES = 3
10
- RETRY_BACKOFF = 10 # seconds before the first retry. doubles on each retry.
11
- RETRYABLE_STATUSES = [429, 503].freeze
12
-
13
- attr_reader :rate_limit
14
-
15
- def initialize rate_limit: DEFAULT_RATE_LIMIT, log: nil
16
- @rate_limit = rate_limit
17
- @log = log
18
- end
19
-
20
- def user_agent
21
- "arxiv-dl/#{VERSION} (+#{SOURCE_URL})"
22
- end
23
-
24
- def get url
25
- retries = 0
26
-
27
- loop do
28
- response = request url
29
- return response if response.status.success?
30
- raise http_error(url, response) unless retryable? response, retries
31
-
32
- retries += 1
33
- wait_before_retry response, retries
34
- end
35
- end
36
-
37
- private
38
-
39
- def request url
40
- throttle
41
- response = HTTP.timeout(TIMEOUTS).headers('User-Agent' => user_agent).follow.get(url)
42
- @last_request_at = Time.now
43
- log_request url, response
44
- response
45
- end
46
-
47
- def http_error url, response
48
- HTTPError.new status: response.status.code, url: url, reason: response.status.reason
49
- end
50
-
51
- def retryable? response, retries
52
- RETRYABLE_STATUSES.include?(response.status.code) && retries < MAX_RETRIES
53
- end
54
-
55
- def wait_before_retry response, retries
56
- seconds = retry_after(response) || (RETRY_BACKOFF * (2**(retries - 1)))
57
- @log&.puts "==> #{response.status}. Retrying in #{seconds}s"
58
- sleep seconds
59
- end
60
-
61
- # Retry-After in delay-seconds form. The HTTP-date form falls back to backoff.
62
- def retry_after response
63
- value = response.headers['Retry-After']
64
- return if value.nil?
65
-
66
- Integer(value, exception: false)
67
- end
68
-
69
- def log_request url, response
70
- return if @log.nil?
71
-
72
- @log.puts "==> GET #{url} (#{response.body.to_s.bytesize} bytes)"
73
- end
74
-
75
- def throttle
76
- return if @rate_limit.zero?
77
- return if @last_request_at.nil?
78
-
79
- elapsed = Time.now - @last_request_at
80
- return if elapsed >= @rate_limit
3
+ # DL::Core::Client with arxiv-dl's User-Agent filled in
4
+ class Client < DL::Core::Client
5
+ SOURCE_URL = 'https://github.com/xoengineering/arxiv-dl'.freeze
6
+ USER_AGENT = "arxiv-dl/#{VERSION} (+#{SOURCE_URL})".freeze
81
7
 
82
- sleep(@rate_limit - elapsed)
8
+ def initialize log: nil, rate_limit: DEFAULT_RATE_LIMIT
9
+ super(user_agent: USER_AGENT, log:, rate_limit:)
83
10
  end
84
11
  end
85
12
  end
@@ -1,5 +1,5 @@
1
1
  module Arxiv
2
2
  module Downloader
3
- class Error < StandardError; end
3
+ class Error < DL::Core::Error; end
4
4
  end
5
5
  end
@@ -1,14 +1,5 @@
1
1
  module Arxiv
2
2
  module Downloader
3
- class HTTPError < Error
4
- attr_reader :status, :url
5
-
6
- def initialize status:, url:, reason: nil
7
- @status = status
8
- @url = url
9
-
10
- super("GET #{url} failed: #{[status, reason].compact.join ' '}")
11
- end
12
- end
3
+ HTTPError = DL::Core::HTTPError
13
4
  end
14
5
  end
@@ -1,33 +1,7 @@
1
- require 'fileutils'
2
- require 'json'
3
-
4
1
  module Arxiv
5
2
  module Downloader
6
3
  class Metadata
7
- class JSON
8
- FILENAME = 'metadata.json'.freeze
9
-
10
- def initialize metadata
11
- @metadata = metadata
12
- end
13
-
14
- def write to:
15
- FileUtils.mkdir_p to
16
- File.write File.join(to, FILENAME), "#{::JSON.pretty_generate(serialize(@metadata.to_h))}\n"
17
- end
18
-
19
- private
20
-
21
- def serialize object
22
- case object
23
- when Hash then object.to_h { |key, value| [key.to_s, serialize(value)] }
24
- when Array then object.map { |item| serialize item }
25
- when Data then serialize object.to_h
26
- when Date, Time then object.iso8601
27
- else object
28
- end
29
- end
30
- end
4
+ JSON = DL::Core::Sidecar::JSON
31
5
  end
32
6
  end
33
7
  end
@@ -1,44 +1,22 @@
1
- require 'fileutils'
2
- require 'yaml'
3
-
4
1
  module Arxiv
5
2
  module Downloader
6
3
  class Metadata
4
+ # metadata.md: dl-core writes the frontmatter; the body is arxiv-specific
7
5
  class Markdown
8
- FILENAME = 'metadata.md'.freeze
9
-
10
6
  def initialize metadata
11
7
  @metadata = metadata
12
8
  end
13
9
 
14
10
  def write to:
15
- FileUtils.mkdir_p to
16
- File.write File.join(to, FILENAME), "#{frontmatter}\n#{body}"
11
+ DL::Core::Sidecar::Markdown.new(@metadata, body:, extras: { bibtex_key: }).write to:
17
12
  end
18
13
 
19
14
  private
20
15
 
21
- def frontmatter
22
- "---\n#{::YAML.dump(stringify(frontmatter_hash)).delete_prefix("---\n")}---"
23
- end
24
-
25
- def frontmatter_hash
26
- @metadata.to_h.merge bibtex_key: bibtex_key
27
- end
28
-
29
16
  def bibtex_key
30
17
  Downloader::Bibtex.new(@metadata).key
31
18
  end
32
19
 
33
- def stringify object
34
- case object
35
- when Hash then object.to_h { |key, value| [key.to_s, stringify(value)] }
36
- when Array then object.map { |item| stringify item }
37
- when Data then stringify object.to_h
38
- else object
39
- end
40
- end
41
-
42
20
  def body
43
21
  <<~MARKDOWN
44
22
 
@@ -1,32 +1,7 @@
1
- require 'fileutils'
2
- require 'yaml'
3
-
4
1
  module Arxiv
5
2
  module Downloader
6
3
  class Metadata
7
- class YAML
8
- FILENAME = 'metadata.yaml'.freeze
9
-
10
- def initialize metadata
11
- @metadata = metadata
12
- end
13
-
14
- def write to:
15
- FileUtils.mkdir_p to
16
- File.write File.join(to, FILENAME), ::YAML.dump(stringify(@metadata.to_h))
17
- end
18
-
19
- private
20
-
21
- def stringify object
22
- case object
23
- when Hash then object.to_h { |key, value| [key.to_s, stringify(value)] }
24
- when Array then object.map { |item| stringify item }
25
- when Data then stringify object.to_h
26
- else object
27
- end
28
- end
29
- end
4
+ YAML = DL::Core::Sidecar::YAML
30
5
  end
31
6
  end
32
7
  end
@@ -1,44 +1,5 @@
1
- require 'stringex'
2
-
3
1
  module Arxiv
4
2
  module Downloader
5
- class Slug
6
- MAX_LENGTH = 80
7
-
8
- TEX_INLINE_MATH = /\$[^$]*\$/ # $...$
9
- TEX_DISPLAY_MATH = /\\\(.*?\\\)|\\\[.*?\\\]/m # \(...\) or \[...\]
10
- TEX_COMMAND = /\\[a-zA-Z]+\*?/ # \emph, \alpha, etc.
11
-
12
- def initialize title
13
- @title = title
14
- end
15
-
16
- def to_s
17
- truncate strip_tex(@title).to_url
18
- end
19
-
20
- private
21
-
22
- def strip_tex string
23
- string
24
- .gsub(TEX_INLINE_MATH, ' ')
25
- .gsub(TEX_DISPLAY_MATH, ' ')
26
- .gsub(TEX_COMMAND, ' ')
27
- end
28
-
29
- def truncate slug
30
- return slug if slug.length <= MAX_LENGTH
31
-
32
- words = slug.split '-'
33
- result = +''
34
- words.each do |word|
35
- break if result.length + 1 + word.length > MAX_LENGTH
36
-
37
- result << '-' unless result.empty?
38
- result << word
39
- end
40
- result
41
- end
42
- end
3
+ Slug = DL::Core::Slug
43
4
  end
44
5
  end
@@ -1,5 +1,5 @@
1
1
  module Arxiv
2
2
  module Downloader
3
- VERSION = '0.3.0'.freeze
3
+ VERSION = '0.3.1'.freeze
4
4
  end
5
5
  end
@@ -1,3 +1,7 @@
1
+ require 'dl/core'
2
+
3
+ require_relative 'downloader/version' # before client: Client::USER_AGENT uses VERSION
4
+
1
5
  require_relative 'downloader/abstract_page'
2
6
  require_relative 'downloader/archive'
3
7
  require_relative 'downloader/assets_cache'
@@ -5,11 +9,11 @@ require_relative 'downloader/author'
5
9
  require_relative 'downloader/bibtex'
6
10
  require_relative 'downloader/categories'
7
11
  require_relative 'downloader/cli'
8
- require_relative 'downloader/client'
12
+ require_relative 'downloader/client' # after version
9
13
  require_relative 'downloader/error' # before errors below that subclass Error
10
14
  require_relative 'downloader/feed_parser'
11
15
  require_relative 'downloader/html_archive'
12
- require_relative 'downloader/http_error' # after error
16
+ require_relative 'downloader/http_error'
13
17
  require_relative 'downloader/identifier' # after error
14
18
  require_relative 'downloader/metadata' # before metadata/*: they reopen class Metadata
15
19
  require_relative 'downloader/metadata/bibtex' # after metadata
@@ -22,7 +26,6 @@ require_relative 'downloader/path'
22
26
  require_relative 'downloader/pdf'
23
27
  require_relative 'downloader/slug'
24
28
  require_relative 'downloader/source_archive'
25
- require_relative 'downloader/version'
26
29
 
27
30
  module Arxiv
28
31
  module Downloader
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arxiv-dl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shane Becker
@@ -10,33 +10,33 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: feedjira
13
+ name: dl-core
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '4.0'
18
+ version: '0.1'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '4.0'
25
+ version: '0.1'
26
26
  - !ruby/object:Gem::Dependency
27
- name: http
27
+ name: feedjira
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.0'
32
+ version: '4.0'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '6.0'
39
+ version: '4.0'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: nokogiri
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -51,34 +51,6 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.19'
54
- - !ruby/object:Gem::Dependency
55
- name: ostruct
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '0.6'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '0.6'
68
- - !ruby/object:Gem::Dependency
69
- name: stringex
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '2.8'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '2.8'
82
54
  description: Command line tool and Ruby library for archiving arxiv.org papers as
83
55
  PDFs with sidecar metadata.
84
56
  email: