notion_publish 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 +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE +21 -0
- data/README.md +221 -0
- data/docs/notion-publish-home-page.md +105 -0
- data/docs/usage.md +648 -0
- data/exe/notion-publish +6 -0
- data/lib/notion_publish/adopter.rb +77 -0
- data/lib/notion_publish/cli.rb +212 -0
- data/lib/notion_publish/client.rb +226 -0
- data/lib/notion_publish/commands/adopt.rb +114 -0
- data/lib/notion_publish/commands/command.rb +114 -0
- data/lib/notion_publish/commands/properties.rb +33 -0
- data/lib/notion_publish/commands/publish.rb +134 -0
- data/lib/notion_publish/commands/relink.rb +95 -0
- data/lib/notion_publish/commands/reporting.rb +57 -0
- data/lib/notion_publish/commands/republish.rb +156 -0
- data/lib/notion_publish/commands/status.rb +84 -0
- data/lib/notion_publish/commands/whoami.rb +27 -0
- data/lib/notion_publish/commands.rb +17 -0
- data/lib/notion_publish/decoration.rb +75 -0
- data/lib/notion_publish/document.rb +75 -0
- data/lib/notion_publish/errors.rb +62 -0
- data/lib/notion_publish/fixups.rb +139 -0
- data/lib/notion_publish/links.rb +65 -0
- data/lib/notion_publish/log.rb +49 -0
- data/lib/notion_publish/manifest.rb +224 -0
- data/lib/notion_publish/media.rb +90 -0
- data/lib/notion_publish/notion_digest.rb +23 -0
- data/lib/notion_publish/pool.rb +103 -0
- data/lib/notion_publish/progress.rb +103 -0
- data/lib/notion_publish/property_set.rb +116 -0
- data/lib/notion_publish/publisher.rb +475 -0
- data/lib/notion_publish/reference.rb +84 -0
- data/lib/notion_publish/resolver.rb +216 -0
- data/lib/notion_publish/schema.rb +206 -0
- data/lib/notion_publish/settings.rb +73 -0
- data/lib/notion_publish/sharing.rb +47 -0
- data/lib/notion_publish/status.rb +127 -0
- data/lib/notion_publish/target.rb +34 -0
- data/lib/notion_publish/uploader.rb +85 -0
- data/lib/notion_publish/users.rb +49 -0
- data/lib/notion_publish/version.rb +5 -0
- data/lib/notion_publish.rb +31 -0
- metadata +96 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NotionPublish
|
|
4
|
+
# Where a document is going to land. Everything the CLI accepts — a page ID, a
|
|
5
|
+
# database ID, a data source ID, a URL, a database name — collapses into one
|
|
6
|
+
# of these before anything is written.
|
|
7
|
+
Target = Data.define(:kind, :id, :title, :database_id, :inline) do
|
|
8
|
+
def page? = kind == :page
|
|
9
|
+
def data_source? = kind == :data_source
|
|
10
|
+
|
|
11
|
+
# The `parent` object for POST /v1/pages.
|
|
12
|
+
def parent_param
|
|
13
|
+
case kind
|
|
14
|
+
when :page then { "page_id" => id }
|
|
15
|
+
when :data_source then { "data_source_id" => id }
|
|
16
|
+
else raise ArgumentError, "unknown target kind: #{kind}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# +inline+ is a tri-state: true, false, or nil when we resolved through a
|
|
21
|
+
# data source and never fetched the database that would tell us.
|
|
22
|
+
def kind_label
|
|
23
|
+
case kind
|
|
24
|
+
when :page then "page"
|
|
25
|
+
when :data_source then inline == true ? "inline database" : "database"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def describe
|
|
30
|
+
name = title.nil? || title.empty? ? "(untitled)" : title
|
|
31
|
+
"#{kind_label} #{name.inspect} (#{id})"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module NotionPublish
|
|
6
|
+
# Puts a local file into Notion's own storage and returns its file_upload id.
|
|
7
|
+
#
|
|
8
|
+
# This is the only way to get a local image onto a page: an uploaded file has
|
|
9
|
+
# no URL of its own -- GET /v1/file_uploads/:id returns no url field at all --
|
|
10
|
+
# so it can only ever be referenced by id from a block.
|
|
11
|
+
class Uploader
|
|
12
|
+
SINGLE_PART_LIMIT = 20 * 1024 * 1024
|
|
13
|
+
|
|
14
|
+
CONTENT_TYPES = {
|
|
15
|
+
".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg",
|
|
16
|
+
".gif" => "image/gif", ".webp" => "image/webp", ".svg" => "image/svg+xml",
|
|
17
|
+
".heic" => "image/heic", ".tif" => "image/tiff", ".tiff" => "image/tiff",
|
|
18
|
+
".ico" => "image/vnd.microsoft.icon", ".bmp" => "image/bmp"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(client)
|
|
22
|
+
@client = client
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def upload(path)
|
|
26
|
+
raise Error, "No such image: #{path}" unless File.file?(path)
|
|
27
|
+
|
|
28
|
+
size = File.size(path)
|
|
29
|
+
check_size(path, size)
|
|
30
|
+
content_type = content_type_for(path)
|
|
31
|
+
|
|
32
|
+
created = @client.post("/v1/file_uploads", {
|
|
33
|
+
"mode" => "single_part",
|
|
34
|
+
"filename" => File.basename(path),
|
|
35
|
+
"content_type" => content_type
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
sent = @client.post_file(created["upload_url"], path: path, content_type: content_type)
|
|
39
|
+
raise Error, "Upload of #{path} finished as #{sent['status'].inspect}." unless sent["status"] == "uploaded"
|
|
40
|
+
|
|
41
|
+
created["id"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The block that references an upload. Notion reports it back as an image of
|
|
45
|
+
# type "file" with a short-lived signed URL, minted fresh on every read.
|
|
46
|
+
def self.image_block(upload_id, caption)
|
|
47
|
+
image = { "type" => "file_upload", "file_upload" => { "id" => upload_id } }
|
|
48
|
+
image["caption"] = [{ "type" => "text", "text" => { "content" => caption.to_s } }] unless caption.to_s.empty?
|
|
49
|
+
{ "object" => "block", "type" => "image", "image" => image }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def check_size(path, size)
|
|
55
|
+
limit = [workspace_limit, SINGLE_PART_LIMIT].compact.min
|
|
56
|
+
return if size <= limit
|
|
57
|
+
|
|
58
|
+
raise Error, <<~MSG.strip
|
|
59
|
+
#{File.basename(path)} is #{human(size)}, over the #{human(limit)} limit.
|
|
60
|
+
|
|
61
|
+
Files above #{human(SINGLE_PART_LIMIT)} need Notion's multi-part upload,
|
|
62
|
+
which notion-publish does not do yet.
|
|
63
|
+
MSG
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def workspace_limit
|
|
67
|
+
@client.me.dig("bot", "workspace_limits", "max_file_upload_size_in_bytes")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def content_type_for(path)
|
|
71
|
+
extension = File.extname(path).downcase
|
|
72
|
+
CONTENT_TYPES[extension] or raise Error, <<~MSG.strip
|
|
73
|
+
Cannot tell what kind of image #{File.basename(path)} is.
|
|
74
|
+
|
|
75
|
+
Known extensions: #{CONTENT_TYPES.keys.join(' ')}
|
|
76
|
+
MSG
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def human(bytes)
|
|
80
|
+
return "#{(bytes / 1024.0 / 1024).round(1)} MB" if bytes >= 1024 * 1024
|
|
81
|
+
|
|
82
|
+
"#{(bytes / 1024.0).round(1)} KB"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "did_you_mean"
|
|
4
|
+
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module NotionPublish
|
|
8
|
+
# Resolves a person's name to a user ID, for people properties. Fetched once,
|
|
9
|
+
# and only when a people property is actually being set by name.
|
|
10
|
+
class Users
|
|
11
|
+
def initialize(client)
|
|
12
|
+
@client = client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resolve(name)
|
|
16
|
+
wanted = name.to_s.strip
|
|
17
|
+
matches = all.select { |u| u["name"].to_s.casecmp?(wanted) }
|
|
18
|
+
|
|
19
|
+
case matches.length
|
|
20
|
+
when 1 then matches.first["id"]
|
|
21
|
+
when 0 then raise Error, no_match_message(wanted)
|
|
22
|
+
else raise Error, ambiguous_message(wanted, matches)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def all
|
|
29
|
+
@all ||= @client.get_all("/v1/users")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def people = all.select { |u| u["type"] == "person" }
|
|
33
|
+
|
|
34
|
+
def no_match_message(wanted)
|
|
35
|
+
close = DidYouMean::SpellChecker.new(dictionary: people.map { |u| u["name"] }.compact).correct(wanted)
|
|
36
|
+
lines = ["No workspace user is named #{wanted.inspect}."]
|
|
37
|
+
lines << "" << "Close matches: #{close.join(', ')}" unless close.empty?
|
|
38
|
+
lines << "" << "A user ID works too."
|
|
39
|
+
lines.join("\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ambiguous_message(wanted, matches)
|
|
43
|
+
lines = ["More than one user is named #{wanted.inspect}:", ""]
|
|
44
|
+
matches.each { |u| lines << " #{u['id']} (#{u['type']})" }
|
|
45
|
+
lines << "" << "Use the ID."
|
|
46
|
+
lines.join("\n")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "notion_publish/version"
|
|
4
|
+
require_relative "notion_publish/errors"
|
|
5
|
+
require_relative "notion_publish/notion_digest"
|
|
6
|
+
require_relative "notion_publish/reference"
|
|
7
|
+
require_relative "notion_publish/target"
|
|
8
|
+
require_relative "notion_publish/sharing"
|
|
9
|
+
require_relative "notion_publish/client"
|
|
10
|
+
require_relative "notion_publish/resolver"
|
|
11
|
+
require_relative "notion_publish/adopter"
|
|
12
|
+
require_relative "notion_publish/settings"
|
|
13
|
+
require_relative "notion_publish/document"
|
|
14
|
+
require_relative "notion_publish/property_set"
|
|
15
|
+
require_relative "notion_publish/schema"
|
|
16
|
+
require_relative "notion_publish/users"
|
|
17
|
+
require_relative "notion_publish/decoration"
|
|
18
|
+
require_relative "notion_publish/fixups"
|
|
19
|
+
require_relative "notion_publish/links"
|
|
20
|
+
require_relative "notion_publish/manifest"
|
|
21
|
+
require_relative "notion_publish/status"
|
|
22
|
+
require_relative "notion_publish/media"
|
|
23
|
+
require_relative "notion_publish/uploader"
|
|
24
|
+
require_relative "notion_publish/publisher"
|
|
25
|
+
require_relative "notion_publish/cli"
|
|
26
|
+
|
|
27
|
+
# Publishes Markdown files into Notion as pages, and keeps them updated in
|
|
28
|
+
# place. See NotionPublish::CLI for the command line and
|
|
29
|
+
# NotionPublish::Publisher for the library entry point.
|
|
30
|
+
module NotionPublish
|
|
31
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: notion_publish
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- John Norman
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
A command-line tool that publishes Markdown files as Notion pages and
|
|
14
|
+
updates them in place on later runs. It uses Notion's Markdown endpoints,
|
|
15
|
+
so Notion does the conversion to blocks. It records which page each file
|
|
16
|
+
became, sets database properties from front matter, uploads local images,
|
|
17
|
+
and refuses to overwrite a page someone has edited in Notion. Not
|
|
18
|
+
affiliated with or endorsed by Notion Labs, Inc.
|
|
19
|
+
email:
|
|
20
|
+
- john@outsidecto.com
|
|
21
|
+
executables:
|
|
22
|
+
- notion-publish
|
|
23
|
+
extensions: []
|
|
24
|
+
extra_rdoc_files: []
|
|
25
|
+
files:
|
|
26
|
+
- CHANGELOG.md
|
|
27
|
+
- LICENSE
|
|
28
|
+
- README.md
|
|
29
|
+
- docs/notion-publish-home-page.md
|
|
30
|
+
- docs/usage.md
|
|
31
|
+
- exe/notion-publish
|
|
32
|
+
- lib/notion_publish.rb
|
|
33
|
+
- lib/notion_publish/adopter.rb
|
|
34
|
+
- lib/notion_publish/cli.rb
|
|
35
|
+
- lib/notion_publish/client.rb
|
|
36
|
+
- lib/notion_publish/commands.rb
|
|
37
|
+
- lib/notion_publish/commands/adopt.rb
|
|
38
|
+
- lib/notion_publish/commands/command.rb
|
|
39
|
+
- lib/notion_publish/commands/properties.rb
|
|
40
|
+
- lib/notion_publish/commands/publish.rb
|
|
41
|
+
- lib/notion_publish/commands/relink.rb
|
|
42
|
+
- lib/notion_publish/commands/reporting.rb
|
|
43
|
+
- lib/notion_publish/commands/republish.rb
|
|
44
|
+
- lib/notion_publish/commands/status.rb
|
|
45
|
+
- lib/notion_publish/commands/whoami.rb
|
|
46
|
+
- lib/notion_publish/decoration.rb
|
|
47
|
+
- lib/notion_publish/document.rb
|
|
48
|
+
- lib/notion_publish/errors.rb
|
|
49
|
+
- lib/notion_publish/fixups.rb
|
|
50
|
+
- lib/notion_publish/links.rb
|
|
51
|
+
- lib/notion_publish/log.rb
|
|
52
|
+
- lib/notion_publish/manifest.rb
|
|
53
|
+
- lib/notion_publish/media.rb
|
|
54
|
+
- lib/notion_publish/notion_digest.rb
|
|
55
|
+
- lib/notion_publish/pool.rb
|
|
56
|
+
- lib/notion_publish/progress.rb
|
|
57
|
+
- lib/notion_publish/property_set.rb
|
|
58
|
+
- lib/notion_publish/publisher.rb
|
|
59
|
+
- lib/notion_publish/reference.rb
|
|
60
|
+
- lib/notion_publish/resolver.rb
|
|
61
|
+
- lib/notion_publish/schema.rb
|
|
62
|
+
- lib/notion_publish/settings.rb
|
|
63
|
+
- lib/notion_publish/sharing.rb
|
|
64
|
+
- lib/notion_publish/status.rb
|
|
65
|
+
- lib/notion_publish/target.rb
|
|
66
|
+
- lib/notion_publish/uploader.rb
|
|
67
|
+
- lib/notion_publish/users.rb
|
|
68
|
+
- lib/notion_publish/version.rb
|
|
69
|
+
homepage: https://outsidecto.com/notion-publish/
|
|
70
|
+
licenses:
|
|
71
|
+
- MIT
|
|
72
|
+
metadata:
|
|
73
|
+
source_code_uri: https://github.com/outsidecto/notion-publish
|
|
74
|
+
changelog_uri: https://github.com/outsidecto/notion-publish/blob/main/CHANGELOG.md
|
|
75
|
+
documentation_uri: https://github.com/outsidecto/notion-publish/blob/main/docs/usage.md
|
|
76
|
+
bug_tracker_uri: https://github.com/outsidecto/notion-publish/issues
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '3.2'
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 4.0.18
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Publish Markdown files from a Git repository into Notion, and keep them in
|
|
95
|
+
sync
|
|
96
|
+
test_files: []
|