readwise 1.0.0 → 1.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 +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +2 -0
- data/CHANGELOG.md +6 -0
- data/README.md +27 -1
- data/exe/readwise +7 -0
- data/lib/readwise/cli/base_command.rb +105 -0
- data/lib/readwise/cli/command_registry.rb +24 -0
- data/lib/readwise/cli/document/create_command.rb +152 -0
- data/lib/readwise/cli.rb +64 -0
- data/lib/readwise/client.rb +1 -1
- data/lib/readwise/constants.rb +6 -0
- data/lib/readwise/version.rb +1 -1
- data/readwise.gemspec +2 -2
- metadata +13 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e37bd88ff6059f63924f1c72ccb905f1d719fbff923411a00635995fcfe70a3
|
4
|
+
data.tar.gz: c3454ae039735d62d06c35889534c41ff76c94843ddeb9849d96b2287fa702d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45f34de451ad75c000b40ba5a78b7a365e701c8af9a27783128e30a951f8ec6674c8f90b12fb51ad77261c3a9f7378486feae6d5494d50e4b3ef9f0f4e0e8837
|
7
|
+
data.tar.gz: a5b2fe3e2a4d6c8ea744065f9dd76414db85858b62ae83be3aa65309281a25aa07fbd4cd72bf86f2581dba12796f890da07100d50a8d6892ee0620d0deae60a4
|
data/.github/workflows/ruby.yml
CHANGED
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
## [Unreleased]
|
4
4
|
|
5
|
+
## [1.1.0] - 2025-07-04
|
6
|
+
- Add CLI command for creating documents from HTML content or URLs ([#16](https://github.com/joshbeckman/readwise-ruby/pull/16) from [@andyw8](https://github.com/andyw8))
|
7
|
+
|
8
|
+
## [1.0.1] - 2025-06-29
|
9
|
+
- Handle absent tags in transforming highlights
|
10
|
+
|
5
11
|
## [1.0.0] - 2025-06-29
|
6
12
|
- Add Readwise API v3 support for Reader documents ([#13](https://github.com/joshbeckman/readwise-ruby/pull/13))
|
7
13
|
- Add `daily_review` client method and Review type ([#14](https://github.com/joshbeckman/readwise-ruby/pull/14))
|
data/README.md
CHANGED
@@ -101,7 +101,7 @@ puts document.child? # is this a highlight/note of another doc
|
|
101
101
|
|
102
102
|
# Check location
|
103
103
|
puts document.in_new?
|
104
|
-
puts document.in_later?
|
104
|
+
puts document.in_later?
|
105
105
|
puts document.in_archive?
|
106
106
|
|
107
107
|
# Check category
|
@@ -142,6 +142,32 @@ document = client.create_document(document: document_create)
|
|
142
142
|
documents = client.create_documents(documents: [document_create1, document_create2])
|
143
143
|
```
|
144
144
|
|
145
|
+
## Command Line Interface
|
146
|
+
|
147
|
+
This gem includes a `readwise` command-line tool for quickly sending HTML content to Readwise Reader.
|
148
|
+
|
149
|
+
First, set your API token:
|
150
|
+
```bash
|
151
|
+
export READWISE_API_KEY=your_token_here
|
152
|
+
```
|
153
|
+
|
154
|
+
Then use the CLI to send HTML files:
|
155
|
+
```bash
|
156
|
+
# Basic usage
|
157
|
+
readwise document create --html-file content.html
|
158
|
+
readwise document create --url https://datatracker.ietf.org/doc/html/rfc2324
|
159
|
+
|
160
|
+
# Short form flag
|
161
|
+
readwise document create -f content.html
|
162
|
+
|
163
|
+
# With options
|
164
|
+
readwise document create --html-file content.html --title="My Article" --location=later
|
165
|
+
|
166
|
+
# See all available options
|
167
|
+
readwise --help
|
168
|
+
readwise document create --help
|
169
|
+
```
|
170
|
+
|
145
171
|
## Development
|
146
172
|
|
147
173
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/readwise
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Readwise
|
4
|
+
class CLI
|
5
|
+
class BaseCommand
|
6
|
+
def initialize
|
7
|
+
@options = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def execute(args)
|
11
|
+
parse_options(args)
|
12
|
+
validate_arguments(args)
|
13
|
+
run(args)
|
14
|
+
rescue OptionParser::InvalidOption => e
|
15
|
+
puts "Error: #{e.message}"
|
16
|
+
show_help
|
17
|
+
exit 1
|
18
|
+
rescue ArgumentError => e
|
19
|
+
puts "Error: #{e.message}"
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :options
|
26
|
+
|
27
|
+
def parse_options(args)
|
28
|
+
parser = create_option_parser
|
29
|
+
parser.parse!(args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_option_parser
|
33
|
+
OptionParser.new do |opts|
|
34
|
+
opts.banner = banner
|
35
|
+
opts.separator ""
|
36
|
+
opts.separator description if description
|
37
|
+
opts.separator ""
|
38
|
+
opts.separator "Options:"
|
39
|
+
|
40
|
+
add_options(opts)
|
41
|
+
|
42
|
+
opts.on("-h", "--help", "Show this help message") do
|
43
|
+
show_help
|
44
|
+
exit
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def show_help
|
50
|
+
puts create_option_parser.help
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_api_client
|
54
|
+
token = ENV['READWISE_API_KEY']
|
55
|
+
unless token
|
56
|
+
puts "Error: READWISE_API_KEY environment variable is not set"
|
57
|
+
exit 1
|
58
|
+
end
|
59
|
+
|
60
|
+
Readwise::Client.new(token: token)
|
61
|
+
end
|
62
|
+
|
63
|
+
def read_file(file_path)
|
64
|
+
unless File.exist?(file_path)
|
65
|
+
puts "Error: File '#{file_path}' not found"
|
66
|
+
exit 1
|
67
|
+
end
|
68
|
+
|
69
|
+
File.read(file_path)
|
70
|
+
end
|
71
|
+
|
72
|
+
def handle_api_error(&block)
|
73
|
+
yield
|
74
|
+
rescue Readwise::Client::Error => e
|
75
|
+
puts "API Error: #{e.message}"
|
76
|
+
exit 1
|
77
|
+
rescue => e
|
78
|
+
puts "Unexpected error: #{e.message}"
|
79
|
+
exit 1
|
80
|
+
end
|
81
|
+
|
82
|
+
# Override these methods in subclasses
|
83
|
+
|
84
|
+
def banner
|
85
|
+
"Usage: readwise"
|
86
|
+
end
|
87
|
+
|
88
|
+
def description
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_options(opts)
|
93
|
+
# Override in subclasses to add specific options
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate_arguments(args)
|
97
|
+
# Override in subclasses to validate arguments
|
98
|
+
end
|
99
|
+
|
100
|
+
def run(args)
|
101
|
+
raise NotImplementedError, "Subclasses must implement #run"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Readwise
|
2
|
+
class CLI
|
3
|
+
class CommandRegistry
|
4
|
+
@commands = {}
|
5
|
+
|
6
|
+
def self.register(resource, action, command_class)
|
7
|
+
key = "#{resource}:#{action}"
|
8
|
+
@commands[key] = command_class
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.find(resource, action)
|
12
|
+
key = "#{resource}:#{action}"
|
13
|
+
@commands[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.all_commands
|
17
|
+
@commands.keys.map { |key| key.split(':') }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Register available commands
|
24
|
+
require_relative 'document/create_command'
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require_relative '../base_command'
|
2
|
+
require_relative '../../constants'
|
3
|
+
require 'uri'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
module Readwise
|
7
|
+
class CLI
|
8
|
+
module Document
|
9
|
+
class CreateCommand < BaseCommand
|
10
|
+
def banner
|
11
|
+
"Usage: readwise document create [options]"
|
12
|
+
end
|
13
|
+
|
14
|
+
def description
|
15
|
+
"Sends HTML content to Readwise Reader API"
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_options(opts)
|
19
|
+
opts.on("-f", "--html-file=FILE", "HTML file path") do |file|
|
20
|
+
options[:file] = file
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("--title=TITLE", "Document title") do |title|
|
24
|
+
options[:title] = title
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("--author=AUTHOR", "Document author") do |author|
|
28
|
+
options[:author] = author
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-u", "--url=URL", "Source URL (defaults to https://example.com/<filename>)") do |url|
|
32
|
+
unless valid_url?(url)
|
33
|
+
puts "Error: Invalid URL format. Please provide a valid URL."
|
34
|
+
exit 1
|
35
|
+
end
|
36
|
+
options[:url] = url
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("--summary=SUMMARY", "Document summary") do |summary|
|
40
|
+
options[:summary] = summary
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("--notes=NOTES", "Personal notes") do |notes|
|
44
|
+
options[:notes] = notes
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("--location=LOCATION", "Document location: #{Readwise::Constants::DOCUMENT_LOCATIONS.join(', ')} (default: new)") do |location|
|
48
|
+
unless Readwise::Constants::DOCUMENT_LOCATIONS.include?(location)
|
49
|
+
puts "Error: Invalid location. Must be one of: #{Readwise::Constants::DOCUMENT_LOCATIONS.join(', ')}"
|
50
|
+
exit 1
|
51
|
+
end
|
52
|
+
options[:location] = location
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on("--category=CATEGORY", "Document category: #{Readwise::Constants::DOCUMENT_CATEGORIES.join(', ')}") do |category|
|
56
|
+
unless Readwise::Constants::DOCUMENT_CATEGORIES.include?(category)
|
57
|
+
puts "Error: Invalid category. Must be one of: #{Readwise::Constants::DOCUMENT_CATEGORIES.join(', ')}"
|
58
|
+
exit 1
|
59
|
+
end
|
60
|
+
options[:category] = category
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on("--tags=TAGS", "Comma-separated list of tags") do |tags|
|
64
|
+
options[:tags] = tags.split(',').map(&:strip)
|
65
|
+
end
|
66
|
+
|
67
|
+
opts.on("--image-url=URL", "Image URL") do |image_url|
|
68
|
+
options[:image_url] = image_url
|
69
|
+
end
|
70
|
+
|
71
|
+
opts.on("--published-date=DATE", "Published date (ISO 8601 format)") do |date|
|
72
|
+
unless valid_iso8601_date?(date)
|
73
|
+
puts "Error: Invalid date format. Please provide a valid ISO 8601 date (e.g., 2023-12-25T10:30:00Z)."
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
options[:published_date] = date
|
77
|
+
end
|
78
|
+
|
79
|
+
opts.on("--[no-]clean-html", "Clean HTML (default: true)") do |clean|
|
80
|
+
options[:should_clean_html] = clean
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on("--saved-using=SOURCE", "Saved using source (default: cli)") do |source|
|
84
|
+
options[:saved_using] = source
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def validate_arguments(args)
|
89
|
+
unless options[:file] || options[:url]
|
90
|
+
puts "Error: File path or URL is required"
|
91
|
+
show_help
|
92
|
+
exit 1
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def run(args)
|
97
|
+
html_file = options[:file]
|
98
|
+
html_content = read_file(html_file) if html_file
|
99
|
+
|
100
|
+
document_params = build_document_params(html_content, html_file)
|
101
|
+
|
102
|
+
handle_api_error do
|
103
|
+
client = get_api_client
|
104
|
+
document_create = Readwise::DocumentCreate.new(**document_params)
|
105
|
+
document = client.create_document(document: document_create)
|
106
|
+
|
107
|
+
puts "Document created successfully!"
|
108
|
+
puts "ID: #{document.id}"
|
109
|
+
puts "Title: #{document.title}"
|
110
|
+
puts "Location: #{document.location}"
|
111
|
+
puts "URL: #{document.url}" if document.url
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def valid_url?(url)
|
118
|
+
uri = URI.parse(url)
|
119
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
120
|
+
rescue URI::InvalidURIError
|
121
|
+
false
|
122
|
+
end
|
123
|
+
|
124
|
+
def valid_iso8601_date?(date)
|
125
|
+
DateTime.iso8601(date)
|
126
|
+
true
|
127
|
+
rescue Date::Error
|
128
|
+
false
|
129
|
+
end
|
130
|
+
|
131
|
+
def build_document_params(html_content, html_file)
|
132
|
+
document_params = {
|
133
|
+
html: html_content,
|
134
|
+
location: options[:location] || 'new',
|
135
|
+
should_clean_html: options.key?(:should_clean_html) ? options[:should_clean_html] : true,
|
136
|
+
saved_using: options[:saved_using] || 'cli',
|
137
|
+
url: options[:url] || "https://example.com/#{File.basename(html_file)}"
|
138
|
+
}
|
139
|
+
|
140
|
+
[:title, :author, :summary, :notes, :category, :tags, :image_url, :published_date].each do |key|
|
141
|
+
document_params[key] = options[key] if options[key]
|
142
|
+
end
|
143
|
+
|
144
|
+
document_params
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Register this command
|
152
|
+
Readwise::CLI::CommandRegistry.register('document', 'create', Readwise::CLI::Document::CreateCommand)
|
data/lib/readwise/cli.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'cli/base_command'
|
2
|
+
require_relative 'cli/command_registry'
|
3
|
+
|
4
|
+
module Readwise
|
5
|
+
class CLI
|
6
|
+
def self.start(args = ARGV)
|
7
|
+
new.start(args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def start(args)
|
11
|
+
if args.empty?
|
12
|
+
show_help
|
13
|
+
exit 1
|
14
|
+
end
|
15
|
+
|
16
|
+
if args.first == '--help' || args.first == '-h'
|
17
|
+
show_help
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
resource = args.shift&.downcase
|
22
|
+
action = args.shift&.downcase
|
23
|
+
|
24
|
+
unless resource && action
|
25
|
+
puts "Error: Resource and action are required"
|
26
|
+
puts "Usage: readwise <resource> <action> [options] [arguments]"
|
27
|
+
puts "Run 'readwise --help' for more information"
|
28
|
+
exit 1
|
29
|
+
end
|
30
|
+
|
31
|
+
command_class = CommandRegistry.find(resource, action)
|
32
|
+
unless command_class
|
33
|
+
puts "Error: Unknown command '#{resource} #{action}'"
|
34
|
+
puts "Run 'readwise --help' to see available commands"
|
35
|
+
exit 1
|
36
|
+
end
|
37
|
+
|
38
|
+
command = command_class.new
|
39
|
+
command.execute(args)
|
40
|
+
rescue => e
|
41
|
+
puts "Unexpected error: #{e.message}"
|
42
|
+
exit 1
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def show_help
|
48
|
+
puts <<~HELP
|
49
|
+
Usage: readwise <resource> <action> [options] [arguments]
|
50
|
+
|
51
|
+
Available commands:
|
52
|
+
document create --html-file <file> Send HTML content to Readwise Reader
|
53
|
+
|
54
|
+
Global options:
|
55
|
+
-h, --help Show this help message
|
56
|
+
|
57
|
+
Examples:
|
58
|
+
readwise document create --html-file content.html --title="My Article"
|
59
|
+
readwise document create -f content.html --title="My Article"
|
60
|
+
readwise document create --help
|
61
|
+
HELP
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/readwise/client.rb
CHANGED
@@ -227,7 +227,7 @@ module Readwise
|
|
227
227
|
location_type: res['location_type'],
|
228
228
|
note: res['note'],
|
229
229
|
readwise_url: res['readwise_url'],
|
230
|
-
tags: res['tags'].map { |tag| transform_tag(tag) },
|
230
|
+
tags: (res['tags'] || []).map { |tag| transform_tag(tag) },
|
231
231
|
text: res['text'],
|
232
232
|
updated_at: res['updated_at'],
|
233
233
|
url: res['url'],
|
data/lib/readwise/version.rb
CHANGED
data/readwise.gemspec
CHANGED
@@ -37,8 +37,8 @@ Gem::Specification.new do |spec|
|
|
37
37
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
38
38
|
spec.require_paths = ["lib"]
|
39
39
|
|
40
|
-
spec.add_development_dependency "bundler", "~> 2.
|
41
|
-
spec.add_development_dependency "rake", "~>
|
40
|
+
spec.add_development_dependency "bundler", "~> 2.4"
|
41
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
42
42
|
spec.add_development_dependency "rspec", "~> 3.0"
|
43
43
|
spec.add_development_dependency "rspec-file_fixtures", "~> 0.1.6"
|
44
44
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: readwise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Beckman
|
@@ -15,28 +15,28 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: '2.
|
18
|
+
version: '2.4'
|
19
19
|
type: :development
|
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: '2.
|
25
|
+
version: '2.4'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: rake
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '13.0'
|
33
33
|
type: :development
|
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: '
|
39
|
+
version: '13.0'
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: rspec
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -68,7 +68,8 @@ dependencies:
|
|
68
68
|
description: Minimal Readwise API client and highlight parsing library
|
69
69
|
email:
|
70
70
|
- josh@joshbeckman.org
|
71
|
-
executables:
|
71
|
+
executables:
|
72
|
+
- readwise
|
72
73
|
extensions: []
|
73
74
|
extra_rdoc_files: []
|
74
75
|
files:
|
@@ -83,9 +84,15 @@ files:
|
|
83
84
|
- Rakefile
|
84
85
|
- bin/console
|
85
86
|
- bin/setup
|
87
|
+
- exe/readwise
|
86
88
|
- lib/readwise.rb
|
87
89
|
- lib/readwise/book.rb
|
90
|
+
- lib/readwise/cli.rb
|
91
|
+
- lib/readwise/cli/base_command.rb
|
92
|
+
- lib/readwise/cli/command_registry.rb
|
93
|
+
- lib/readwise/cli/document/create_command.rb
|
88
94
|
- lib/readwise/client.rb
|
95
|
+
- lib/readwise/constants.rb
|
89
96
|
- lib/readwise/document.rb
|
90
97
|
- lib/readwise/highlight.rb
|
91
98
|
- lib/readwise/review.rb
|