notion-client-ruby 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/.env.example +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +95 -0
- data/Rakefile +120 -0
- data/benchmark/load.rb +9 -0
- data/benchmark/objects.rb +14 -0
- data/codegen/generator.rb +277 -0
- data/codegen/naming.yml +61 -0
- data/codegen/openapi.json +47206 -0
- data/codegen/openapi.lock +1 -0
- data/codegen/overrides.yml +14 -0
- data/docs/design.md +7 -0
- data/docs/recipes/csv-import.md +14 -0
- data/docs/recipes/oauth.md +9 -0
- data/docs/recipes/page-sync.md +10 -0
- data/docs/recipes/pagination.md +9 -0
- data/docs/recipes/uploads.md +9 -0
- data/docs/recipes/webhooks.md +28 -0
- data/lib/generators/notion/install_generator.rb +15 -0
- data/lib/generators/notion/templates/notion.rb +5 -0
- data/lib/notion/batch.rb +45 -0
- data/lib/notion/blocks/builder.rb +57 -0
- data/lib/notion/blocks/chunker.rb +70 -0
- data/lib/notion/blocks.rb +4 -0
- data/lib/notion/client.rb +138 -0
- data/lib/notion/compat/legacy.rb +47 -0
- data/lib/notion/compat.rb +94 -0
- data/lib/notion/config.rb +88 -0
- data/lib/notion/endpoints/async_tasks.rb +26 -0
- data/lib/notion/endpoints/base.rb +107 -0
- data/lib/notion/endpoints/blocks.rb +39 -0
- data/lib/notion/endpoints/data_sources.rb +30 -0
- data/lib/notion/endpoints/file_uploads.rb +35 -0
- data/lib/notion/endpoints/pages.rb +56 -0
- data/lib/notion/endpoints/views.rb +16 -0
- data/lib/notion/errors.rb +111 -0
- data/lib/notion/experimental.rb +12 -0
- data/lib/notion/file_uploader.rb +142 -0
- data/lib/notion/generated/endpoints/agents.rb +64 -0
- data/lib/notion/generated/endpoints/async_tasks.rb +20 -0
- data/lib/notion/generated/endpoints/blocks.rb +49 -0
- data/lib/notion/generated/endpoints/comments.rb +48 -0
- data/lib/notion/generated/endpoints/custom_emojis.rb +20 -0
- data/lib/notion/generated/endpoints/data_sources.rb +49 -0
- data/lib/notion/generated/endpoints/databases.rb +34 -0
- data/lib/notion/generated/endpoints/file_uploads.rb +51 -0
- data/lib/notion/generated/endpoints/meeting_notes.rb +27 -0
- data/lib/notion/generated/endpoints/oauth.rb +34 -0
- data/lib/notion/generated/endpoints/pages.rb +65 -0
- data/lib/notion/generated/endpoints/search.rb +20 -0
- data/lib/notion/generated/endpoints/sessions.rb +48 -0
- data/lib/notion/generated/endpoints/users.rb +34 -0
- data/lib/notion/generated/endpoints/views.rb +73 -0
- data/lib/notion/generated.rb +41 -0
- data/lib/notion/id.rb +14 -0
- data/lib/notion/markdown/local_converter.rb +34 -0
- data/lib/notion/middleware/api_version.rb +18 -0
- data/lib/notion/middleware/authentication.rb +52 -0
- data/lib/notion/middleware/compatibility.rb +17 -0
- data/lib/notion/middleware/instrumentation.rb +63 -0
- data/lib/notion/middleware/json_codec.rb +37 -0
- data/lib/notion/middleware/logging.rb +36 -0
- data/lib/notion/middleware/rate_limiter.rb +78 -0
- data/lib/notion/middleware/retry.rb +45 -0
- data/lib/notion/middleware/stack.rb +20 -0
- data/lib/notion/middleware.rb +11 -0
- data/lib/notion/multipart.rb +39 -0
- data/lib/notion/oauth.rb +91 -0
- data/lib/notion/object_factory.rb +39 -0
- data/lib/notion/objects/base.rb +62 -0
- data/lib/notion/objects/list.rb +56 -0
- data/lib/notion/objects/property_schema.rb +57 -0
- data/lib/notion/objects/property_value.rb +56 -0
- data/lib/notion/objects/types.rb +56 -0
- data/lib/notion/objects.rb +8 -0
- data/lib/notion/pagination/cursor_paginator.rb +25 -0
- data/lib/notion/query/builder.rb +117 -0
- data/lib/notion/query.rb +3 -0
- data/lib/notion/railtie.rb +11 -0
- data/lib/notion/resolver.rb +72 -0
- data/lib/notion/testing.rb +45 -0
- data/lib/notion/transport/net_http_adapter.rb +90 -0
- data/lib/notion/transport.rb +21 -0
- data/lib/notion/version.rb +5 -0
- data/lib/notion/webhooks/event.rb +7 -0
- data/lib/notion/webhooks/rack_middleware.rb +43 -0
- data/lib/notion/webhooks/signature.rb +22 -0
- data/lib/notion/webhooks.rb +7 -0
- data/lib/notion-client-ruby.rb +3 -0
- data/lib/notion.rb +53 -0
- metadata +132 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notion
|
|
4
|
+
module Generated
|
|
5
|
+
module Endpoints
|
|
6
|
+
class AsyncTasks
|
|
7
|
+
TERMINAL = %w[success completed failed canceled cancelled].freeze
|
|
8
|
+
|
|
9
|
+
def wait(task_id:, timeout: 300, interval: 1, sleeper: Kernel.method(:sleep))
|
|
10
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
11
|
+
attempt = 0
|
|
12
|
+
loop do
|
|
13
|
+
task = retrieve(task_id: task_id)
|
|
14
|
+
status = task.status.to_s
|
|
15
|
+
raise Error, "async task #{status}" if status.match?(/fail|cancel/)
|
|
16
|
+
return task if TERMINAL.include?(status)
|
|
17
|
+
raise TimeoutError, "async task timed out" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
18
|
+
|
|
19
|
+
sleeper.call(interval == :exponential ? [2**attempt, 30].min : interval)
|
|
20
|
+
attempt += 1
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Notion
|
|
6
|
+
module Endpoints
|
|
7
|
+
class Base
|
|
8
|
+
class << self
|
|
9
|
+
def operations
|
|
10
|
+
@operations ||= {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def operation(name, verb, template, path:, query:, query_rules:, body_rules:)
|
|
14
|
+
operations[name] = {
|
|
15
|
+
verb: verb,
|
|
16
|
+
path: template,
|
|
17
|
+
path_params: path,
|
|
18
|
+
query_params: query,
|
|
19
|
+
query_rules: query_rules,
|
|
20
|
+
body_rules: body_rules
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(client)
|
|
26
|
+
@client = client
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def invoke_operation(name, params)
|
|
32
|
+
if block_given?
|
|
33
|
+
builder = Query::Builder.new
|
|
34
|
+
yield builder
|
|
35
|
+
params = params.merge(builder.to_h)
|
|
36
|
+
end
|
|
37
|
+
operation = self.class.operations.fetch(name)
|
|
38
|
+
perform(
|
|
39
|
+
name,
|
|
40
|
+
operation[:verb],
|
|
41
|
+
operation[:path],
|
|
42
|
+
params,
|
|
43
|
+
path: operation[:path_params],
|
|
44
|
+
query: operation[:query_params],
|
|
45
|
+
rules: operation.slice(:query_rules, :body_rules)
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def perform(name, verb, template, params, path:, query:, rules: {})
|
|
50
|
+
original_params = params.dup
|
|
51
|
+
request_path = path.reduce(template) { |value, key| value.sub("{#{key}}", escape(params.delete(key.to_sym))) }
|
|
52
|
+
validate_query!(params, rules.fetch(:query_rules, {}))
|
|
53
|
+
request_query = params.slice(*query.map(&:to_sym))
|
|
54
|
+
request_body = prepare_body(params.except(*query.map(&:to_sym)), rules.fetch(:body_rules, {}))
|
|
55
|
+
result = @client.request(verb, request_path, query: request_query, body: request_body)
|
|
56
|
+
return result unless result.is_a?(Objects::List)
|
|
57
|
+
|
|
58
|
+
result.with_fetcher { |cursor| public_send(name, **original_params, start_cursor: cursor) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def prepare_body(body, rules)
|
|
62
|
+
missing = Array(rules[:required]).reject { |key| body.key?(key.to_sym) }
|
|
63
|
+
raise ArgumentError, "required body parameters are missing: #{missing.join(', ')}" unless missing.empty?
|
|
64
|
+
|
|
65
|
+
validate_body_variants!(body, rules[:variants])
|
|
66
|
+
validate_constraints!(body, rules)
|
|
67
|
+
body.empty? && !rules[:send_empty] ? nil : body
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_body_variants!(body, variants)
|
|
71
|
+
variants = Array(variants)
|
|
72
|
+
return if variants.empty? || variants.any? { |keys| keys.all? { |key| body.key?(key.to_sym) } }
|
|
73
|
+
|
|
74
|
+
expected = variants.map { |keys| keys.join(" + ") }.join(" or ")
|
|
75
|
+
raise ArgumentError, "body must include #{expected}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def validate_query!(params, rules)
|
|
79
|
+
missing = Array(rules[:required]).reject { |key| params.key?(key.to_sym) }
|
|
80
|
+
raise ArgumentError, "required query parameters are missing: #{missing.join(', ')}" unless missing.empty?
|
|
81
|
+
|
|
82
|
+
validate_constraints!(params, rules)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_constraints!(params, rules)
|
|
86
|
+
rules.fetch(:enums, {}).each do |key, values|
|
|
87
|
+
value = params[key.to_sym]
|
|
88
|
+
next if value.nil? || values.include?(value.to_s)
|
|
89
|
+
|
|
90
|
+
raise ArgumentError, "#{key} must be one of: #{values.join(', ')}"
|
|
91
|
+
end
|
|
92
|
+
rules.fetch(:max_lengths, {}).each do |key, length|
|
|
93
|
+
value = params[key.to_sym]
|
|
94
|
+
next unless value.respond_to?(:length) && value.length > length
|
|
95
|
+
|
|
96
|
+
raise ArgumentError, "#{key} exceeds #{length} characters"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def escape(value)
|
|
101
|
+
raise ArgumentError, "required path parameter is missing" if value.nil? || value.to_s.empty?
|
|
102
|
+
|
|
103
|
+
URI.encode_uri_component(value.to_s)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notion
|
|
4
|
+
module Generated
|
|
5
|
+
module Endpoints
|
|
6
|
+
class Blocks
|
|
7
|
+
def append_children(block_id:, children:, position: nil, after: nil, on_oversize: :split, **params)
|
|
8
|
+
successful_ids = []
|
|
9
|
+
Notion::Blocks::Chunker.new(children, on_oversize: on_oversize).chunks.each_with_index do |chunk, index|
|
|
10
|
+
placement = if index.zero?
|
|
11
|
+
position || after_position(after)
|
|
12
|
+
else
|
|
13
|
+
after_position(successful_ids.last)
|
|
14
|
+
end
|
|
15
|
+
response = perform(
|
|
16
|
+
:append_children,
|
|
17
|
+
:patch,
|
|
18
|
+
"/v1/blocks/{block_id}/children",
|
|
19
|
+
params.merge(block_id: block_id, children: chunk, position: placement).compact,
|
|
20
|
+
path: ["block_id"],
|
|
21
|
+
query: [],
|
|
22
|
+
rules: { body_rules: { send_empty: true, required: ["children"] } }
|
|
23
|
+
)
|
|
24
|
+
successful_ids.concat(response.results.filter_map(&:id)) if response.respond_to?(:results)
|
|
25
|
+
end
|
|
26
|
+
successful_ids
|
|
27
|
+
rescue Error => e
|
|
28
|
+
raise PartialAppendError.new(e.message, successful_ids: successful_ids, cause: e)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def after_position(id)
|
|
34
|
+
id && { type: "after_block", after_block: { id: id } }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notion
|
|
4
|
+
module Generated
|
|
5
|
+
module Endpoints
|
|
6
|
+
class DataSources
|
|
7
|
+
def query(data_source_id:, **params)
|
|
8
|
+
if block_given?
|
|
9
|
+
builder = Query::Builder.new(
|
|
10
|
+
schema: @client.schema_for(data_source_id),
|
|
11
|
+
strict: @client.config.strict
|
|
12
|
+
)
|
|
13
|
+
yield builder
|
|
14
|
+
params.merge!(builder.to_h)
|
|
15
|
+
end
|
|
16
|
+
params[:data_source_id] = data_source_id
|
|
17
|
+
invoke_operation(:query, params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sync_since(data_source_id:, time:, **params)
|
|
21
|
+
filter = {
|
|
22
|
+
timestamp: "last_edited_time",
|
|
23
|
+
last_edited_time: { on_or_after: time.iso8601 }
|
|
24
|
+
}
|
|
25
|
+
query(data_source_id: data_source_id, **params, filter: filter)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notion
|
|
4
|
+
module Generated
|
|
5
|
+
module Endpoints
|
|
6
|
+
class FileUploads
|
|
7
|
+
def send(file_upload_id:, data:, filename:, content_type:, part_number: nil)
|
|
8
|
+
send_part(
|
|
9
|
+
file_upload_id: file_upload_id,
|
|
10
|
+
data: data,
|
|
11
|
+
filename: filename,
|
|
12
|
+
content_type: content_type,
|
|
13
|
+
part_number: part_number
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def send_part(file_upload_id:, data:, filename:, content_type:, part_number: nil)
|
|
18
|
+
body, header = Multipart.encode(
|
|
19
|
+
data,
|
|
20
|
+
filename: filename,
|
|
21
|
+
content_type: content_type,
|
|
22
|
+
part_number: part_number
|
|
23
|
+
)
|
|
24
|
+
@client.request(
|
|
25
|
+
:post,
|
|
26
|
+
"/v1/file_uploads/#{URI.encode_uri_component(file_upload_id)}/send",
|
|
27
|
+
headers: { "content-type" => header },
|
|
28
|
+
body: body,
|
|
29
|
+
idempotent: !part_number.nil?
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notion
|
|
4
|
+
module Generated
|
|
5
|
+
module Endpoints
|
|
6
|
+
class Pages
|
|
7
|
+
def create(parent:, **params)
|
|
8
|
+
params[:parent] = parent
|
|
9
|
+
properties = params[:properties]
|
|
10
|
+
parent, data_source_id = normalize_parent(parent)
|
|
11
|
+
params[:parent] = parent unless parent.empty?
|
|
12
|
+
params[:properties] = convert(properties, data_source_id) if properties && data_source_id
|
|
13
|
+
invoke_operation(:create, params)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update(page_id:, **params)
|
|
17
|
+
if primitive?(params[:properties])
|
|
18
|
+
page = retrieve(page_id: page_id)
|
|
19
|
+
parent = page.raw.fetch("parent", {})
|
|
20
|
+
data_source_id = parent["data_source_id"] || parent["database_id"]
|
|
21
|
+
params[:properties] = convert(params[:properties], data_source_id)
|
|
22
|
+
end
|
|
23
|
+
params[:page_id] = page_id
|
|
24
|
+
invoke_operation(:update, params)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def normalize_parent(parent)
|
|
30
|
+
data_source_id = parent[:data_source_id] || parent["data_source_id"]
|
|
31
|
+
database_id = parent[:database_id] || parent["database_id"]
|
|
32
|
+
return [parent, data_source_id] if data_source_id || !database_id
|
|
33
|
+
return [parent, database_id] if @client.config.notion_version == "2022-06-28"
|
|
34
|
+
|
|
35
|
+
data_source_id = @client.resolve_data_source(database_id: database_id)
|
|
36
|
+
normalized = parent.except(:database_id, "database_id").merge(data_source_id: data_source_id)
|
|
37
|
+
[normalized, data_source_id]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def primitive?(properties)
|
|
41
|
+
properties&.any? { |_name, value| !value.is_a?(Hash) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def convert(properties, data_source_id)
|
|
45
|
+
return properties unless data_source_id
|
|
46
|
+
|
|
47
|
+
Objects::PropertySchema.convert(
|
|
48
|
+
properties,
|
|
49
|
+
@client.schema_for(data_source_id),
|
|
50
|
+
strict: @client.config.strict
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notion
|
|
4
|
+
module Generated
|
|
5
|
+
module Endpoints
|
|
6
|
+
class Views
|
|
7
|
+
def with_query(view_id:, **params)
|
|
8
|
+
query = create_query(view_id: view_id, **params)
|
|
9
|
+
yield query_results(view_id: view_id, query_id: query.id)
|
|
10
|
+
ensure
|
|
11
|
+
delete_query(view_id: view_id, query_id: query.id) if query.respond_to?(:id)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notion
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class TransportError < Error
|
|
7
|
+
attr_reader :cause
|
|
8
|
+
|
|
9
|
+
def initialize(message, cause: nil)
|
|
10
|
+
@cause = cause
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class TimeoutError < TransportError; end
|
|
16
|
+
|
|
17
|
+
class APIError < Error
|
|
18
|
+
attr_reader :status, :code, :request_id, :additional_data, :response, :retry_after
|
|
19
|
+
|
|
20
|
+
def initialize(response)
|
|
21
|
+
unless response.respond_to?(:body)
|
|
22
|
+
super(response.to_s)
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
body = response.body.is_a?(Hash) ? response.body : {}
|
|
27
|
+
@status = response.status
|
|
28
|
+
@code = body["code"]
|
|
29
|
+
@request_id = response.request_id || body["request_id"]
|
|
30
|
+
@additional_data = body["additional_data"]
|
|
31
|
+
@retry_after = response.headers["retry-after"]&.to_f
|
|
32
|
+
@response = response
|
|
33
|
+
super(body["message"] || "Notion API request failed with status #{@status}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def retryable?
|
|
37
|
+
status == 429 || status == 529 || status.between?(500, 599)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.from_response(response)
|
|
41
|
+
code = response.body.is_a?(Hash) && response.body["code"]
|
|
42
|
+
(ERROR_CODES[code] || STATUS_ERRORS[response.status] || self).new(response)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class BadRequestError < APIError; end
|
|
47
|
+
class InvalidJSONError < BadRequestError; end
|
|
48
|
+
class InvalidRequestURLError < BadRequestError; end
|
|
49
|
+
class InvalidRequestError < BadRequestError; end
|
|
50
|
+
class InvalidGrantError < BadRequestError; end
|
|
51
|
+
class ValidationError < BadRequestError; end
|
|
52
|
+
class MissingVersionError < BadRequestError; end
|
|
53
|
+
class InvalidBetaError < BadRequestError; end
|
|
54
|
+
class UnauthorizedError < APIError; end
|
|
55
|
+
class RestrictedResourceError < APIError; end
|
|
56
|
+
class ObjectNotFoundError < APIError; end
|
|
57
|
+
class ConflictError < APIError; end
|
|
58
|
+
class RateLimitedError < APIError; end
|
|
59
|
+
class ServerError < APIError; end
|
|
60
|
+
class InternalServerError < ServerError; end
|
|
61
|
+
class BadGatewayError < ServerError; end
|
|
62
|
+
class ServiceUnavailableError < ServerError; end
|
|
63
|
+
class DatabaseConnectionUnavailableError < ServiceUnavailableError; end
|
|
64
|
+
class GatewayTimeoutError < ServerError; end
|
|
65
|
+
class ServiceOverloadError < ServerError; end
|
|
66
|
+
class UnsupportedInVersionError < Error; end
|
|
67
|
+
|
|
68
|
+
class PartialAppendError < Error
|
|
69
|
+
attr_reader :successful_ids, :cause
|
|
70
|
+
|
|
71
|
+
def initialize(message, successful_ids:, cause: nil)
|
|
72
|
+
@successful_ids = successful_ids
|
|
73
|
+
@cause = cause
|
|
74
|
+
super(message)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
APIError::ERROR_CODES = {
|
|
79
|
+
"invalid_json" => InvalidJSONError,
|
|
80
|
+
"invalid_request_url" => InvalidRequestURLError,
|
|
81
|
+
"invalid_request" => InvalidRequestError,
|
|
82
|
+
"invalid_grant" => InvalidGrantError,
|
|
83
|
+
"validation_error" => ValidationError,
|
|
84
|
+
"missing_version" => MissingVersionError,
|
|
85
|
+
"invalid_beta" => InvalidBetaError,
|
|
86
|
+
"unauthorized" => UnauthorizedError,
|
|
87
|
+
"restricted_resource" => RestrictedResourceError,
|
|
88
|
+
"object_not_found" => ObjectNotFoundError,
|
|
89
|
+
"conflict_error" => ConflictError,
|
|
90
|
+
"rate_limited" => RateLimitedError,
|
|
91
|
+
"internal_server_error" => InternalServerError,
|
|
92
|
+
"bad_gateway" => BadGatewayError,
|
|
93
|
+
"service_unavailable" => ServiceUnavailableError,
|
|
94
|
+
"database_connection_unavailable" => DatabaseConnectionUnavailableError,
|
|
95
|
+
"gateway_timeout" => GatewayTimeoutError,
|
|
96
|
+
"service_overload" => ServiceOverloadError
|
|
97
|
+
}.freeze
|
|
98
|
+
APIError::STATUS_ERRORS = {
|
|
99
|
+
400 => BadRequestError,
|
|
100
|
+
401 => UnauthorizedError,
|
|
101
|
+
403 => RestrictedResourceError,
|
|
102
|
+
404 => ObjectNotFoundError,
|
|
103
|
+
409 => ConflictError,
|
|
104
|
+
429 => RateLimitedError,
|
|
105
|
+
500 => InternalServerError,
|
|
106
|
+
502 => BadGatewayError,
|
|
107
|
+
503 => ServiceUnavailableError,
|
|
108
|
+
504 => GatewayTimeoutError,
|
|
109
|
+
529 => ServiceOverloadError
|
|
110
|
+
}.freeze
|
|
111
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module Notion
|
|
7
|
+
class FileUploader
|
|
8
|
+
PART_SIZE = 20 * 1024 * 1024
|
|
9
|
+
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def upload(path: nil, io: nil, filename: nil, content_type: "application/octet-stream", on_progress: nil,
|
|
15
|
+
concurrency: 3)
|
|
16
|
+
raise ArgumentError, "concurrency must be positive" unless concurrency.to_i.positive?
|
|
17
|
+
|
|
18
|
+
if path
|
|
19
|
+
return File.open(path, "rb") do |file|
|
|
20
|
+
upload_io(file, filename || File.basename(path), content_type, on_progress, concurrency)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
raise ArgumentError, "path or io is required" unless io
|
|
25
|
+
|
|
26
|
+
upload_io(io, filename || "upload.bin", content_type, on_progress, concurrency)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def import(url:, filename: nil, content_type: nil, wait: true, timeout: 300, interval: 1,
|
|
30
|
+
sleeper: Kernel.method(:sleep))
|
|
31
|
+
uri = URI(url)
|
|
32
|
+
raise ArgumentError, "URL must use http or https" unless %w[http https].include?(uri.scheme)
|
|
33
|
+
|
|
34
|
+
upload = @client.file_uploads.create(**{
|
|
35
|
+
mode: "external_url",
|
|
36
|
+
external_url: url,
|
|
37
|
+
filename: filename,
|
|
38
|
+
content_type: content_type
|
|
39
|
+
}.compact)
|
|
40
|
+
wait ? wait_for_upload(upload, timeout, interval, sleeper) : upload
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def wait_for_upload(upload, timeout, interval, sleeper)
|
|
46
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
47
|
+
loop do
|
|
48
|
+
status = upload.respond_to?(:status) ? upload.status.to_s : ""
|
|
49
|
+
return upload if status == "uploaded"
|
|
50
|
+
raise Error, "file import #{status}" if status.match?(/fail|expire/)
|
|
51
|
+
raise TimeoutError, "file import timed out" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
52
|
+
|
|
53
|
+
sleeper.call(interval)
|
|
54
|
+
upload = @client.file_uploads.retrieve(file_upload_id: upload.id)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def upload_io(io, filename, content_type, on_progress, concurrency)
|
|
59
|
+
size = io.respond_to?(:size) ? io.size : nil
|
|
60
|
+
return upload_unsized(io, filename, content_type, on_progress, concurrency) unless size
|
|
61
|
+
|
|
62
|
+
mode = size > PART_SIZE ? "multi_part" : "single_part"
|
|
63
|
+
parts = (size.to_f / PART_SIZE).ceil
|
|
64
|
+
create_params = {
|
|
65
|
+
mode: mode,
|
|
66
|
+
filename: filename,
|
|
67
|
+
content_type: content_type
|
|
68
|
+
}
|
|
69
|
+
create_params[:number_of_parts] = parts if mode == "multi_part"
|
|
70
|
+
upload = @client.file_uploads.create(**create_params)
|
|
71
|
+
options = { filename: filename, content_type: content_type, size: size, on_progress: on_progress,
|
|
72
|
+
concurrency: concurrency }
|
|
73
|
+
send_parts(upload.id, io, mode, options)
|
|
74
|
+
mode == "multi_part" ? @client.file_uploads.complete(file_upload_id: upload.id) : upload
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def upload_unsized(io, filename, content_type, on_progress, concurrency)
|
|
78
|
+
Tempfile.create("notion-upload") do |file|
|
|
79
|
+
loop do
|
|
80
|
+
chunk = io.read(PART_SIZE)
|
|
81
|
+
break unless chunk
|
|
82
|
+
|
|
83
|
+
file.write(chunk)
|
|
84
|
+
end
|
|
85
|
+
file.flush
|
|
86
|
+
file.rewind
|
|
87
|
+
upload_io(file, filename, content_type, on_progress, concurrency)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def send_parts(id, io, mode, options)
|
|
92
|
+
if mode == "multi_part"
|
|
93
|
+
return send_parts_in_parallel(
|
|
94
|
+
id, io, options[:filename], options[:content_type], options[:size], options[:on_progress],
|
|
95
|
+
options[:concurrency]
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
send_part(id, io.read(PART_SIZE), options[:filename], options[:content_type], nil)
|
|
100
|
+
options[:on_progress]&.call(options[:size], options[:size])
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def send_parts_in_parallel(id, io, filename, content_type, size, on_progress, concurrency)
|
|
104
|
+
read_mutex = Mutex.new
|
|
105
|
+
progress_mutex = Mutex.new
|
|
106
|
+
sent = 0
|
|
107
|
+
part = 0
|
|
108
|
+
errors = Queue.new
|
|
109
|
+
workers = Array.new(concurrency) do
|
|
110
|
+
Thread.new do
|
|
111
|
+
loop do
|
|
112
|
+
job = read_mutex.synchronize do
|
|
113
|
+
data = io.read(PART_SIZE)
|
|
114
|
+
data && [part += 1, data]
|
|
115
|
+
end
|
|
116
|
+
break unless job
|
|
117
|
+
|
|
118
|
+
send_part(id, job.last, filename, content_type, job.first)
|
|
119
|
+
progress_mutex.synchronize do
|
|
120
|
+
sent += job.last.bytesize
|
|
121
|
+
on_progress&.call(sent, size)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
errors << e
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
workers.each(&:join)
|
|
129
|
+
raise errors.pop unless errors.empty?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def send_part(id, data, filename, content_type, part_number)
|
|
133
|
+
@client.file_uploads.send_part(
|
|
134
|
+
file_upload_id: id,
|
|
135
|
+
data: data,
|
|
136
|
+
filename: filename,
|
|
137
|
+
content_type: content_type,
|
|
138
|
+
part_number: part_number
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is generated. DO NOT EDIT.
|
|
4
|
+
|
|
5
|
+
module Notion
|
|
6
|
+
module Generated
|
|
7
|
+
module Endpoints
|
|
8
|
+
class Agents < Notion::Endpoints::Base
|
|
9
|
+
operation :query, :post, "/v1/agents/query", path: [], query: [], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: true, required: [], variants: [], enums: {}, max_lengths: {} }
|
|
10
|
+
operation :retrieve, :get, "/v1/agents/{agent_id}", path: ["agent_id"], query: ["verbose"], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: false, required: [], variants: [], enums: {}, max_lengths: {} }
|
|
11
|
+
operation :delete, :delete, "/v1/agents/{agent_id}", path: ["agent_id"], query: [], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: false, required: [], variants: [], enums: {}, max_lengths: {} }
|
|
12
|
+
operation :insights, :get, "/v1/agents/{agent_id}/insights", path: ["agent_id"], query: ["start_time", "end_time"], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: false, required: [], variants: [], enums: {}, max_lengths: {} }
|
|
13
|
+
operation :update_status, :patch, "/v1/agents/{agent_id}/status", path: ["agent_id"], query: [], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: true, required: ["status"], variants: [["status"]], enums: {"status" => ["active", "disabled"]}, max_lengths: {} }
|
|
14
|
+
operation :update_credit_limit, :patch, "/v1/agents/{agent_id}/credit_limit", path: ["agent_id"], query: [], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: true, required: ["credit_limit"], variants: [["credit_limit"]], enums: {}, max_lengths: {} }
|
|
15
|
+
operation :batch, :post, "/v1/agents/batch", path: [], query: [], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: true, required: ["operations"], variants: [["operations"]], enums: {}, max_lengths: {} }
|
|
16
|
+
|
|
17
|
+
# Query agents
|
|
18
|
+
# @see https://developers.notion.com/reference/notion-agent-apis/query-agents
|
|
19
|
+
def query(**params, &block)
|
|
20
|
+
|
|
21
|
+
invoke_operation(:query, params, &block)
|
|
22
|
+
end
|
|
23
|
+
# Get agent
|
|
24
|
+
# @see https://developers.notion.com/reference/notion-agent-apis/retrieve-agent
|
|
25
|
+
def retrieve(agent_id:, **params, &block)
|
|
26
|
+
params[:agent_id] = agent_id
|
|
27
|
+
invoke_operation(:retrieve, params, &block)
|
|
28
|
+
end
|
|
29
|
+
# Delete agent
|
|
30
|
+
# @see https://developers.notion.com/reference/notion-agent-apis/delete-agent
|
|
31
|
+
def delete(agent_id:, **params, &block)
|
|
32
|
+
params[:agent_id] = agent_id
|
|
33
|
+
invoke_operation(:delete, params, &block)
|
|
34
|
+
end
|
|
35
|
+
# Get agent insights
|
|
36
|
+
# @see https://developers.notion.com/reference/notion-agent-apis/retrieve-agent-insights
|
|
37
|
+
def insights(agent_id:, **params, &block)
|
|
38
|
+
params[:agent_id] = agent_id
|
|
39
|
+
invoke_operation(:insights, params, &block)
|
|
40
|
+
end
|
|
41
|
+
# Update agent status
|
|
42
|
+
# @see https://developers.notion.com/reference/notion-agent-apis/update-agent-status
|
|
43
|
+
def update_status(agent_id:, status:, **params, &block)
|
|
44
|
+
params[:agent_id] = agent_id
|
|
45
|
+
params[:status] = status
|
|
46
|
+
invoke_operation(:update_status, params, &block)
|
|
47
|
+
end
|
|
48
|
+
# Update agent credit limit
|
|
49
|
+
# @see https://developers.notion.com/reference/notion-agent-apis/update-agent-credit-limit
|
|
50
|
+
def update_credit_limit(agent_id:, credit_limit:, **params, &block)
|
|
51
|
+
params[:agent_id] = agent_id
|
|
52
|
+
params[:credit_limit] = credit_limit
|
|
53
|
+
invoke_operation(:update_credit_limit, params, &block)
|
|
54
|
+
end
|
|
55
|
+
# Apply many agent operations
|
|
56
|
+
# @see https://developers.notion.com/reference/notion-agent-apis/batch-manage-agent
|
|
57
|
+
def batch(operations:, **params, &block)
|
|
58
|
+
params[:operations] = operations
|
|
59
|
+
invoke_operation(:batch, params, &block)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This file is generated. DO NOT EDIT.
|
|
4
|
+
|
|
5
|
+
module Notion
|
|
6
|
+
module Generated
|
|
7
|
+
module Endpoints
|
|
8
|
+
class AsyncTasks < Notion::Endpoints::Base
|
|
9
|
+
operation :retrieve, :get, "/v1/async_tasks/{task_id}", path: ["task_id"], query: [], query_rules: { required: [], enums: {}, max_lengths: {} }, body_rules: { send_empty: false, required: [], variants: [], enums: {}, max_lengths: {} }
|
|
10
|
+
|
|
11
|
+
# Retrieve an async task
|
|
12
|
+
# @see https://developers.notion.com/reference/retrieve-async-task
|
|
13
|
+
def retrieve(task_id:, **params, &block)
|
|
14
|
+
params[:task_id] = task_id
|
|
15
|
+
invoke_operation(:retrieve, params, &block)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|