strata-cli 0.1.7 → 0.1.9
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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +24 -1
- data/lib/strata/cli/ai/services/table_generator.rb +35 -20
- data/lib/strata/cli/api/client.rb +23 -63
- data/lib/strata/cli/api/response_error_handler.rb +115 -0
- data/lib/strata/cli/credentials.rb +5 -9
- data/lib/strata/cli/error_reporter.rb +4 -1
- data/lib/strata/cli/generators/datasource.rb +4 -3
- data/lib/strata/cli/generators/group.rb +37 -0
- data/lib/strata/cli/generators/migration.rb +2 -1
- data/lib/strata/cli/generators/project.rb +18 -11
- data/lib/strata/cli/generators/relation.rb +2 -1
- data/lib/strata/cli/generators/table.rb +5 -8
- data/lib/strata/cli/generators/templates/adapters/databricks.yml +4 -3
- data/lib/strata/cli/generators/templates/adapters/snowflake.yml +1 -21
- data/lib/strata/cli/generators/templates/table.table_name.yml +1 -1
- data/lib/strata/cli/generators/test.rb +2 -1
- data/lib/strata/cli/guard.rb +4 -1
- data/lib/strata/cli/helpers/command_context.rb +8 -9
- data/lib/strata/cli/helpers/datasource_helper.rb +1 -1
- data/lib/strata/cli/helpers/description_helper.rb +2 -1
- data/lib/strata/cli/main.rb +15 -3
- data/lib/strata/cli/output.rb +103 -0
- data/lib/strata/cli/sub_commands/audit.rb +4 -3
- data/lib/strata/cli/sub_commands/branch.rb +163 -0
- data/lib/strata/cli/sub_commands/create.rb +1 -2
- data/lib/strata/cli/sub_commands/datasource.rb +13 -4
- data/lib/strata/cli/sub_commands/deploy.rb +14 -13
- data/lib/strata/cli/sub_commands/project.rb +4 -3
- data/lib/strata/cli/sub_commands/table.rb +9 -8
- data/lib/strata/cli/terminal.rb +7 -4
- data/lib/strata/cli/ui/field_editor.rb +21 -27
- data/lib/strata/cli/utils/deployment_monitor.rb +15 -34
- data/lib/strata/cli/utils/git.rb +78 -0
- data/lib/strata/cli/utils/import_manager.rb +4 -1
- data/lib/strata/cli/utils/test_reporter.rb +4 -32
- data/lib/strata/cli/utils/version_checker.rb +4 -8
- data/lib/strata/cli/utils.rb +3 -1
- data/lib/strata/cli/version.rb +1 -1
- data/lib/strata/cli.rb +4 -3
- metadata +4 -2
- data/lib/strata/cli/helpers/color_helper.rb +0 -103
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31a0327b1b8df2f7a982ece6c882cf019322cdf6e649b8cafdf035132424a705
|
|
4
|
+
data.tar.gz: 16dbe764a144c7098574900d54fdad599727a65dbfc82fef531b6f63a59e39a7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2fa33f77c3082a06a9a7e3e52cbeebd5b12ed4195b621011380ed2c39d124f3ebcd28b1962f8cf1565a999e101c8df7db15ea864dafed4d778211272a5c3f250
|
|
7
|
+
data.tar.gz: b1b1250691596a599af3b3193220242fe36f50eb4c7cab83eaac18d3a59795979b4b861c11799a65588e6a077594b2ac396e45a54c4aa216421aaf6846896146
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.9] - 2026-05-13
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Branch subcommands**: `strata branch` with `list` (local and server branches), `create`, `checkout`, and `delete` (remove a branch on the Strata server). Git helpers extended to support these flows.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Terminal output**: Centralized printing through `Output`, reduced verbose logs from deployment monitoring and test reporting; removed `ColorHelper` in favor of shared output helpers.
|
|
12
|
+
- **API client errors**: Added `ResponseErrorHandler` to centralize HTTP error handling and user-facing messages across the client and generators.
|
|
13
|
+
- **AI table generation**: Updated `SYSTEM_PROMPT` for YAML model / field generation.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **`list` command**: Correctly includes nested models in listings.
|
|
18
|
+
- **Field editor**: Dimension lookup flags apply only to dimensions.
|
|
19
|
+
|
|
20
|
+
## [0.1.8] - 2026-04-28
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Snowflake authentication**: Removed OAuth flow from CLI prompts and templates; Snowflake now supports `pat` and `kp` only.
|
|
25
|
+
- **Databricks authentication**: Standardized on OAuth M2M (service principal) only; removed U2M flow and auth-mode selection prompt.
|
|
26
|
+
- **CLI credential UX**: Added explicit guidance during Databricks credential collection that service principal `oauth_client_id` and `oauth_client_secret` are expected.
|
|
27
|
+
|
|
3
28
|
## [0.1.7] - 2026-04-23
|
|
4
29
|
|
|
5
30
|
### Added
|
data/README.md
CHANGED
|
@@ -342,14 +342,37 @@ my_snowflake:
|
|
|
342
342
|
schema: PUBLIC
|
|
343
343
|
role: ACCOUNTADMIN
|
|
344
344
|
auth_mode: pat
|
|
345
|
+
|
|
346
|
+
my_databricks:
|
|
347
|
+
adapter: databricks
|
|
348
|
+
name: Databricks Warehouse
|
|
349
|
+
host: workspace.cloud.databricks.com
|
|
350
|
+
warehouse: warehouse_id
|
|
351
|
+
catalog: main
|
|
352
|
+
schema: default
|
|
353
|
+
auth_mode: oauth_m2m
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Databricks authentication
|
|
357
|
+
|
|
358
|
+
Databricks now requires explicit `auth_mode`.
|
|
359
|
+
|
|
360
|
+
If you already have Databricks datasources, update `datasources.yml`:
|
|
361
|
+
|
|
362
|
+
```yaml
|
|
363
|
+
my_databricks:
|
|
364
|
+
adapter: databricks
|
|
365
|
+
auth_mode: oauth_m2m
|
|
345
366
|
```
|
|
346
367
|
|
|
368
|
+
Use service principal credentials and run `strata ds auth my_databricks` to save `oauth_client_id` and `oauth_client_secret` in `.strata`.
|
|
369
|
+
|
|
347
370
|
## Supported Data Warehouse Adapters
|
|
348
371
|
|
|
349
372
|
- **PostgreSQL** - Full support
|
|
350
373
|
- **MySQL** - Full support
|
|
351
374
|
- **SQL Server** - Full support (including Azure)
|
|
352
|
-
- **Snowflake** - Full support (PAT, Key Pair
|
|
375
|
+
- **Snowflake** - Full support (PAT, Key Pair)
|
|
353
376
|
- **Athena** - AWS Athena support
|
|
354
377
|
- **Trino** - Trino/Presto support
|
|
355
378
|
- **DuckDB** - Embedded analytics database
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../client"
|
|
4
|
+
require_relative "../../output"
|
|
4
5
|
require "yaml"
|
|
5
6
|
|
|
6
7
|
module Strata
|
|
@@ -14,17 +15,29 @@ module Strata
|
|
|
14
15
|
You are a semantic modeling expert for the Strata Business Intelligence platform.
|
|
15
16
|
Analyze database columns and generate semantic field definitions.
|
|
16
17
|
|
|
17
|
-
You must understand that Names are extremely important in the Strata application.
|
|
18
|
+
You must understand that Names are extremely important in the Strata application. When the same named dimension
|
|
18
19
|
is mapped to multiple tables, Strata treats them as the same entity but with multiple potential tables. Strata
|
|
19
|
-
will at query generation time choose the best table.
|
|
20
|
-
potential dimension or measure is the same as an existing one. You can
|
|
21
|
-
column name,
|
|
20
|
+
will at query generation time choose the best table. Therefore it is extremely important to consider whether a
|
|
21
|
+
potential dimension or measure is the same as an existing one. You can infer that based on the table name,
|
|
22
|
+
column name, existing fields, and business context. Reuse an existing dimension name only when it represents
|
|
23
|
+
the same business concept across tables. If the same column label describes the current table's own entity,
|
|
24
|
+
lifecycle, or attributes, create a table-specific name instead of reusing a generic existing name.
|
|
25
|
+
Example:
|
|
26
|
+
existing dimension name: Record Start Date
|
|
27
|
+
table name: web_page
|
|
28
|
+
column name: record_start_date
|
|
29
|
+
dimension name: Web Page Record Start Date
|
|
30
|
+
Example:
|
|
31
|
+
existing dimension name: Country
|
|
32
|
+
table name: customer
|
|
33
|
+
column name: country
|
|
34
|
+
dimension name: Country
|
|
22
35
|
The edge case is when you have a Dimension table like Customer with column first_name and another Dimension
|
|
23
36
|
table called Billed Customer with first_name, then they are likely not the same. A dimension created from
|
|
24
37
|
Billed Customer should be given an appropriately prefixed name: Billed Customer First Name.
|
|
25
38
|
|
|
26
|
-
Next, if the column is something highly
|
|
27
|
-
|
|
39
|
+
Next, if the column is something highly ambiguous and the current table is likely a dimension table, prefix
|
|
40
|
+
the dimension name with an appropriate prefix based on the name of the dimension table.
|
|
28
41
|
Example:
|
|
29
42
|
table name: item_dim
|
|
30
43
|
column name: color
|
|
@@ -48,8 +61,8 @@ module Strata
|
|
|
48
61
|
Example: "State" in a geography table → ["province"]
|
|
49
62
|
Example: "Customer Return Date" → [] -- name has high specificity already so no synonyms needed
|
|
50
63
|
|
|
51
|
-
In cases where
|
|
52
|
-
except the following fields: name, schema_type, data_type, expression.
|
|
64
|
+
In cases where you intentionally reuse an existing dimension name because it is the same business concept,
|
|
65
|
+
omit everything except the following fields: name, schema_type, data_type, expression.
|
|
53
66
|
|
|
54
67
|
Output ONLY valid JSON array, no explanations.
|
|
55
68
|
PROMPT
|
|
@@ -68,7 +81,7 @@ module Strata
|
|
|
68
81
|
# @return [Hash, nil] Generated model data or nil if AI disabled
|
|
69
82
|
def call(table_name:, columns:, datasource:, user_context: nil)
|
|
70
83
|
unless ai_available?
|
|
71
|
-
|
|
84
|
+
Output.print_warning("AI not available - using rule-based fallback")
|
|
72
85
|
return fallback_fields(columns)
|
|
73
86
|
end
|
|
74
87
|
|
|
@@ -85,7 +98,7 @@ module Strata
|
|
|
85
98
|
parse_response(response, columns)
|
|
86
99
|
rescue => e
|
|
87
100
|
# Fallback to basic field generation on AI error
|
|
88
|
-
|
|
101
|
+
Output.print_warning("AI failed to generate fields: #{e.message} - performing basic field generation")
|
|
89
102
|
fallback_fields(columns)
|
|
90
103
|
end
|
|
91
104
|
end
|
|
@@ -99,8 +112,7 @@ module Strata
|
|
|
99
112
|
# @return [Array<Hash>] Regenerated fields
|
|
100
113
|
def call_with_prompt(table_name:, columns:, datasource:, user_prompt:, current_fields:)
|
|
101
114
|
unless ai_available?
|
|
102
|
-
|
|
103
|
-
return nil
|
|
115
|
+
raise AIError, "AI is not available - configure ai_api_key in .strata"
|
|
104
116
|
end
|
|
105
117
|
|
|
106
118
|
prompt = build_prompt_with_user_input(
|
|
@@ -114,8 +126,7 @@ module Strata
|
|
|
114
126
|
response = @client.complete(prompt, system_prompt: SYSTEM_PROMPT)
|
|
115
127
|
parse_response(response, columns)
|
|
116
128
|
rescue => e
|
|
117
|
-
|
|
118
|
-
nil
|
|
129
|
+
raise AIError, "AI failed to generate fields: #{e.message}"
|
|
119
130
|
end
|
|
120
131
|
end
|
|
121
132
|
|
|
@@ -134,7 +145,7 @@ module Strata
|
|
|
134
145
|
end.join("\n")
|
|
135
146
|
|
|
136
147
|
context_section = if existing_models_context && !existing_models_context.empty?
|
|
137
|
-
"\n\nExisting model
|
|
148
|
+
"\n\nExisting semantic model fields for naming comparison:\n#{existing_models_context}"
|
|
138
149
|
else
|
|
139
150
|
""
|
|
140
151
|
end
|
|
@@ -168,7 +179,7 @@ module Strata
|
|
|
168
179
|
|
|
169
180
|
existing_models = load_existing_models_context
|
|
170
181
|
context_section = if existing_models && !existing_models.empty?
|
|
171
|
-
"\n\nExisting model
|
|
182
|
+
"\n\nExisting semantic model fields for naming comparison:\n#{existing_models}"
|
|
172
183
|
else
|
|
173
184
|
""
|
|
174
185
|
end
|
|
@@ -190,17 +201,21 @@ module Strata
|
|
|
190
201
|
def load_existing_models_context
|
|
191
202
|
return nil unless Dir.exist?("models")
|
|
192
203
|
|
|
193
|
-
model_files = Dir.glob("models/**/tbl
|
|
204
|
+
model_files = Dir.glob("models/**/tbl[._]*.yml").first(100) # Limit to 5 for performance
|
|
194
205
|
return nil if model_files.empty?
|
|
195
206
|
|
|
196
207
|
contexts = model_files.filter_map do |file|
|
|
197
208
|
model = YAML.safe_load_file(file, permitted_classes: [Date, Time], aliases: true) || {}
|
|
198
209
|
fields_summary = (model["fields"] || []).map do |f|
|
|
199
|
-
|
|
210
|
+
expression = f["expression"]
|
|
211
|
+
expression_sql = expression.is_a?(Hash) ? expression["sql"] : expression
|
|
212
|
+
"#{f["name"]} (#{f["type"]}) data type: #{f["data_type"]}, expression: #{expression_sql}"
|
|
200
213
|
end.join("\n\t")
|
|
201
|
-
"Model: #{model["name"]} \n Fields: \n\t#{fields_summary}"
|
|
214
|
+
"Model: #{model["name"]}, physical table: #{model["physical_name"]}\n Fields: \n\t#{fields_summary}"
|
|
202
215
|
rescue => e
|
|
203
|
-
|
|
216
|
+
if ENV["DEBUG"]
|
|
217
|
+
Output.print_warning("Failed to load model file #{file}: #{e.message}")
|
|
218
|
+
end
|
|
204
219
|
nil
|
|
205
220
|
end
|
|
206
221
|
|
|
@@ -4,12 +4,14 @@ require "faraday"
|
|
|
4
4
|
require "faraday/multipart"
|
|
5
5
|
require "json"
|
|
6
6
|
require_relative "connection_error_handler"
|
|
7
|
+
require_relative "response_error_handler"
|
|
7
8
|
|
|
8
9
|
module Strata
|
|
9
10
|
module CLI
|
|
10
11
|
module API
|
|
11
12
|
class Client
|
|
12
13
|
include ConnectionErrorHandler
|
|
14
|
+
include ResponseErrorHandler
|
|
13
15
|
|
|
14
16
|
def initialize(server_url, api_key)
|
|
15
17
|
@server_url = server_url.chomp("/")
|
|
@@ -65,6 +67,21 @@ module Strata
|
|
|
65
67
|
deployments.first
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def branches(project_id)
|
|
71
|
+
response = with_connection_error_handling(@server_url) { connection.get(branches_url(project_id)) }
|
|
72
|
+
return [] if response.status == 404
|
|
73
|
+
|
|
74
|
+
handle_response(response)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def delete_branch(project_id, branch_id)
|
|
78
|
+
response = with_connection_error_handling(@server_url) { connection.delete(branch_url(project_id, branch_id)) }
|
|
79
|
+
return false if response.status == 404
|
|
80
|
+
|
|
81
|
+
handle_response(response)
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
68
85
|
def create_project(name, uid, description: nil, git: nil, production_branch: "main")
|
|
69
86
|
url = "#{@server_url}/api/v1/projects"
|
|
70
87
|
response = with_connection_error_handling(@server_url) do
|
|
@@ -96,73 +113,16 @@ module Strata
|
|
|
96
113
|
end
|
|
97
114
|
end
|
|
98
115
|
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
when 200..299
|
|
102
|
-
response.body
|
|
103
|
-
when 401
|
|
104
|
-
raise Strata::CommandError, "Authentication failed. Check your API key."
|
|
105
|
-
when 403
|
|
106
|
-
raise Strata::CommandError, "Access denied. You don't have permission for this action."
|
|
107
|
-
when 404
|
|
108
|
-
raise Strata::CommandError, "Resource not found."
|
|
109
|
-
when 422
|
|
110
|
-
errors = extract_errors(response.body)
|
|
111
|
-
raise Strata::CommandError, "Validation failed: #{errors}"
|
|
112
|
-
when 500..599
|
|
113
|
-
raise Strata::CommandError, "Server error: #{response.status}"
|
|
114
|
-
else
|
|
115
|
-
raise Strata::CommandError, "Unexpected response: #{response.status}"
|
|
116
|
-
end
|
|
117
|
-
rescue Faraday::Error => e
|
|
118
|
-
raise Strata::CommandError, "Network error: #{e.message}"
|
|
116
|
+
def deployment_url(project_id, branch_id)
|
|
117
|
+
"#{@server_url}/api/v1/projects/#{project_id}/b/#{branch_id}/deploys"
|
|
119
118
|
end
|
|
120
119
|
|
|
121
|
-
def
|
|
122
|
-
#
|
|
123
|
-
return "Unknown error" if body.nil? || body.empty?
|
|
124
|
-
|
|
125
|
-
# Check if it's an HTML error page (Rails error page)
|
|
126
|
-
if body.is_a?(String) && body.include?("<!DOCTYPE html>")
|
|
127
|
-
# Try to extract error message from HTML
|
|
128
|
-
if body =~ %r{<h1[^>]*>(.*?)</h1>}
|
|
129
|
-
return "Server error: #{::Regexp.last_match(1).strip}"
|
|
130
|
-
elsif body =~ %r{<title[^>]*>(.*?)</title>}
|
|
131
|
-
return "Server error: #{::Regexp.last_match(1).strip}"
|
|
132
|
-
else
|
|
133
|
-
return "Server returned an HTML error page. Check server logs for details."
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Parse JSON string if needed
|
|
138
|
-
parsed_body = if body.is_a?(String)
|
|
139
|
-
begin
|
|
140
|
-
JSON.parse(body)
|
|
141
|
-
rescue JSON::ParserError
|
|
142
|
-
return body[0..200] # Return first 200 chars if not JSON
|
|
143
|
-
end
|
|
144
|
-
else
|
|
145
|
-
body
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Extract error messages from various formats
|
|
149
|
-
if parsed_body.is_a?(Hash)
|
|
150
|
-
if parsed_body["errors"].is_a?(Array)
|
|
151
|
-
parsed_body["errors"].join(", ")
|
|
152
|
-
elsif parsed_body["error"].is_a?(String)
|
|
153
|
-
parsed_body["error"]
|
|
154
|
-
elsif parsed_body["message"].is_a?(String)
|
|
155
|
-
parsed_body["message"]
|
|
156
|
-
else
|
|
157
|
-
parsed_body.to_json
|
|
158
|
-
end
|
|
159
|
-
else
|
|
160
|
-
parsed_body.to_s[0..200] # Limit length
|
|
161
|
-
end
|
|
120
|
+
def branches_url(project_id)
|
|
121
|
+
"#{@server_url}/api/v1/projects/#{project_id}/branches"
|
|
162
122
|
end
|
|
163
123
|
|
|
164
|
-
def
|
|
165
|
-
"#{@server_url}/api/v1/projects/#{project_id}/b/#{branch_id}
|
|
124
|
+
def branch_url(project_id, branch_id)
|
|
125
|
+
"#{@server_url}/api/v1/projects/#{project_id}/b/#{branch_id}"
|
|
166
126
|
end
|
|
167
127
|
end
|
|
168
128
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Strata
|
|
7
|
+
module CLI
|
|
8
|
+
module API
|
|
9
|
+
module ResponseErrorHandler
|
|
10
|
+
DETAIL_LIMIT = 200
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def handle_response(response)
|
|
15
|
+
case response.status
|
|
16
|
+
when 200..299
|
|
17
|
+
response.body
|
|
18
|
+
else
|
|
19
|
+
raise Strata::CommandError, response_error_message(response)
|
|
20
|
+
end
|
|
21
|
+
rescue Faraday::Error => e
|
|
22
|
+
raise Strata::CommandError, "Network error: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def response_error_message(response, default_message: nil)
|
|
26
|
+
status = response.status
|
|
27
|
+
return "Server error: #{status}" if (500..599).cover?(status)
|
|
28
|
+
|
|
29
|
+
message = default_message || base_response_error_message(status)
|
|
30
|
+
details = extract_error_details(response.body)
|
|
31
|
+
return message unless details
|
|
32
|
+
|
|
33
|
+
return "#{message}: #{details}" if status == 422
|
|
34
|
+
|
|
35
|
+
"#{message} Details: #{details}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def base_response_error_message(status)
|
|
39
|
+
case status
|
|
40
|
+
when 401
|
|
41
|
+
"Authentication failed. Check your API key."
|
|
42
|
+
when 403
|
|
43
|
+
"Access denied. You don't have permission for this action."
|
|
44
|
+
when 404
|
|
45
|
+
"Resource not found."
|
|
46
|
+
when 422
|
|
47
|
+
"Validation failed"
|
|
48
|
+
else
|
|
49
|
+
"Request failed with status #{status}."
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_error_details(body)
|
|
54
|
+
return nil if body.nil? || (body.respond_to?(:empty?) && body.empty?)
|
|
55
|
+
|
|
56
|
+
return extract_html_error_details(body) if body.is_a?(String) && body.include?("<!DOCTYPE html>")
|
|
57
|
+
|
|
58
|
+
parsed_body = parse_error_body(body)
|
|
59
|
+
details = details_from_parsed_body(parsed_body)
|
|
60
|
+
truncate_error_detail(details)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_html_error_details(body)
|
|
64
|
+
detail = if body =~ %r{<h1[^>]*>(.*?)</h1>}m
|
|
65
|
+
::Regexp.last_match(1)
|
|
66
|
+
elsif body =~ %r{<title[^>]*>(.*?)</title>}m
|
|
67
|
+
::Regexp.last_match(1)
|
|
68
|
+
else
|
|
69
|
+
"Server returned an HTML error page."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
truncate_error_detail(detail)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_error_body(body)
|
|
76
|
+
return body unless body.is_a?(String)
|
|
77
|
+
|
|
78
|
+
JSON.parse(body)
|
|
79
|
+
rescue JSON::ParserError
|
|
80
|
+
body
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def details_from_parsed_body(parsed_body)
|
|
84
|
+
case parsed_body
|
|
85
|
+
when Hash
|
|
86
|
+
parsed_body = parsed_body.transform_keys(&:to_s)
|
|
87
|
+
|
|
88
|
+
if parsed_body["errors"].is_a?(Array)
|
|
89
|
+
parsed_body["errors"].join(", ")
|
|
90
|
+
elsif parsed_body["errors"].is_a?(Hash)
|
|
91
|
+
parsed_body["errors"].flat_map do |field, messages|
|
|
92
|
+
Array(messages).map { |message| "#{field} #{message}" }
|
|
93
|
+
end.join(", ")
|
|
94
|
+
elsif parsed_body["error"].is_a?(String)
|
|
95
|
+
parsed_body["error"]
|
|
96
|
+
elsif parsed_body["message"].is_a?(String)
|
|
97
|
+
parsed_body["message"]
|
|
98
|
+
else
|
|
99
|
+
parsed_body.to_json
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
parsed_body.to_s
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def truncate_error_detail(detail)
|
|
107
|
+
detail = detail.to_s.gsub(/\s+/, " ").strip
|
|
108
|
+
return nil if detail.empty?
|
|
109
|
+
|
|
110
|
+
(detail.length > DETAIL_LIMIT) ? "#{detail[0...DETAIL_LIMIT]}..." : detail
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -16,8 +16,9 @@ module Strata
|
|
|
16
16
|
|
|
17
17
|
attr_reader :adapter, :credentials
|
|
18
18
|
|
|
19
|
-
def initialize(adapter)
|
|
19
|
+
def initialize(adapter, datasource_config: nil)
|
|
20
20
|
@adapter = adapter.downcase.strip
|
|
21
|
+
@datasource_config = datasource_config || {}
|
|
21
22
|
@prompt = TTY::Prompt.new
|
|
22
23
|
end
|
|
23
24
|
|
|
@@ -30,7 +31,7 @@ module Strata
|
|
|
30
31
|
|
|
31
32
|
case adapter
|
|
32
33
|
when "snowflake"
|
|
33
|
-
auth_mode = @prompt.select("Authentication mode:", %w[pat kp
|
|
34
|
+
auth_mode = @prompt.select("Authentication mode:", %w[pat kp], default: "pat")
|
|
34
35
|
credentials["auth_mode"] = auth_mode
|
|
35
36
|
|
|
36
37
|
case auth_mode
|
|
@@ -39,18 +40,13 @@ module Strata
|
|
|
39
40
|
when "kp"
|
|
40
41
|
credentials["username"] = @prompt.ask("Enter Username:")
|
|
41
42
|
credentials["private_key"] = @prompt.ask("Enter Private Key Absolute Path:")
|
|
42
|
-
when "oauth"
|
|
43
|
-
credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
|
|
44
|
-
credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
|
|
45
|
-
credentials["oauth_redirect_uri"] = @prompt.ask("OAuth Redirect URI:", default: "https://localhost:3420/callback")
|
|
46
|
-
oauth_scope = @prompt.ask("OAuth Scope (optional):")
|
|
47
|
-
credentials["oauth_scope"] = oauth_scope unless oauth_scope.empty?
|
|
48
43
|
end
|
|
49
44
|
when "athena"
|
|
50
45
|
credentials["access_key_id"] = @prompt.ask("AWS Access Key ID:")
|
|
51
46
|
credentials["secret_access_key"] = @prompt.ask("AWS Secret Access Key:")
|
|
52
47
|
when "databricks"
|
|
53
|
-
credentials["
|
|
48
|
+
credentials["auth_mode"] = "oauth_m2m"
|
|
49
|
+
credentials["oauth_client_id"] = @prompt.ask("OAuth Client ID:")
|
|
54
50
|
credentials["oauth_client_secret"] = @prompt.ask("OAuth Client Secret:")
|
|
55
51
|
else
|
|
56
52
|
if required?
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "time"
|
|
5
|
+
require_relative "output"
|
|
5
6
|
|
|
6
7
|
module Strata
|
|
7
8
|
module CLI
|
|
@@ -63,7 +64,9 @@ module Strata
|
|
|
63
64
|
f.puts("")
|
|
64
65
|
end
|
|
65
66
|
rescue => e
|
|
66
|
-
|
|
67
|
+
if ENV["DEBUG"]
|
|
68
|
+
Output.print_warning("Failed to write CLI log: #{e.message}")
|
|
69
|
+
end
|
|
67
70
|
end
|
|
68
71
|
end
|
|
69
72
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "group"
|
|
4
4
|
require "open3"
|
|
5
|
+
require_relative "../output"
|
|
5
6
|
|
|
6
7
|
module Strata
|
|
7
8
|
module CLI
|
|
@@ -24,7 +25,7 @@ module Strata
|
|
|
24
25
|
|
|
25
26
|
def add_datasource_config
|
|
26
27
|
@ds_key = get_unique_ds_key
|
|
27
|
-
|
|
28
|
+
Output.print_status(:adapter, "adding #{adapter} config to datasources", type: :warning, context: self)
|
|
28
29
|
|
|
29
30
|
# Interactive mode: write config directly from prompts
|
|
30
31
|
config_yaml = {@ds_key => options[:config]}.to_yaml.sub(/^---\n/, "\n")
|
|
@@ -44,12 +45,12 @@ module Strata
|
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def install_duckdb_gem
|
|
47
|
-
|
|
48
|
+
Output.print_status(:gem, "Installing duckdb gem...", type: :warning, context: self)
|
|
48
49
|
|
|
49
50
|
begin
|
|
50
51
|
# Install the duckdb gem
|
|
51
52
|
run "gem install duckdb", verbose: false, capture: true
|
|
52
|
-
|
|
53
|
+
Output.print_status(:success, "duckdb gem installed successfully", type: :success, context: self)
|
|
53
54
|
rescue => e
|
|
54
55
|
raise DWH::ConfigError, "Failed to install duckdb gem: #{e.message}"
|
|
55
56
|
end
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../output"
|
|
4
|
+
|
|
3
5
|
module Strata
|
|
4
6
|
module CLI
|
|
5
7
|
module Generators
|
|
6
8
|
class Group < Thor::Group
|
|
7
9
|
include Thor::Actions
|
|
10
|
+
include Output
|
|
11
|
+
|
|
12
|
+
class QuietActionsShell < Thor::Shell::Color
|
|
13
|
+
DEFAULT_ALLOWED_STATUSES = %w[
|
|
14
|
+
created
|
|
15
|
+
setup
|
|
16
|
+
clone
|
|
17
|
+
success
|
|
18
|
+
adapter
|
|
19
|
+
gem
|
|
20
|
+
].freeze
|
|
21
|
+
|
|
22
|
+
def initialize(verbose: false, allowed_statuses: DEFAULT_ALLOWED_STATUSES)
|
|
23
|
+
super()
|
|
24
|
+
@verbose = verbose
|
|
25
|
+
@allowed_statuses = allowed_statuses
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def say_status(status, message, log_status = true)
|
|
29
|
+
return super if @verbose
|
|
30
|
+
return if log_status == false
|
|
31
|
+
|
|
32
|
+
s = status.to_s
|
|
33
|
+
return super if @allowed_statuses.include?(s)
|
|
34
|
+
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
no_commands do
|
|
40
|
+
def initialize(*args)
|
|
41
|
+
super
|
|
42
|
+
@shell = QuietActionsShell.new(verbose: options[:verbose] == true)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
8
45
|
|
|
9
46
|
def self.source_root
|
|
10
47
|
File.expand_path("templates/", __dir__)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "group"
|
|
4
|
+
require_relative "../output"
|
|
4
5
|
|
|
5
6
|
module Strata
|
|
6
7
|
module CLI
|
|
@@ -28,7 +29,7 @@ module Strata
|
|
|
28
29
|
# Write the updated template
|
|
29
30
|
create_file output_path, updated_content
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
Output.print_status(:created, output_path, type: :success, context: self)
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
private
|