pinot 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/Guardfile +42 -0
- data/README.md +58 -11
- data/lib/pinot/client.rb +92 -0
- data/lib/pinot/response.rb +32 -0
- data/lib/pinot/version.rb +1 -1
- data/lib/pinot.rb +4 -0
- metadata +63 -5
- data/pinot.gemspec +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dc12c931af8cd247929cbab2185c0ccd69a7d241e750211ba95287af996e3914
|
4
|
+
data.tar.gz: 3682d27b5db1c615f1084af77ef1d64496859a6db92a21a4bdf4d45761f8b741
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9a4464727f662abe8c7cace2952076e45ea08802d1380a09568b9c60f4d2b58b6f8bd501f29216e59525a2d54f131bd66d19de12dbb7a5880780dc1bab06831
|
7
|
+
data.tar.gz: 890d0696eb36c2059873a9d8330f584fdd3b6ff13870567f5eee7d69a7fd286f895b9143f6ecc387a150fce460f27e3c7db4b063fb1500d9d849a7403d3c3468
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.1.2] - 2024-05-29
|
4
|
+
|
5
|
+
- feature: support socks5
|
6
|
+
- feature: support authentication bearer token
|
7
|
+
|
8
|
+
## [0.1.1] - 2024-05-16
|
9
|
+
|
10
|
+
- feature: Add support for controller host
|
11
|
+
- fix: handle nil column names
|
12
|
+
|
3
13
|
## [0.1.0] - 2024-01-16
|
4
14
|
|
5
15
|
- Initial release
|
data/Guardfile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
guard :minitest do
|
19
|
+
# with Minitest::Unit
|
20
|
+
watch(%r{^test/(.*)/?test_(.*)\.rb$})
|
21
|
+
watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
|
22
|
+
watch(%r{^test/test_helper\.rb$}) { "test" }
|
23
|
+
|
24
|
+
# with Minitest::Spec
|
25
|
+
# watch(%r{^spec/(.*)_spec\.rb$})
|
26
|
+
# watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
27
|
+
# watch(%r{^spec/spec_helper\.rb$}) { 'spec' }
|
28
|
+
|
29
|
+
# Rails 4
|
30
|
+
# watch(%r{^app/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
|
31
|
+
# watch(%r{^app/controllers/application_controller\.rb$}) { 'test/controllers' }
|
32
|
+
# watch(%r{^app/controllers/(.+)_controller\.rb$}) { |m| "test/integration/#{m[1]}_test.rb" }
|
33
|
+
# watch(%r{^app/views/(.+)_mailer/.+}) { |m| "test/mailers/#{m[1]}_mailer_test.rb" }
|
34
|
+
# watch(%r{^lib/(.+)\.rb$}) { |m| "test/lib/#{m[1]}_test.rb" }
|
35
|
+
# watch(%r{^test/.+_test\.rb$})
|
36
|
+
# watch(%r{^test/test_helper\.rb$}) { 'test' }
|
37
|
+
|
38
|
+
# Rails < 4
|
39
|
+
# watch(%r{^app/controllers/(.*)\.rb$}) { |m| "test/functional/#{m[1]}_test.rb" }
|
40
|
+
# watch(%r{^app/helpers/(.*)\.rb$}) { |m| "test/helpers/#{m[1]}_test.rb" }
|
41
|
+
# watch(%r{^app/models/(.*)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
|
42
|
+
end
|
data/README.md
CHANGED
@@ -1,24 +1,71 @@
|
|
1
1
|
# Pinot
|
2
2
|
|
3
|
-
TODO: Delete this and the text below, and describe your gem
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pinot`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
-
|
7
3
|
## Installation
|
8
4
|
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
5
|
Install the gem and add to the application's Gemfile by executing:
|
12
6
|
|
13
|
-
$ bundle add
|
7
|
+
$ bundle add pinot
|
14
8
|
|
15
9
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
10
|
|
17
|
-
$ gem install
|
11
|
+
$ gem install pinot
|
18
12
|
|
19
13
|
## Usage
|
20
14
|
|
21
|
-
|
15
|
+
To configure our client, we must have some data about our pinot cluster, its broken and controller host, and which port/protocol both services are using. After having this information, we can setup the client to use it.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
host = "localhost"
|
19
|
+
controller_host = "localhost"
|
20
|
+
client = Pinot::Client.new(host: host, port: 443, protocol: :https, controller_host: controller_host, controller_port: 443)
|
21
|
+
```
|
22
|
+
|
23
|
+
After setting up your client, we can start using it to query the cluster.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
client.execute("select * from table;")
|
27
|
+
```
|
28
|
+
|
29
|
+
After querying, it returns a `HTTPX::ErrorResponse` in case of any HTTP error, like a timeout, or connection refused, we are returning the client's error to make easier to troubleshoot any problem and handle it accordingly.
|
30
|
+
|
31
|
+
In case of success, it returns a `Pinot::Response`, an enumerable where you can interact over the returned rows.
|
32
|
+
|
33
|
+
Each row is an array with row's values.
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
result = client.execute("select id, name, age from people;")
|
37
|
+
result.each do |row|
|
38
|
+
puts row[0] # id
|
39
|
+
puts row[1] # name
|
40
|
+
puts row[2] # age
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
If the column names and types are needed, can call `columns` method on the response.
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
result = client.execute("select count(*) from people;")
|
48
|
+
resp.columns
|
49
|
+
# {"count(*)"=>"LONG"}
|
50
|
+
```
|
51
|
+
|
52
|
+
It's not on the public API, but the instance variable `@payload` contains all information returned from the Pinot cluster in case can help with troubleshooting
|
53
|
+
|
54
|
+
### Authentication
|
55
|
+
|
56
|
+
In case your pinot cluster is using authentication bearer token, you can specify it on the client constructor
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
client = Pinot::Client.new(bearer_token: "my-secret", **client_args)
|
60
|
+
```
|
61
|
+
|
62
|
+
### Socks5
|
63
|
+
|
64
|
+
Using Pinot cluster inside a VPC and need to jump through a bastion host using socks5? We got your back!
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
client = Pinot::Client.new(socks5_uri: "socks5://localhost:80", **client_args)
|
68
|
+
```
|
22
69
|
|
23
70
|
## Development
|
24
71
|
|
@@ -28,8 +75,8 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
28
75
|
|
29
76
|
## Contributing
|
30
77
|
|
31
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
78
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/clickfunnels2/pinot. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/clickfunnels2/pinot/blob/main/CODE_OF_CONDUCT.md).
|
32
79
|
|
33
80
|
## Code of Conduct
|
34
81
|
|
35
|
-
Everyone interacting in the Pinot project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
82
|
+
Everyone interacting in the Pinot project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/clickfunnels2/pinot/blob/main/CODE_OF_CONDUCT.md).
|
data/lib/pinot/client.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
module Pinot
|
2
|
+
class Client
|
3
|
+
attr_reader :host, :port, :controller_host, :controller_port, :protocol, :socks5_uri, :bearer_token
|
4
|
+
|
5
|
+
def initialize(host:, port:, controller_port:, controller_host: nil, protocol: :http, socks5_uri: nil, bearer_token: nil)
|
6
|
+
@host = host
|
7
|
+
@port = port
|
8
|
+
@controller_port = controller_port
|
9
|
+
@controller_host = controller_host || host
|
10
|
+
@protocol = protocol
|
11
|
+
@socks5_uri = socks5_uri
|
12
|
+
@bearer_token = bearer_token
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(sql)
|
16
|
+
response = http.post(query_sql_uri, json: {sql: sql})
|
17
|
+
return response if response.is_a?(HTTPX::ErrorResponse)
|
18
|
+
Response.new(JSON.parse(response))
|
19
|
+
end
|
20
|
+
|
21
|
+
def schema(name)
|
22
|
+
url = "#{controller_uri}/schemas/#{name}"
|
23
|
+
response = http.get(url)
|
24
|
+
JSON.parse(response)
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete_segments(name, type: :offline)
|
28
|
+
type = type.to_s.upcase
|
29
|
+
url = "#{controller_uri}/segments/#{name}?type=#{type}"
|
30
|
+
response = http.delete(url)
|
31
|
+
JSON.parse(response)
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_table(schema)
|
35
|
+
url = "#{controller_uri}/tables"
|
36
|
+
response = http.post(url, body: schema)
|
37
|
+
JSON.parse(response)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete_table(name, type: :offline)
|
41
|
+
type = type.to_s.downcase
|
42
|
+
url = "#{controller_uri}/tables/#{name}?type=#{type}"
|
43
|
+
response = http.delete(url)
|
44
|
+
JSON.parse(response)
|
45
|
+
end
|
46
|
+
|
47
|
+
def ingest_json(file, table:)
|
48
|
+
url = "#{controller_uri}/ingestFromFile?tableNameWithType=#{table}&batchConfigMapStr=%7B%22inputFormat%22%3A%22json%22%7D"
|
49
|
+
content_type = "multipart/form-data"
|
50
|
+
response = HTTPX.post(
|
51
|
+
url,
|
52
|
+
form: {
|
53
|
+
file: {
|
54
|
+
filename: File.basename(file.path),
|
55
|
+
content_type: content_type,
|
56
|
+
body: file.read
|
57
|
+
}
|
58
|
+
}
|
59
|
+
)
|
60
|
+
JSON.parse(response)
|
61
|
+
end
|
62
|
+
|
63
|
+
def create_schema(schema, override: true, force: false)
|
64
|
+
url = "#{controller_uri}/schemas?override=#{override}&force=#{force}"
|
65
|
+
response = http.post(url, body: schema)
|
66
|
+
JSON.parse(response)
|
67
|
+
end
|
68
|
+
|
69
|
+
def http(content_type: "application/json")
|
70
|
+
return @http if !@http.nil?
|
71
|
+
default_headers = {"Content-Type" => content_type}
|
72
|
+
default_headers["Authorization"] = "Bearer #{bearer_token}" if bearer_token
|
73
|
+
@http = HTTPX.with(headers: default_headers, timeout: { connect_timeout: 5 })
|
74
|
+
if socks5_uri
|
75
|
+
@http = @http.plugin(:proxy).with_proxy(uri: socks5_uri) if socks5_uri
|
76
|
+
end
|
77
|
+
@http
|
78
|
+
end
|
79
|
+
|
80
|
+
def uri
|
81
|
+
"#{protocol}://#{host}:#{port}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def controller_uri
|
85
|
+
"#{protocol}://#{controller_host}:#{controller_port}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def query_sql_uri
|
89
|
+
"#{uri}/query/sql"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Pinot
|
2
|
+
class Response
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(payload)
|
6
|
+
@payload = payload
|
7
|
+
end
|
8
|
+
|
9
|
+
def rows
|
10
|
+
@payload.dig("resultTable", "rows")
|
11
|
+
end
|
12
|
+
|
13
|
+
def columns
|
14
|
+
names = @payload.dig("resultTable", "dataSchema", "columnNames")
|
15
|
+
types = @payload.dig("resultTable", "dataSchema", "columnDataTypes")
|
16
|
+
return {} if @payload["exceptions"].any?
|
17
|
+
ix = 0
|
18
|
+
names ||= []
|
19
|
+
names.map do |name|
|
20
|
+
ret = [name, types[ix]]
|
21
|
+
ix += 1
|
22
|
+
ret
|
23
|
+
end.to_h
|
24
|
+
end
|
25
|
+
|
26
|
+
def each(&block)
|
27
|
+
rows.each do |row|
|
28
|
+
block.call(row)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/pinot/version.rb
CHANGED
data/lib/pinot.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,71 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pinot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Celso Fernandes
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
12
|
-
dependencies:
|
11
|
+
date: 2024-05-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: httpx
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: guard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: guard-minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-reporters
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
13
69
|
description: Client for Apache Pinot
|
14
70
|
email:
|
15
71
|
- celso.fernandes@clickfunnels.com
|
@@ -20,11 +76,13 @@ files:
|
|
20
76
|
- ".standard.yml"
|
21
77
|
- CHANGELOG.md
|
22
78
|
- CODE_OF_CONDUCT.md
|
79
|
+
- Guardfile
|
23
80
|
- README.md
|
24
81
|
- Rakefile
|
25
82
|
- lib/pinot.rb
|
83
|
+
- lib/pinot/client.rb
|
84
|
+
- lib/pinot/response.rb
|
26
85
|
- lib/pinot/version.rb
|
27
|
-
- pinot.gemspec
|
28
86
|
- sig/pinot.rbs
|
29
87
|
homepage: https://github.com/fernandes/pinot-ruby
|
30
88
|
licenses: []
|
@@ -47,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
105
|
- !ruby/object:Gem::Version
|
48
106
|
version: '0'
|
49
107
|
requirements: []
|
50
|
-
rubygems_version: 3.
|
108
|
+
rubygems_version: 3.5.3
|
51
109
|
signing_key:
|
52
110
|
specification_version: 4
|
53
111
|
summary: Client for Apache Pinot
|
data/pinot.gemspec
DELETED
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative "lib/pinot/version"
|
4
|
-
|
5
|
-
Gem::Specification.new do |spec|
|
6
|
-
spec.name = "pinot"
|
7
|
-
spec.version = Pinot::VERSION
|
8
|
-
spec.authors = ["Celso Fernandes"]
|
9
|
-
spec.email = ["celso.fernandes@clickfunnels.com"]
|
10
|
-
|
11
|
-
spec.summary = "Client for Apache Pinot"
|
12
|
-
spec.description = "Client for Apache Pinot"
|
13
|
-
spec.homepage = "https://github.com/fernandes/pinot-ruby"
|
14
|
-
spec.required_ruby_version = ">= 2.6.0"
|
15
|
-
|
16
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
-
spec.metadata["source_code_uri"] = "https://github.com/fernandes/pinot"
|
18
|
-
spec.metadata["changelog_uri"] = "https://github.com/fernandes/pinot/blob/main/CHANGELOG.md"
|
19
|
-
|
20
|
-
# Specify which files should be added to the gem when it is released.
|
21
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
-
spec.files = Dir.chdir(__dir__) do
|
23
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
24
|
-
(File.expand_path(f) == __FILE__) ||
|
25
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
26
|
-
end
|
27
|
-
end
|
28
|
-
spec.bindir = "exe"
|
29
|
-
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
-
spec.require_paths = ["lib"]
|
31
|
-
|
32
|
-
# Uncomment to register a new dependency of your gem
|
33
|
-
# spec.add_dependency "example-gem", "~> 1.0"
|
34
|
-
|
35
|
-
# For more information and examples about making a new gem, check out our
|
36
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
37
|
-
end
|