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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +36 -0
  3. data/LICENSE +21 -0
  4. data/README.md +221 -0
  5. data/docs/notion-publish-home-page.md +105 -0
  6. data/docs/usage.md +648 -0
  7. data/exe/notion-publish +6 -0
  8. data/lib/notion_publish/adopter.rb +77 -0
  9. data/lib/notion_publish/cli.rb +212 -0
  10. data/lib/notion_publish/client.rb +226 -0
  11. data/lib/notion_publish/commands/adopt.rb +114 -0
  12. data/lib/notion_publish/commands/command.rb +114 -0
  13. data/lib/notion_publish/commands/properties.rb +33 -0
  14. data/lib/notion_publish/commands/publish.rb +134 -0
  15. data/lib/notion_publish/commands/relink.rb +95 -0
  16. data/lib/notion_publish/commands/reporting.rb +57 -0
  17. data/lib/notion_publish/commands/republish.rb +156 -0
  18. data/lib/notion_publish/commands/status.rb +84 -0
  19. data/lib/notion_publish/commands/whoami.rb +27 -0
  20. data/lib/notion_publish/commands.rb +17 -0
  21. data/lib/notion_publish/decoration.rb +75 -0
  22. data/lib/notion_publish/document.rb +75 -0
  23. data/lib/notion_publish/errors.rb +62 -0
  24. data/lib/notion_publish/fixups.rb +139 -0
  25. data/lib/notion_publish/links.rb +65 -0
  26. data/lib/notion_publish/log.rb +49 -0
  27. data/lib/notion_publish/manifest.rb +224 -0
  28. data/lib/notion_publish/media.rb +90 -0
  29. data/lib/notion_publish/notion_digest.rb +23 -0
  30. data/lib/notion_publish/pool.rb +103 -0
  31. data/lib/notion_publish/progress.rb +103 -0
  32. data/lib/notion_publish/property_set.rb +116 -0
  33. data/lib/notion_publish/publisher.rb +475 -0
  34. data/lib/notion_publish/reference.rb +84 -0
  35. data/lib/notion_publish/resolver.rb +216 -0
  36. data/lib/notion_publish/schema.rb +206 -0
  37. data/lib/notion_publish/settings.rb +73 -0
  38. data/lib/notion_publish/sharing.rb +47 -0
  39. data/lib/notion_publish/status.rb +127 -0
  40. data/lib/notion_publish/target.rb +34 -0
  41. data/lib/notion_publish/uploader.rb +85 -0
  42. data/lib/notion_publish/users.rb +49 -0
  43. data/lib/notion_publish/version.rb +5 -0
  44. data/lib/notion_publish.rb +31 -0
  45. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionPublish
4
+ VERSION = "0.1.0"
5
+ 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: []