altertable-lakehouse 0.2.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/.github/workflows/ci.yml +40 -0
- data/.gitmodules +3 -0
- data/.rubocop.yml +58 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +196 -0
- data/Rakefile +6 -0
- data/lib/altertable/lakehouse/client.rb +218 -0
- data/lib/altertable/lakehouse/errors.rb +27 -0
- data/lib/altertable/lakehouse/models.rb +157 -0
- data/lib/altertable/lakehouse/version.rb +5 -0
- data/lib/altertable/lakehouse.rb +10 -0
- metadata +168 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8292ffff4927b361aa1a04dcc2352542e008a977d7ca5d3d91e5913532f490f5
|
|
4
|
+
data.tar.gz: e00935a9e22c975c3318b13f2cf05512b6c5082e0109975a01057287775c7897
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 443d6b8c28ff54b50dfc7fa52a8d9ac00a44a5398d92fc385ff60e8add97fc63e23be05a5d1b81674e8ffa568c0f8b4eecca80dfb6daf521c50e41af29d5c826
|
|
7
|
+
data.tar.gz: c6239d5ff43b91c17652401ef67c8258142df8db73eb6e852988ab6c404f8dae066c32a2a333033852b2f0b3a1535cb5e672bb9122b925fa0988c1f4291e37e7
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4', '4.0']
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
with:
|
|
19
|
+
submodules: recursive
|
|
20
|
+
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@v1
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
25
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
26
|
+
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: bundle exec rake spec
|
|
29
|
+
|
|
30
|
+
lint:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
- name: Set up Ruby
|
|
35
|
+
uses: ruby/setup-ruby@v1
|
|
36
|
+
with:
|
|
37
|
+
ruby-version: '3.4'
|
|
38
|
+
bundler-cache: true
|
|
39
|
+
- name: Run RuboCop
|
|
40
|
+
run: bundle exec rubocop
|
data/.gitmodules
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
AllCops:
|
|
3
|
+
NewCops: enable
|
|
4
|
+
TargetRubyVersion: 3.0
|
|
5
|
+
Exclude:
|
|
6
|
+
- 'vendor/**/*'
|
|
7
|
+
- 'spec/**/*'
|
|
8
|
+
- 'bin/**/*'
|
|
9
|
+
- 'gemfiles/**/*'
|
|
10
|
+
|
|
11
|
+
Style/Documentation:
|
|
12
|
+
Enabled: false
|
|
13
|
+
Style/FrozenStringLiteralComment:
|
|
14
|
+
Enabled: false
|
|
15
|
+
Style/StringLiterals:
|
|
16
|
+
Enabled: false
|
|
17
|
+
Layout/LineLength:
|
|
18
|
+
Enabled: false
|
|
19
|
+
Metrics/MethodLength:
|
|
20
|
+
Enabled: false
|
|
21
|
+
Metrics/ParameterLists:
|
|
22
|
+
Enabled: false
|
|
23
|
+
Metrics/AbcSize:
|
|
24
|
+
Enabled: false
|
|
25
|
+
Metrics/CyclomaticComplexity:
|
|
26
|
+
Enabled: false
|
|
27
|
+
Metrics/PerceivedComplexity:
|
|
28
|
+
Enabled: false
|
|
29
|
+
Layout/TrailingWhitespace:
|
|
30
|
+
Enabled: false
|
|
31
|
+
Metrics/ClassLength:
|
|
32
|
+
Enabled: false
|
|
33
|
+
Gemspec/RequireMFA:
|
|
34
|
+
Enabled: false
|
|
35
|
+
Gemspec/OrderedDependencies:
|
|
36
|
+
Enabled: false
|
|
37
|
+
Gemspec/DevelopmentDependencies:
|
|
38
|
+
Enabled: false
|
|
39
|
+
Style/IfUnlessModifier:
|
|
40
|
+
Enabled: false
|
|
41
|
+
Layout/EmptyLineAfterGuardClause:
|
|
42
|
+
Enabled: false
|
|
43
|
+
Layout/EmptyLinesAroundModuleBody:
|
|
44
|
+
Enabled: false
|
|
45
|
+
Lint/MissingSuper:
|
|
46
|
+
Enabled: false
|
|
47
|
+
Naming/MethodParameterName:
|
|
48
|
+
Enabled: false
|
|
49
|
+
Layout/IndentationWidth:
|
|
50
|
+
Enabled: false
|
|
51
|
+
Style/Proc:
|
|
52
|
+
Enabled: false
|
|
53
|
+
Style/FetchEnvVar:
|
|
54
|
+
Enabled: false
|
|
55
|
+
Style/MutableConstant:
|
|
56
|
+
Enabled: false
|
|
57
|
+
Bundler/OrderedGems:
|
|
58
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **BREAKING**: Removed per-request credential overrides from all client methods to comply with v0.3.0 specs. Authentication must now be configured at initialization time.
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-03-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **BREAKING**: Replaced Bearer token authentication with HTTP Basic Auth.
|
|
14
|
+
- **BREAKING**: Removed `api_key` argument from `Altertable::Lakehouse::Client.new`.
|
|
15
|
+
- Added support for `username`/`password` and `basic_auth_token` in client initialization.
|
|
16
|
+
- Added environment variable support for `ALTERTABLE_USERNAME`, `ALTERTABLE_PASSWORD`, and `ALTERTABLE_BASIC_AUTH_TOKEN`.
|
|
17
|
+
- Updated streaming query parsing to raise `ParseError` on malformed JSON lines instead of silently ignoring them.
|
|
18
|
+
|
|
19
|
+
## [0.1.0] - 2026-03-04
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Initial implementation of Altertable Lakehouse Ruby Client.
|
|
23
|
+
- Support for `append`, `query` (streamed/accumulated), `upload`, `validate`.
|
|
24
|
+
- Support for `get_query`, `cancel_query`.
|
|
25
|
+
- Typed request/response models.
|
|
26
|
+
- Faraday-based HTTP client with retries.
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Altertable AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Altertable Lakehouse Ruby Client
|
|
2
|
+
|
|
3
|
+
Official Ruby client for the [Altertable Lakehouse API](https://altertable.ai/docs).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'altertable-lakehouse'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And run:
|
|
14
|
+
|
|
15
|
+
$ bundle install
|
|
16
|
+
|
|
17
|
+
Or install it yourself as:
|
|
18
|
+
|
|
19
|
+
$ gem install altertable-lakehouse
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "altertable/lakehouse"
|
|
25
|
+
|
|
26
|
+
# Initialize with credentials
|
|
27
|
+
client = Altertable::Lakehouse::Client.new(
|
|
28
|
+
username: "your_username",
|
|
29
|
+
password: "your_password"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Append a row
|
|
33
|
+
client.append(
|
|
34
|
+
catalog: "main",
|
|
35
|
+
schema: "public",
|
|
36
|
+
table: "events",
|
|
37
|
+
payload: { user_id: 123, event: "signup", timestamp: Time.now.iso8601 }
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Query data
|
|
41
|
+
result = client.query_all(
|
|
42
|
+
statement: "SELECT * FROM main.public.events LIMIT 10"
|
|
43
|
+
)
|
|
44
|
+
result[:rows].each { |row| puts row }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API Reference
|
|
48
|
+
|
|
49
|
+
### Initialization
|
|
50
|
+
|
|
51
|
+
Supports Basic Authentication via username/password or pre-encoded token.
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# 1. Username/Password
|
|
55
|
+
client = Altertable::Lakehouse::Client.new(
|
|
56
|
+
username: "your_username",
|
|
57
|
+
password: "your_password",
|
|
58
|
+
base_url: "https://api.altertable.ai", # Optional
|
|
59
|
+
timeout: 10 # Optional
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 2. Pre-encoded Basic Auth Token
|
|
63
|
+
client = Altertable::Lakehouse::Client.new(
|
|
64
|
+
basic_auth_token: "dXNlcm5hbWU6cGFzc3dvcmQ=" # base64(username:password)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# 3. Environment Variables
|
|
68
|
+
# Set ALTERTABLE_USERNAME and ALTERTABLE_PASSWORD
|
|
69
|
+
# OR set ALTERTABLE_BASIC_AUTH_TOKEN
|
|
70
|
+
client = Altertable::Lakehouse::Client.new
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `append`
|
|
74
|
+
|
|
75
|
+
Appends one or more rows to a table.
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
# Single row
|
|
79
|
+
client.append(
|
|
80
|
+
catalog: "main",
|
|
81
|
+
schema: "public",
|
|
82
|
+
table: "events",
|
|
83
|
+
payload: { user_id: 123, event: "signup" }
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Batch append
|
|
87
|
+
client.append(
|
|
88
|
+
catalog: "main",
|
|
89
|
+
schema: "public",
|
|
90
|
+
table: "events",
|
|
91
|
+
payload: [
|
|
92
|
+
{ user_id: 123, event: "click" },
|
|
93
|
+
{ user_id: 456, event: "view" }
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Override credentials per-request
|
|
98
|
+
client.append(
|
|
99
|
+
catalog: "main",
|
|
100
|
+
schema: "public",
|
|
101
|
+
table: "events",
|
|
102
|
+
payload: { user_id: 789 },
|
|
103
|
+
username: "other_user",
|
|
104
|
+
password: "other_password"
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `query_all`
|
|
109
|
+
|
|
110
|
+
Executes a SQL query and returns all rows in memory (accumulated).
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
result = client.query_all(
|
|
114
|
+
statement: "SELECT * FROM main.public.events LIMIT 10"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
puts result[:metadata] # Hash
|
|
118
|
+
puts result[:columns] # Array of columns
|
|
119
|
+
result[:rows].each do |row|
|
|
120
|
+
puts row
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `query` (Streaming)
|
|
125
|
+
|
|
126
|
+
Executes a SQL query and streams rows efficiently for large result sets.
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
result = client.query(
|
|
130
|
+
statement: "SELECT * FROM main.public.events"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Note: metadata/columns are available after iteration starts
|
|
134
|
+
result.each do |row|
|
|
135
|
+
puts row
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `upload`
|
|
140
|
+
|
|
141
|
+
Uploads a file (CSV, Parquet, etc.) to a table.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
File.open("data.csv", "rb") do |file|
|
|
145
|
+
client.upload(
|
|
146
|
+
catalog: "main",
|
|
147
|
+
schema: "public",
|
|
148
|
+
table: "events",
|
|
149
|
+
format: "csv",
|
|
150
|
+
mode: "append",
|
|
151
|
+
file_io: file
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `get_query`
|
|
157
|
+
|
|
158
|
+
Retrieves information about a query execution.
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
info = client.get_query("query-uuid")
|
|
162
|
+
puts info.state # e.g. "RUNNING", "COMPLETED"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `cancel_query`
|
|
166
|
+
|
|
167
|
+
Cancels a running query.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
client.cancel_query("query-uuid", session_id: "session-123")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `validate`
|
|
174
|
+
|
|
175
|
+
Validates a SQL statement without executing it.
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
resp = client.validate(statement: "SELECT * FROM invalid_table")
|
|
179
|
+
puts resp.valid # => false
|
|
180
|
+
puts resp.error
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Configuration
|
|
184
|
+
|
|
185
|
+
| Option | Type | Default | Description |
|
|
186
|
+
| :--- | :--- | :--- | :--- |
|
|
187
|
+
| `username` | String | `ENV["ALTERTABLE_USERNAME"]` | Basic Auth username |
|
|
188
|
+
| `password` | String | `ENV["ALTERTABLE_PASSWORD"]` | Basic Auth password |
|
|
189
|
+
| `basic_auth_token` | String | `ENV["ALTERTABLE_BASIC_AUTH_TOKEN"]` | Pre-encoded Basic Auth token |
|
|
190
|
+
| `base_url` | String | `https://api.altertable.ai` | API base URL |
|
|
191
|
+
| `timeout` | Integer | `10` | Request timeout in seconds |
|
|
192
|
+
| `user_agent` | String | `nil` | Custom User-Agent suffix |
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/retry"
|
|
3
|
+
require "faraday/net_http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
6
|
+
require_relative "models"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
require_relative "version"
|
|
9
|
+
|
|
10
|
+
module Altertable
|
|
11
|
+
module Lakehouse
|
|
12
|
+
class Client
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.altertable.ai"
|
|
14
|
+
DEFAULT_TIMEOUT = 10
|
|
15
|
+
|
|
16
|
+
def initialize(username: nil, password: nil, basic_auth_token: nil, base_url: nil, timeout: nil, user_agent: nil)
|
|
17
|
+
# 1. Try passed basic_auth_token
|
|
18
|
+
# 2. Try passed username/password
|
|
19
|
+
# 3. Try ENV["ALTERTABLE_BASIC_AUTH_TOKEN"]
|
|
20
|
+
# 4. Try ENV["ALTERTABLE_USERNAME"] / ENV["ALTERTABLE_PASSWORD"]
|
|
21
|
+
|
|
22
|
+
if basic_auth_token
|
|
23
|
+
@auth_header = "Basic #{basic_auth_token}"
|
|
24
|
+
elsif username && password
|
|
25
|
+
@auth_header = "Basic #{Base64.strict_encode64("#{username}:#{password}")}"
|
|
26
|
+
elsif (env_token = ENV["ALTERTABLE_BASIC_AUTH_TOKEN"])
|
|
27
|
+
@auth_header = "Basic #{env_token}"
|
|
28
|
+
elsif (env_user = ENV["ALTERTABLE_USERNAME"]) && (env_pass = ENV["ALTERTABLE_PASSWORD"])
|
|
29
|
+
@auth_header = "Basic #{Base64.strict_encode64("#{env_user}:#{env_pass}")}"
|
|
30
|
+
else
|
|
31
|
+
raise ConfigurationError, "Authentication credentials required (username/password or basic_auth_token)"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@base_url = base_url || DEFAULT_BASE_URL
|
|
35
|
+
@timeout = timeout || DEFAULT_TIMEOUT
|
|
36
|
+
@user_agent = user_agent ? "AltertableRuby/#{VERSION} #{user_agent}" : "AltertableRuby/#{VERSION}"
|
|
37
|
+
|
|
38
|
+
@conn = Faraday.new(url: @base_url) do |f|
|
|
39
|
+
f.headers["Authorization"] = @auth_header
|
|
40
|
+
f.headers["User-Agent"] = @user_agent
|
|
41
|
+
f.headers["Content-Type"] = "application/json"
|
|
42
|
+
f.options.timeout = @timeout
|
|
43
|
+
f.request :retry, max: 3, interval: 0.05, backoff_factor: 2
|
|
44
|
+
f.adapter Faraday.default_adapter
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# POST /append
|
|
49
|
+
def append(catalog:, schema:, table:, payload:)
|
|
50
|
+
params = { catalog: catalog, schema: schema, table: table }
|
|
51
|
+
req = Models::AppendRequest.new(payload)
|
|
52
|
+
resp = request(:post, "/append", body: req.to_h, query: params)
|
|
53
|
+
Models::AppendResponse.from_h(resp)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# POST /query (streamed)
|
|
57
|
+
def query(statement:, **options)
|
|
58
|
+
req_body = Models::QueryRequest.new(statement: statement, **options).to_h.to_json
|
|
59
|
+
|
|
60
|
+
enum = Enumerator.new do |yielder|
|
|
61
|
+
buffer = ""
|
|
62
|
+
@conn.post("/query") do |req|
|
|
63
|
+
req.headers["Content-Type"] = "application/json"
|
|
64
|
+
req.body = req_body
|
|
65
|
+
req.options.on_data = Proc.new do |chunk, _|
|
|
66
|
+
buffer << chunk
|
|
67
|
+
while (line_end = buffer.index("\n"))
|
|
68
|
+
line = buffer.slice!(0, line_end + 1).strip
|
|
69
|
+
next if line.empty?
|
|
70
|
+
begin
|
|
71
|
+
yielder << JSON.parse(line)
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
raise ParseError, "Invalid JSON line: #{line}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Process remaining buffer
|
|
80
|
+
unless buffer.empty?
|
|
81
|
+
begin
|
|
82
|
+
yielder << JSON.parse(buffer.strip)
|
|
83
|
+
rescue JSON::ParserError
|
|
84
|
+
raise ParseError, "Invalid JSON line: #{buffer}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
QueryResult.new(enum)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# POST /query (accumulated)
|
|
93
|
+
def query_all(statement:, **options)
|
|
94
|
+
result = query(statement: statement, **options)
|
|
95
|
+
rows = result.to_a # Accumulate
|
|
96
|
+
{
|
|
97
|
+
metadata: result.metadata,
|
|
98
|
+
columns: result.columns,
|
|
99
|
+
rows: rows
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# POST /upload
|
|
104
|
+
def upload(catalog:, schema:, table:, format:, mode:, file_io:, primary_key: nil)
|
|
105
|
+
params = {
|
|
106
|
+
catalog: catalog,
|
|
107
|
+
schema: schema,
|
|
108
|
+
table: table,
|
|
109
|
+
format: format,
|
|
110
|
+
mode: mode
|
|
111
|
+
}
|
|
112
|
+
params[:primary_key] = primary_key if primary_key
|
|
113
|
+
|
|
114
|
+
# Use a separate connection for multipart/binary if needed,
|
|
115
|
+
# but spec says body is octet-stream.
|
|
116
|
+
resp = @conn.post("/upload") do |req|
|
|
117
|
+
req.params = params
|
|
118
|
+
req.headers["Content-Type"] = "application/octet-stream"
|
|
119
|
+
req.body = file_io # IO object or string
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
handle_response(resp)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# GET /query/:query_id
|
|
126
|
+
def get_query(query_id)
|
|
127
|
+
resp = request(:get, "/query/#{query_id}")
|
|
128
|
+
Models::QueryLogResponse.from_h(resp)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# DELETE /query/:query_id
|
|
132
|
+
def cancel_query(query_id, session_id:)
|
|
133
|
+
resp = request(:delete, "/query/#{query_id}", query: { session_id: session_id })
|
|
134
|
+
Models::CancelQueryResponse.from_h(resp)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# POST /validate
|
|
138
|
+
def validate(statement:)
|
|
139
|
+
req = Models::ValidateRequest.new(statement: statement)
|
|
140
|
+
resp = request(:post, "/validate", body: req.to_h)
|
|
141
|
+
Models::ValidateResponse.from_h(resp)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def request(method, path, body: nil, query: nil, stream: false, &block)
|
|
147
|
+
resp = @conn.send(method, path) do |req|
|
|
148
|
+
req.params = query if query
|
|
149
|
+
req.body = body.to_json if body
|
|
150
|
+
if stream
|
|
151
|
+
req.options.on_data = block
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
return if stream # Block handles data
|
|
156
|
+
|
|
157
|
+
handle_response(resp)
|
|
158
|
+
rescue Faraday::ConnectionFailed => e
|
|
159
|
+
raise NetworkError, e.message
|
|
160
|
+
rescue Faraday::TimeoutError => e
|
|
161
|
+
raise TimeoutError, e.message
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def handle_response(resp)
|
|
165
|
+
case resp.status
|
|
166
|
+
when 200..299
|
|
167
|
+
return nil if resp.body.nil? || resp.body.empty?
|
|
168
|
+
begin
|
|
169
|
+
JSON.parse(resp.body)
|
|
170
|
+
rescue JSON::ParserError
|
|
171
|
+
# For non-JSON responses (like empty upload response?)
|
|
172
|
+
resp.body
|
|
173
|
+
end
|
|
174
|
+
when 400
|
|
175
|
+
raise BadRequestError, "Bad Request: #{resp.body}"
|
|
176
|
+
when 401
|
|
177
|
+
raise AuthError, "Unauthorized"
|
|
178
|
+
when 404
|
|
179
|
+
raise ApiError, "Not Found: #{resp.url}" # Could be specific
|
|
180
|
+
else
|
|
181
|
+
raise ApiError, "API Error #{resp.status}: #{resp.body}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class QueryResult
|
|
187
|
+
include Enumerable
|
|
188
|
+
|
|
189
|
+
attr_reader :metadata, :columns
|
|
190
|
+
|
|
191
|
+
def initialize(enum)
|
|
192
|
+
@enum = enum
|
|
193
|
+
@metadata = nil
|
|
194
|
+
@columns = nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def each(&block)
|
|
198
|
+
# We need to wrap the enum to extract metadata/columns first
|
|
199
|
+
# Note: This will re-trigger the request if enumerated multiple times
|
|
200
|
+
first = true
|
|
201
|
+
second = true
|
|
202
|
+
|
|
203
|
+
@enum.each do |item|
|
|
204
|
+
if first
|
|
205
|
+
@metadata = item
|
|
206
|
+
first = false
|
|
207
|
+
elsif second
|
|
208
|
+
@columns = item
|
|
209
|
+
second = false
|
|
210
|
+
else
|
|
211
|
+
block.call(item)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Altertable
|
|
2
|
+
module Lakehouse
|
|
3
|
+
class Error < StandardError
|
|
4
|
+
attr_reader :operation, :http_method, :http_path, :status_code, :retriable, :request_id, :cause
|
|
5
|
+
|
|
6
|
+
def initialize(message, operation: nil, http_method: nil, http_path: nil, status_code: nil, retriable: false, request_id: nil, cause: nil)
|
|
7
|
+
super(message)
|
|
8
|
+
@operation = operation
|
|
9
|
+
@http_method = http_method
|
|
10
|
+
@http_path = http_path
|
|
11
|
+
@status_code = status_code
|
|
12
|
+
@retriable = retriable
|
|
13
|
+
@request_id = request_id
|
|
14
|
+
@cause = cause
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class AuthError < Error; end
|
|
19
|
+
class BadRequestError < Error; end
|
|
20
|
+
class NetworkError < Error; end
|
|
21
|
+
class TimeoutError < Error; end
|
|
22
|
+
class SerializationError < Error; end
|
|
23
|
+
class ParseError < Error; end
|
|
24
|
+
class ApiError < Error; end
|
|
25
|
+
class ConfigurationError < Error; end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
module Altertable
|
|
2
|
+
module Lakehouse
|
|
3
|
+
module Models
|
|
4
|
+
class Request
|
|
5
|
+
def to_h
|
|
6
|
+
raise NotImplementedError
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class AppendRequest < Request
|
|
11
|
+
attr_reader :payload
|
|
12
|
+
|
|
13
|
+
# Payload can be a single Hash or an Array of Hashes
|
|
14
|
+
def initialize(payload)
|
|
15
|
+
@payload = payload
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
@payload
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class AppendResponse < Request
|
|
24
|
+
attr_reader :ok, :error_code
|
|
25
|
+
|
|
26
|
+
def initialize(ok:, error_code: nil)
|
|
27
|
+
@ok = ok
|
|
28
|
+
@error_code = error_code
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.from_h(h)
|
|
32
|
+
new(ok: h["ok"], error_code: h["error_code"])
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class QueryRequest < Request
|
|
37
|
+
attr_reader :statement, :catalog, :schema, :session_id, :compute_size, :sanitize, :limit, :offset, :timezone, :ephemeral, :visible, :requested_by, :query_id
|
|
38
|
+
|
|
39
|
+
def initialize(statement:, catalog: nil, schema: nil, session_id: nil, compute_size: nil, sanitize: nil, limit: nil, offset: nil, timezone: nil, ephemeral: nil, visible: nil, requested_by: nil, query_id: nil)
|
|
40
|
+
@statement = statement
|
|
41
|
+
@catalog = catalog
|
|
42
|
+
@schema = schema
|
|
43
|
+
@session_id = session_id
|
|
44
|
+
@compute_size = compute_size
|
|
45
|
+
@sanitize = sanitize
|
|
46
|
+
@limit = limit
|
|
47
|
+
@offset = offset
|
|
48
|
+
@timezone = timezone
|
|
49
|
+
@ephemeral = ephemeral
|
|
50
|
+
@visible = visible
|
|
51
|
+
@requested_by = requested_by
|
|
52
|
+
@query_id = query_id
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_h
|
|
56
|
+
h = { statement: @statement }
|
|
57
|
+
h[:catalog] = @catalog if @catalog
|
|
58
|
+
h[:schema] = @schema if @schema
|
|
59
|
+
h[:session_id] = @session_id if @session_id
|
|
60
|
+
h[:compute_size] = @compute_size if @compute_size
|
|
61
|
+
h[:sanitize] = @sanitize unless @sanitize.nil?
|
|
62
|
+
h[:limit] = @limit if @limit
|
|
63
|
+
h[:offset] = @offset if @offset
|
|
64
|
+
h[:timezone] = @timezone if @timezone
|
|
65
|
+
h[:ephemeral] = @ephemeral unless @ephemeral.nil?
|
|
66
|
+
h[:visible] = @visible unless @visible.nil?
|
|
67
|
+
h[:requested_by] = @requested_by if @requested_by
|
|
68
|
+
h[:query_id] = @query_id if @query_id
|
|
69
|
+
h
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class ValidateRequest < Request
|
|
74
|
+
attr_reader :statement
|
|
75
|
+
|
|
76
|
+
def initialize(statement:)
|
|
77
|
+
@statement = statement
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_h
|
|
81
|
+
{ statement: @statement }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class ValidateResponse < Request
|
|
86
|
+
attr_reader :valid, :statement, :connections_errors, :error
|
|
87
|
+
|
|
88
|
+
def initialize(valid:, statement:, connections_errors: nil, error: nil)
|
|
89
|
+
@valid = valid
|
|
90
|
+
@statement = statement
|
|
91
|
+
@connections_errors = connections_errors
|
|
92
|
+
@error = error
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.from_h(h)
|
|
96
|
+
new(
|
|
97
|
+
valid: h["valid"],
|
|
98
|
+
statement: h["statement"],
|
|
99
|
+
connections_errors: h["connections_errors"],
|
|
100
|
+
error: h["error"]
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class QueryLogResponse < Request
|
|
106
|
+
attr_reader :uuid, :start_time, :end_time, :duration_ms, :query, :session_id, :client_interface, :error, :stats, :progress, :visible, :requested_by, :user_agent
|
|
107
|
+
|
|
108
|
+
def initialize(uuid:, start_time:, end_time:, duration_ms:, query:, session_id:, client_interface:, error:, stats:, progress:, visible:, requested_by:, user_agent:)
|
|
109
|
+
@uuid = uuid
|
|
110
|
+
@start_time = start_time
|
|
111
|
+
@end_time = end_time
|
|
112
|
+
@duration_ms = duration_ms
|
|
113
|
+
@query = query
|
|
114
|
+
@session_id = session_id
|
|
115
|
+
@client_interface = client_interface
|
|
116
|
+
@error = error
|
|
117
|
+
@stats = stats
|
|
118
|
+
@progress = progress
|
|
119
|
+
@visible = visible
|
|
120
|
+
@requested_by = requested_by
|
|
121
|
+
@user_agent = user_agent
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.from_h(h)
|
|
125
|
+
new(
|
|
126
|
+
uuid: h["uuid"],
|
|
127
|
+
start_time: h["start_time"],
|
|
128
|
+
end_time: h["end_time"],
|
|
129
|
+
duration_ms: h["duration_ms"],
|
|
130
|
+
query: h["query"],
|
|
131
|
+
session_id: h["session_id"],
|
|
132
|
+
client_interface: h["client_interface"],
|
|
133
|
+
error: h["error"],
|
|
134
|
+
stats: h["stats"],
|
|
135
|
+
progress: h["progress"],
|
|
136
|
+
visible: h["visible"],
|
|
137
|
+
requested_by: h["requested_by"],
|
|
138
|
+
user_agent: h["user_agent"]
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
class CancelQueryResponse < Request
|
|
144
|
+
attr_reader :cancelled, :message
|
|
145
|
+
|
|
146
|
+
def initialize(cancelled:, message:)
|
|
147
|
+
@cancelled = cancelled
|
|
148
|
+
@message = message
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.from_h(h)
|
|
152
|
+
new(cancelled: h["cancelled"], message: h["message"])
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: altertable-lakehouse
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Altertable AI
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.12'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.12'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-retry
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday-net_http
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: base64
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '13.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '13.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: rspec
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.0'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: webmock
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '3.18'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.18'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: rubocop
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '1.50'
|
|
117
|
+
type: :development
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '1.50'
|
|
124
|
+
description: Official Ruby client for Altertable Lakehouse API.
|
|
125
|
+
email:
|
|
126
|
+
- support@altertable.ai
|
|
127
|
+
executables: []
|
|
128
|
+
extensions: []
|
|
129
|
+
extra_rdoc_files: []
|
|
130
|
+
files:
|
|
131
|
+
- ".github/workflows/ci.yml"
|
|
132
|
+
- ".gitmodules"
|
|
133
|
+
- ".rubocop.yml"
|
|
134
|
+
- CHANGELOG.md
|
|
135
|
+
- Gemfile
|
|
136
|
+
- LICENSE.txt
|
|
137
|
+
- README.md
|
|
138
|
+
- Rakefile
|
|
139
|
+
- lib/altertable/lakehouse.rb
|
|
140
|
+
- lib/altertable/lakehouse/client.rb
|
|
141
|
+
- lib/altertable/lakehouse/errors.rb
|
|
142
|
+
- lib/altertable/lakehouse/models.rb
|
|
143
|
+
- lib/altertable/lakehouse/version.rb
|
|
144
|
+
homepage: https://github.com/altertable-ai/altertable-lakehouse-ruby
|
|
145
|
+
licenses:
|
|
146
|
+
- MIT
|
|
147
|
+
metadata:
|
|
148
|
+
homepage_uri: https://github.com/altertable-ai/altertable-lakehouse-ruby
|
|
149
|
+
source_code_uri: https://github.com/altertable-ai/altertable-lakehouse-ruby
|
|
150
|
+
changelog_uri: https://github.com/altertable-ai/altertable-lakehouse-ruby/blob/main/CHANGELOG.md
|
|
151
|
+
rdoc_options: []
|
|
152
|
+
require_paths:
|
|
153
|
+
- lib
|
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - ">="
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: 3.0.0
|
|
159
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
160
|
+
requirements:
|
|
161
|
+
- - ">="
|
|
162
|
+
- !ruby/object:Gem::Version
|
|
163
|
+
version: '0'
|
|
164
|
+
requirements: []
|
|
165
|
+
rubygems_version: 3.6.7
|
|
166
|
+
specification_version: 4
|
|
167
|
+
summary: Official Ruby client for Altertable Lakehouse
|
|
168
|
+
test_files: []
|