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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +8 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +95 -0
  5. data/Rakefile +120 -0
  6. data/benchmark/load.rb +9 -0
  7. data/benchmark/objects.rb +14 -0
  8. data/codegen/generator.rb +277 -0
  9. data/codegen/naming.yml +61 -0
  10. data/codegen/openapi.json +47206 -0
  11. data/codegen/openapi.lock +1 -0
  12. data/codegen/overrides.yml +14 -0
  13. data/docs/design.md +7 -0
  14. data/docs/recipes/csv-import.md +14 -0
  15. data/docs/recipes/oauth.md +9 -0
  16. data/docs/recipes/page-sync.md +10 -0
  17. data/docs/recipes/pagination.md +9 -0
  18. data/docs/recipes/uploads.md +9 -0
  19. data/docs/recipes/webhooks.md +28 -0
  20. data/lib/generators/notion/install_generator.rb +15 -0
  21. data/lib/generators/notion/templates/notion.rb +5 -0
  22. data/lib/notion/batch.rb +45 -0
  23. data/lib/notion/blocks/builder.rb +57 -0
  24. data/lib/notion/blocks/chunker.rb +70 -0
  25. data/lib/notion/blocks.rb +4 -0
  26. data/lib/notion/client.rb +138 -0
  27. data/lib/notion/compat/legacy.rb +47 -0
  28. data/lib/notion/compat.rb +94 -0
  29. data/lib/notion/config.rb +88 -0
  30. data/lib/notion/endpoints/async_tasks.rb +26 -0
  31. data/lib/notion/endpoints/base.rb +107 -0
  32. data/lib/notion/endpoints/blocks.rb +39 -0
  33. data/lib/notion/endpoints/data_sources.rb +30 -0
  34. data/lib/notion/endpoints/file_uploads.rb +35 -0
  35. data/lib/notion/endpoints/pages.rb +56 -0
  36. data/lib/notion/endpoints/views.rb +16 -0
  37. data/lib/notion/errors.rb +111 -0
  38. data/lib/notion/experimental.rb +12 -0
  39. data/lib/notion/file_uploader.rb +142 -0
  40. data/lib/notion/generated/endpoints/agents.rb +64 -0
  41. data/lib/notion/generated/endpoints/async_tasks.rb +20 -0
  42. data/lib/notion/generated/endpoints/blocks.rb +49 -0
  43. data/lib/notion/generated/endpoints/comments.rb +48 -0
  44. data/lib/notion/generated/endpoints/custom_emojis.rb +20 -0
  45. data/lib/notion/generated/endpoints/data_sources.rb +49 -0
  46. data/lib/notion/generated/endpoints/databases.rb +34 -0
  47. data/lib/notion/generated/endpoints/file_uploads.rb +51 -0
  48. data/lib/notion/generated/endpoints/meeting_notes.rb +27 -0
  49. data/lib/notion/generated/endpoints/oauth.rb +34 -0
  50. data/lib/notion/generated/endpoints/pages.rb +65 -0
  51. data/lib/notion/generated/endpoints/search.rb +20 -0
  52. data/lib/notion/generated/endpoints/sessions.rb +48 -0
  53. data/lib/notion/generated/endpoints/users.rb +34 -0
  54. data/lib/notion/generated/endpoints/views.rb +73 -0
  55. data/lib/notion/generated.rb +41 -0
  56. data/lib/notion/id.rb +14 -0
  57. data/lib/notion/markdown/local_converter.rb +34 -0
  58. data/lib/notion/middleware/api_version.rb +18 -0
  59. data/lib/notion/middleware/authentication.rb +52 -0
  60. data/lib/notion/middleware/compatibility.rb +17 -0
  61. data/lib/notion/middleware/instrumentation.rb +63 -0
  62. data/lib/notion/middleware/json_codec.rb +37 -0
  63. data/lib/notion/middleware/logging.rb +36 -0
  64. data/lib/notion/middleware/rate_limiter.rb +78 -0
  65. data/lib/notion/middleware/retry.rb +45 -0
  66. data/lib/notion/middleware/stack.rb +20 -0
  67. data/lib/notion/middleware.rb +11 -0
  68. data/lib/notion/multipart.rb +39 -0
  69. data/lib/notion/oauth.rb +91 -0
  70. data/lib/notion/object_factory.rb +39 -0
  71. data/lib/notion/objects/base.rb +62 -0
  72. data/lib/notion/objects/list.rb +56 -0
  73. data/lib/notion/objects/property_schema.rb +57 -0
  74. data/lib/notion/objects/property_value.rb +56 -0
  75. data/lib/notion/objects/types.rb +56 -0
  76. data/lib/notion/objects.rb +8 -0
  77. data/lib/notion/pagination/cursor_paginator.rb +25 -0
  78. data/lib/notion/query/builder.rb +117 -0
  79. data/lib/notion/query.rb +3 -0
  80. data/lib/notion/railtie.rb +11 -0
  81. data/lib/notion/resolver.rb +72 -0
  82. data/lib/notion/testing.rb +45 -0
  83. data/lib/notion/transport/net_http_adapter.rb +90 -0
  84. data/lib/notion/transport.rb +21 -0
  85. data/lib/notion/version.rb +5 -0
  86. data/lib/notion/webhooks/event.rb +7 -0
  87. data/lib/notion/webhooks/rack_middleware.rb +43 -0
  88. data/lib/notion/webhooks/signature.rb +22 -0
  89. data/lib/notion/webhooks.rb +7 -0
  90. data/lib/notion-client-ruby.rb +3 -0
  91. data/lib/notion.rb +53 -0
  92. 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notion
4
+ class Experimental
5
+ def initialize(client)
6
+ @client = client
7
+ end
8
+
9
+ def agents = @client.agents
10
+ def sessions = @client.sessions
11
+ end
12
+ 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