exa-ai-ruby 1.0.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 +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +247 -0
  5. data/lib/exa/client.rb +33 -0
  6. data/lib/exa/errors.rb +34 -0
  7. data/lib/exa/internal/transport/base_client.rb +171 -0
  8. data/lib/exa/internal/transport/pooled_net_requester.rb +113 -0
  9. data/lib/exa/internal/transport/stream.rb +74 -0
  10. data/lib/exa/internal/util.rb +133 -0
  11. data/lib/exa/resources/base.rb +26 -0
  12. data/lib/exa/resources/events.rb +32 -0
  13. data/lib/exa/resources/imports.rb +58 -0
  14. data/lib/exa/resources/research.rb +50 -0
  15. data/lib/exa/resources/search.rb +44 -0
  16. data/lib/exa/resources/webhooks.rb +67 -0
  17. data/lib/exa/resources/websets/enrichments.rb +57 -0
  18. data/lib/exa/resources/websets/items.rb +40 -0
  19. data/lib/exa/resources/websets/monitors.rb +75 -0
  20. data/lib/exa/resources/websets.rb +71 -0
  21. data/lib/exa/resources.rb +9 -0
  22. data/lib/exa/responses/contents_response.rb +35 -0
  23. data/lib/exa/responses/event_response.rb +43 -0
  24. data/lib/exa/responses/helpers.rb +29 -0
  25. data/lib/exa/responses/import_response.rb +90 -0
  26. data/lib/exa/responses/monitor_response.rb +77 -0
  27. data/lib/exa/responses/raw_response.rb +14 -0
  28. data/lib/exa/responses/research_response.rb +56 -0
  29. data/lib/exa/responses/result.rb +61 -0
  30. data/lib/exa/responses/search_response.rb +43 -0
  31. data/lib/exa/responses/webhook_response.rb +95 -0
  32. data/lib/exa/responses/webset_response.rb +136 -0
  33. data/lib/exa/responses.rb +13 -0
  34. data/lib/exa/types/answer.rb +30 -0
  35. data/lib/exa/types/base.rb +66 -0
  36. data/lib/exa/types/contents.rb +25 -0
  37. data/lib/exa/types/enums.rb +47 -0
  38. data/lib/exa/types/find_similar.rb +26 -0
  39. data/lib/exa/types/research.rb +18 -0
  40. data/lib/exa/types/schema.rb +58 -0
  41. data/lib/exa/types/search.rb +74 -0
  42. data/lib/exa/types.rb +10 -0
  43. data/lib/exa/version.rb +5 -0
  44. data/lib/exa.rb +14 -0
  45. metadata +170 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Exa
6
+ module Resources
7
+ class Websets < Base
8
+ require_relative "websets/monitors"
9
+ require_relative "websets/items"
10
+ require_relative "websets/enrichments"
11
+
12
+ attr_reader :monitors, :items, :enrichments
13
+
14
+ def initialize(client:)
15
+ super
16
+ @monitors = Exa::Resources::Websets::Monitors.new(client: client)
17
+ @items = Exa::Resources::Websets::Items.new(client: client)
18
+ @enrichments = Exa::Resources::Websets::Enrichments.new(client: client)
19
+ end
20
+
21
+ def create(params)
22
+ client.request(
23
+ method: :post,
24
+ path: websets_path,
25
+ body: params,
26
+ response_model: Exa::Responses::Webset
27
+ )
28
+ end
29
+
30
+ def list(params = nil)
31
+ client.request(
32
+ method: :get,
33
+ path: websets_path,
34
+ query: params,
35
+ response_model: Exa::Responses::WebsetListResponse
36
+ )
37
+ end
38
+
39
+ def retrieve(webset_id)
40
+ client.request(
41
+ method: :get,
42
+ path: websets_path(webset_id),
43
+ response_model: Exa::Responses::Webset
44
+ )
45
+ end
46
+
47
+ def update(webset_id, params)
48
+ client.request(
49
+ method: :patch,
50
+ path: websets_path(webset_id),
51
+ body: params,
52
+ response_model: Exa::Responses::Webset
53
+ )
54
+ end
55
+
56
+ def delete(webset_id)
57
+ client.request(
58
+ method: :delete,
59
+ path: websets_path(webset_id),
60
+ response_model: Exa::Responses::Webset
61
+ )
62
+ end
63
+
64
+ private
65
+
66
+ def websets_path(*parts)
67
+ ["v0", "websets", *parts]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resources/base"
4
+ require_relative "resources/search"
5
+ require_relative "resources/research"
6
+ require_relative "resources/websets"
7
+ require_relative "resources/events"
8
+ require_relative "resources/webhooks"
9
+ require_relative "resources/imports"
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ class ContentStatus < T::Struct
6
+ const :id, String
7
+ const :status, String
8
+ const :error, T.nilable(T::Hash[Symbol, T.untyped])
9
+
10
+ def self.from_hash(hash)
11
+ sym = Helpers.symbolize_keys(hash)
12
+ new(id: sym[:id], status: sym[:status], error: sym[:error])
13
+ end
14
+ end
15
+
16
+ class ContentsResponse < T::Struct
17
+ const :request_id, T.nilable(String)
18
+ const :results, T::Array[ResultWithContent]
19
+ const :context, T.nilable(String)
20
+ const :statuses, T.nilable(T::Array[ContentStatus])
21
+ const :cost_dollars, T.nilable(Float)
22
+
23
+ def self.from_hash(hash)
24
+ sym = Helpers.symbolize_keys(hash)
25
+ new(
26
+ request_id: sym[:requestId],
27
+ results: Array(sym[:results]).map { ResultWithContent.from_hash(_1) },
28
+ context: sym[:context],
29
+ statuses: sym[:statuses]&.map { ContentStatus.from_hash(_1) },
30
+ cost_dollars: sym[:costDollars]&.to_f
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ class Event < T::Struct
6
+ const :id, String
7
+ const :object, T.nilable(String)
8
+ const :type, String
9
+ const :data, T.nilable(T.untyped)
10
+ const :created_at, T.nilable(String)
11
+ const :raw, T::Hash[Symbol, T.untyped]
12
+
13
+ def self.from_hash(hash)
14
+ sym = Helpers.symbolize_keys(hash)
15
+ data = sym[:data]
16
+ normalized_data = data.is_a?(Hash) ? Helpers.symbolize_keys(data) : data
17
+ new(
18
+ id: sym[:id],
19
+ object: sym[:object],
20
+ type: sym[:type],
21
+ data: normalized_data,
22
+ created_at: sym[:createdAt],
23
+ raw: sym
24
+ )
25
+ end
26
+ end
27
+
28
+ class EventListResponse < T::Struct
29
+ const :data, T::Array[Event]
30
+ const :has_more, T.nilable(T::Boolean)
31
+ const :next_cursor, T.nilable(String)
32
+
33
+ def self.from_hash(hash)
34
+ sym = Helpers.symbolize_keys(hash)
35
+ new(
36
+ data: Array(sym[:data]).map { Event.from_hash(_1) },
37
+ has_more: sym[:hasMore],
38
+ next_cursor: sym[:nextCursor]
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ module Helpers
6
+ module_function
7
+
8
+ def symbolize_keys(value)
9
+ case value
10
+ when Hash
11
+ value.each_with_object({}) do |(k, v), acc|
12
+ acc[k.to_sym] = symbolize_keys(v)
13
+ end
14
+ when Array
15
+ value.map { symbolize_keys(_1) }
16
+ else
17
+ value
18
+ end
19
+ end
20
+
21
+ def stringify_string_hash(hash)
22
+ return nil if hash.nil?
23
+ hash.each_with_object({}) do |(k, v), acc|
24
+ acc[k.to_s] = v
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ module ImportAttributes
6
+ module_function
7
+
8
+ def apply(base)
9
+ base.const :id, String
10
+ base.const :object, T.nilable(String)
11
+ base.const :status, T.nilable(String)
12
+ base.const :format, T.nilable(String)
13
+ base.const :entity, T.nilable(T.untyped)
14
+ base.const :title, T.nilable(String)
15
+ base.const :count, T.nilable(Integer)
16
+ base.const :metadata, T.nilable(T::Hash[String, String])
17
+ base.const :failed_reason, T.nilable(String)
18
+ base.const :failed_at, T.nilable(String)
19
+ base.const :failed_message, T.nilable(String)
20
+ base.const :created_at, T.nilable(String)
21
+ base.const :updated_at, T.nilable(String)
22
+ end
23
+ end
24
+
25
+ module ImportResponseHelpers
26
+ module_function
27
+
28
+ def build_common(sym)
29
+ entity = sym[:entity]
30
+ entity = Helpers.symbolize_keys(entity) if entity.is_a?(Hash)
31
+ {
32
+ id: sym[:id],
33
+ object: sym[:object],
34
+ status: sym[:status],
35
+ format: sym[:format],
36
+ entity: entity,
37
+ title: sym[:title],
38
+ count: sym[:count]&.to_i,
39
+ metadata: Helpers.stringify_string_hash(sym[:metadata]),
40
+ failed_reason: sym[:failedReason],
41
+ failed_at: sym[:failedAt],
42
+ failed_message: sym[:failedMessage],
43
+ created_at: sym[:createdAt],
44
+ updated_at: sym[:updatedAt]
45
+ }
46
+ end
47
+ end
48
+
49
+ class Import < T::Struct
50
+ ImportAttributes.apply(self)
51
+
52
+ def self.from_hash(hash)
53
+ sym = Helpers.symbolize_keys(hash)
54
+ new(**ImportResponseHelpers.build_common(sym))
55
+ end
56
+ end
57
+
58
+ class ImportCreationResponse < T::Struct
59
+ ImportAttributes.apply(self)
60
+ const :upload_url, T.nilable(String)
61
+ const :upload_valid_until, T.nilable(String)
62
+
63
+ def self.from_hash(hash)
64
+ sym = Helpers.symbolize_keys(hash)
65
+ attrs = ImportResponseHelpers.build_common(sym).merge(
66
+ upload_url: sym[:uploadUrl],
67
+ upload_valid_until: sym[:uploadValidUntil]
68
+ )
69
+ new(**attrs)
70
+ end
71
+ end
72
+
73
+ class ImportListResponse < T::Struct
74
+ const :data, T::Array[Import]
75
+ const :has_more, T.nilable(T::Boolean)
76
+ const :next_cursor, T.nilable(String)
77
+
78
+ def self.from_hash(hash)
79
+ sym = Helpers.symbolize_keys(hash)
80
+ new(
81
+ data: Array(sym[:data]).map { Import.from_hash(_1) },
82
+ has_more: sym[:hasMore],
83
+ next_cursor: sym[:nextCursor]
84
+ )
85
+ end
86
+ end
87
+
88
+ ImportResponse = T.type_alias { T.any(Import, ImportCreationResponse) }
89
+ end
90
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ class Monitor < T::Struct
6
+ const :id, String
7
+ const :status, T.nilable(String)
8
+ const :webset_id, T.nilable(String)
9
+ const :cadence, T.nilable(T::Hash[Symbol, T.untyped])
10
+ const :behavior, T.nilable(T::Hash[Symbol, T.untyped])
11
+ const :search_parameters, T.nilable(T::Hash[Symbol, T.untyped])
12
+ const :raw, T::Hash[Symbol, T.untyped]
13
+
14
+ def self.from_hash(hash)
15
+ sym = Helpers.symbolize_keys(hash)
16
+ new(
17
+ id: sym[:id],
18
+ status: sym[:status],
19
+ webset_id: sym[:websetId],
20
+ cadence: sym[:cadence],
21
+ behavior: sym[:behavior],
22
+ search_parameters: sym[:searchParameters],
23
+ raw: sym
24
+ )
25
+ end
26
+ end
27
+
28
+ class MonitorListResponse < T::Struct
29
+ const :data, T::Array[Monitor]
30
+ const :next_page_token, T.nilable(String)
31
+
32
+ def self.from_hash(hash)
33
+ sym = Helpers.symbolize_keys(hash)
34
+ new(
35
+ data: Array(sym[:data]).map { Monitor.from_hash(_1) },
36
+ next_page_token: sym[:nextPageToken]
37
+ )
38
+ end
39
+ end
40
+
41
+ class MonitorRun < T::Struct
42
+ const :id, String
43
+ const :monitor_id, T.nilable(String)
44
+ const :status, T.nilable(String)
45
+ const :run_type, T.nilable(String)
46
+ const :started_at, T.nilable(String)
47
+ const :finished_at, T.nilable(String)
48
+ const :raw, T::Hash[Symbol, T.untyped]
49
+
50
+ def self.from_hash(hash)
51
+ sym = Helpers.symbolize_keys(hash)
52
+ new(
53
+ id: sym[:id],
54
+ monitor_id: sym[:monitorId],
55
+ status: sym[:status],
56
+ run_type: sym[:type] || sym[:runType],
57
+ started_at: sym[:startedAt],
58
+ finished_at: sym[:finishedAt],
59
+ raw: sym
60
+ )
61
+ end
62
+ end
63
+
64
+ class MonitorRunListResponse < T::Struct
65
+ const :data, T::Array[MonitorRun]
66
+ const :next_page_token, T.nilable(String)
67
+
68
+ def self.from_hash(hash)
69
+ sym = Helpers.symbolize_keys(hash)
70
+ new(
71
+ data: Array(sym[:data]).map { MonitorRun.from_hash(_1) },
72
+ next_page_token: sym[:nextPageToken]
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ class RawResponse < T::Struct
6
+ const :raw, T::Hash[Symbol, T.untyped]
7
+
8
+ def self.from_hash(hash)
9
+ sym = Helpers.symbolize_keys(hash)
10
+ new(raw: sym)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ class Research < T::Struct
6
+ const :id, String
7
+ const :model, T.nilable(String)
8
+ const :instructions, T.nilable(String)
9
+ const :status, T.nilable(String)
10
+ const :created_at, T.nilable(Integer)
11
+ const :events, T::Array[T.untyped]
12
+ const :operations, T::Array[T.untyped]
13
+ const :output, T.nilable(T::Hash[Symbol, T.untyped])
14
+ const :error, T.nilable(String)
15
+ const :raw, T::Hash[Symbol, T.untyped]
16
+
17
+ def self.from_hash(hash)
18
+ sym = Helpers.symbolize_keys(hash)
19
+ new(
20
+ id: sym[:researchId],
21
+ model: sym[:model],
22
+ instructions: sym[:instructions],
23
+ status: sym[:status],
24
+ created_at: sym[:createdAt]&.to_i,
25
+ events: Array(sym[:events]),
26
+ operations: Array(sym[:operations]),
27
+ output: normalize_hash(sym[:output]),
28
+ error: sym[:error],
29
+ raw: sym
30
+ )
31
+ end
32
+
33
+ def self.normalize_hash(value)
34
+ return nil if value.nil?
35
+ value.each_with_object({}) do |(k, v), acc|
36
+ acc[k.to_sym] = v
37
+ end
38
+ end
39
+ end
40
+
41
+ class ResearchListResponse < T::Struct
42
+ const :data, T::Array[Research]
43
+ const :has_more, T.nilable(T::Boolean)
44
+ const :next_cursor, T.nilable(String)
45
+
46
+ def self.from_hash(hash)
47
+ sym = Helpers.symbolize_keys(hash)
48
+ new(
49
+ data: Array(sym[:data]).map { Research.from_hash(_1) },
50
+ has_more: sym[:hasMore],
51
+ next_cursor: sym[:nextCursor]
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ module ResultProps
6
+ def self.included(base)
7
+ base.const :title, T.nilable(String)
8
+ base.const :url, T.nilable(String)
9
+ base.const :published_date, T.nilable(String)
10
+ base.const :author, T.nilable(String)
11
+ base.const :score, T.nilable(Float)
12
+ base.const :id, T.nilable(String)
13
+ base.const :image, T.nilable(String)
14
+ base.const :favicon, T.nilable(String)
15
+ end
16
+ end
17
+
18
+ class Result < T::Struct
19
+ include ResultProps
20
+
21
+ def self.from_hash(hash)
22
+ sym = Helpers.symbolize_keys(hash)
23
+ new(
24
+ title: sym[:title],
25
+ url: sym[:url],
26
+ published_date: sym[:publishedDate],
27
+ author: sym[:author],
28
+ score: sym[:score]&.to_f,
29
+ id: sym[:id],
30
+ image: sym[:image],
31
+ favicon: sym[:favicon]
32
+ )
33
+ end
34
+ end
35
+
36
+ class ResultWithContent < T::Struct
37
+ include ResultProps
38
+ const :text, T.nilable(String)
39
+ const :highlights, T.nilable(T::Array[String])
40
+ const :highlight_scores, T.nilable(T::Array[Float])
41
+ const :summary, T.nilable(String)
42
+ const :subpages, T.nilable(T::Array[Result])
43
+ const :extras, T.nilable(T::Hash[String, T.untyped])
44
+
45
+ def self.from_hash(hash)
46
+ sym = Helpers.symbolize_keys(hash)
47
+ base = Result.from_hash(sym)
48
+ attrs = base.serialize.transform_keys { _1.to_sym }
49
+ attrs = attrs.merge(
50
+ text: sym[:text],
51
+ highlights: sym[:highlights],
52
+ highlight_scores: sym[:highlightScores]&.map(&:to_f),
53
+ summary: sym[:summary],
54
+ subpages: sym[:subpages]&.map { ResultWithContent.from_hash(_1) },
55
+ extras: sym[:extras]
56
+ )
57
+ new(**attrs)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ class SearchResponse < T::Struct
6
+ const :request_id, T.nilable(String)
7
+ const :resolved_search_type, T.nilable(String)
8
+ const :search_type, T.nilable(String)
9
+ const :results, T::Array[ResultWithContent]
10
+ const :context, T.nilable(String)
11
+ const :cost_dollars, T.nilable(Float)
12
+
13
+ def self.from_hash(hash)
14
+ sym = Helpers.symbolize_keys(hash)
15
+ new(
16
+ request_id: sym[:requestId],
17
+ resolved_search_type: sym[:resolvedSearchType],
18
+ search_type: sym[:searchType],
19
+ results: Array(sym[:results]).map { ResultWithContent.from_hash(_1) },
20
+ context: sym[:context],
21
+ cost_dollars: sym[:costDollars]&.to_f
22
+ )
23
+ end
24
+ end
25
+
26
+ class FindSimilarResponse < T::Struct
27
+ const :request_id, T.nilable(String)
28
+ const :results, T::Array[ResultWithContent]
29
+ const :context, T.nilable(String)
30
+ const :cost_dollars, T.nilable(Float)
31
+
32
+ def self.from_hash(hash)
33
+ sym = Helpers.symbolize_keys(hash)
34
+ new(
35
+ request_id: sym[:requestId],
36
+ results: Array(sym[:results]).map { ResultWithContent.from_hash(_1) },
37
+ context: sym[:context],
38
+ cost_dollars: sym[:costDollars]&.to_f
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exa
4
+ module Responses
5
+ class Webhook < T::Struct
6
+ const :id, String
7
+ const :object, T.nilable(String)
8
+ const :status, T.nilable(String)
9
+ const :events, T::Array[String]
10
+ const :url, T.nilable(String)
11
+ const :secret, T.nilable(String)
12
+ const :metadata, T.nilable(T::Hash[String, String])
13
+ const :created_at, T.nilable(String)
14
+ const :updated_at, T.nilable(String)
15
+
16
+ def self.from_hash(hash)
17
+ sym = Helpers.symbolize_keys(hash)
18
+ new(
19
+ id: sym[:id],
20
+ object: sym[:object],
21
+ status: sym[:status],
22
+ events: Array(sym[:events]).map(&:to_s),
23
+ url: sym[:url],
24
+ secret: sym[:secret],
25
+ metadata: Helpers.stringify_string_hash(sym[:metadata]),
26
+ created_at: sym[:createdAt],
27
+ updated_at: sym[:updatedAt]
28
+ )
29
+ end
30
+ end
31
+
32
+ class WebhookListResponse < T::Struct
33
+ const :data, T::Array[Webhook]
34
+ const :has_more, T.nilable(T::Boolean)
35
+ const :next_cursor, T.nilable(String)
36
+
37
+ def self.from_hash(hash)
38
+ sym = Helpers.symbolize_keys(hash)
39
+ new(
40
+ data: Array(sym[:data]).map { Webhook.from_hash(_1) },
41
+ has_more: sym[:hasMore],
42
+ next_cursor: sym[:nextCursor]
43
+ )
44
+ end
45
+ end
46
+
47
+ class WebhookAttempt < T::Struct
48
+ const :id, String
49
+ const :object, T.nilable(String)
50
+ const :event_id, T.nilable(String)
51
+ const :event_type, T.nilable(String)
52
+ const :webhook_id, T.nilable(String)
53
+ const :url, T.nilable(String)
54
+ const :successful, T.nilable(T::Boolean)
55
+ const :response_headers, T.nilable(T::Hash[String, String])
56
+ const :response_body, T.nilable(String)
57
+ const :response_status_code, T.nilable(Integer)
58
+ const :attempt, T.nilable(Integer)
59
+ const :attempted_at, T.nilable(String)
60
+
61
+ def self.from_hash(hash)
62
+ sym = Helpers.symbolize_keys(hash)
63
+ new(
64
+ id: sym[:id],
65
+ object: sym[:object],
66
+ event_id: sym[:eventId],
67
+ event_type: sym[:eventType],
68
+ webhook_id: sym[:webhookId],
69
+ url: sym[:url],
70
+ successful: sym[:successful],
71
+ response_headers: Helpers.stringify_string_hash(sym[:responseHeaders]),
72
+ response_body: sym[:responseBody],
73
+ response_status_code: sym[:responseStatusCode]&.to_i,
74
+ attempt: sym[:attempt]&.to_i,
75
+ attempted_at: sym[:attemptedAt]
76
+ )
77
+ end
78
+ end
79
+
80
+ class WebhookAttemptListResponse < T::Struct
81
+ const :data, T::Array[WebhookAttempt]
82
+ const :has_more, T.nilable(T::Boolean)
83
+ const :next_cursor, T.nilable(String)
84
+
85
+ def self.from_hash(hash)
86
+ sym = Helpers.symbolize_keys(hash)
87
+ new(
88
+ data: Array(sym[:data]).map { WebhookAttempt.from_hash(_1) },
89
+ has_more: sym[:hasMore],
90
+ next_cursor: sym[:nextCursor]
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end