hatchet-sdk 0.0.0 → 0.0.1
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/CLAUDE.md +41 -4
- data/lib/hatchet/config.rb +482 -0
- data/lib/hatchet/version.rb +1 -1
- data/lib/hatchet-sdk.rb +53 -3
- data/sig/hatchet-sdk.rbs +104 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 415716e0955c26837a2887bd5703497b2bda92f167593742747aff057d4db77a
|
|
4
|
+
data.tar.gz: 510bc99100f79cb590eafc874b0e4114035bec15cff11f77229a6b59a497ad70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 174e40cbb3d3c5470c3ae2aaa3a148048240fd4665519177e33d2dddd4f567c3105ba01dad812eefa586fda560c353395689a3f0f228e19c9a554a670d2508df
|
|
7
|
+
data.tar.gz: ae343d08ea3648c703a7d4f66b7bd23b0527dca64ee6beb0a4fee66c300c8ac95d42ed50f30155a5372cb6ebf0bd663c7c4fa8776c13a09b31250c56e8863958
|
data/CLAUDE.md
CHANGED
|
@@ -4,7 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
This is the Ruby SDK for Hatchet (gem name: hatchet-sdk), currently in early development (version 0.0.0). The project follows standard Ruby gem conventions with a simple structure containing a main `Hatchet` module with a `Client` class for
|
|
7
|
+
This is the Ruby SDK for Hatchet (gem name: hatchet-sdk), currently in early development (version 0.0.0). The project follows standard Ruby gem conventions with a simple structure containing a main `Hatchet` module with a `Client` class for JWT token authentication.
|
|
8
|
+
|
|
9
|
+
The SDK includes comprehensive documentation and type signatures for excellent IDE support, including parameter hints, auto-completion, and type checking.
|
|
8
10
|
|
|
9
11
|
## Common Commands
|
|
10
12
|
|
|
@@ -25,18 +27,53 @@ This is the Ruby SDK for Hatchet (gem name: hatchet-sdk), currently in early dev
|
|
|
25
27
|
**Gem Management:**
|
|
26
28
|
- `bundle exec rake release` - Release new version (updates version, creates git tag, pushes to rubygems)
|
|
27
29
|
|
|
30
|
+
**Documentation:**
|
|
31
|
+
- `yard doc` - Generate YARD documentation (if yard gem is installed)
|
|
32
|
+
- `rbs validate` - Validate RBS type signatures for syntax errors
|
|
33
|
+
|
|
28
34
|
## Architecture
|
|
29
35
|
|
|
30
36
|
**Core Structure:**
|
|
31
37
|
- `lib/hatchet-sdk.rb` - Main entry point, defines `Hatchet` module with `Error` and `Client` classes
|
|
32
38
|
- `lib/hatchet/version.rb` - Version constant
|
|
33
|
-
- `
|
|
34
|
-
- `
|
|
39
|
+
- `lib/hatchet/config.rb` - Configuration classes with comprehensive JWT token support
|
|
40
|
+
- `spec/` - RSpec tests with monkey patching disabled (36+ test cases)
|
|
41
|
+
- `sig/hatchet-sdk.rbs` - Ruby type signatures for IDE integration
|
|
35
42
|
|
|
36
43
|
**Key Classes:**
|
|
37
|
-
- `Hatchet::Client` - Main client class that accepts
|
|
44
|
+
- `Hatchet::Client` - Main client class that accepts JWT token for authentication
|
|
45
|
+
- `Hatchet::Config` - Configuration class supporting multiple sources (params, env vars, JWT payload)
|
|
46
|
+
- `Hatchet::TLSConfig` - TLS configuration for secure connections
|
|
47
|
+
- `Hatchet::HealthcheckConfig` - Worker health monitoring configuration
|
|
48
|
+
- `Hatchet::OpenTelemetryConfig` - Observability configuration
|
|
38
49
|
- `Hatchet::Error` - Base error class for gem-specific exceptions
|
|
39
50
|
|
|
51
|
+
**Configuration Sources (priority order):**
|
|
52
|
+
1. Explicit constructor parameters (highest priority)
|
|
53
|
+
2. Environment variables (`HATCHET_CLIENT_*`)
|
|
54
|
+
3. JWT token payload (tenant_id extracted from 'sub' field)
|
|
55
|
+
4. Default values (lowest priority)
|
|
56
|
+
|
|
57
|
+
**Documentation & IDE Support:**
|
|
58
|
+
- **YARD documentation** - Comprehensive JSDoc-style comments with examples
|
|
59
|
+
- **RBS type signatures** - Complete type definitions for IDE parameter hints
|
|
60
|
+
- **Sorbet compatibility** - Tagged with `# typed: strict` for type checking
|
|
61
|
+
- IDEs with Ruby LSP/RubyMine will show parameter hints, auto-completion, and types
|
|
62
|
+
|
|
40
63
|
The codebase uses frozen string literals and follows Ruby 3.1+ requirements.
|
|
41
64
|
|
|
65
|
+
## Development Notes
|
|
66
|
+
|
|
67
|
+
**When adding new configuration options:**
|
|
68
|
+
1. Add the parameter to `Config#initialize` method
|
|
69
|
+
2. Update the `@option` YARD documentation in both `Client` and `Config` classes
|
|
70
|
+
3. Add the parameter to RBS type signatures in `sig/hatchet-sdk.rbs`
|
|
71
|
+
4. Add comprehensive test coverage in `spec/hatchet/config_spec.rb`
|
|
72
|
+
5. Update this CLAUDE.md file with any architectural changes
|
|
73
|
+
|
|
74
|
+
**Testing JWT token functionality:**
|
|
75
|
+
- Use `Base64.encode64('{"sub":"tenant-id"}').gsub(/\n/, "").gsub(/=+$/, "")` to create test JWT payloads
|
|
76
|
+
- The config extracts tenant_id from the 'sub' field in JWT tokens
|
|
77
|
+
- Test both explicit tenant_id override and JWT extraction scenarios
|
|
78
|
+
|
|
42
79
|
Keep the CLAUDE.md instructions up to date as the project continues to develop.
|
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
6
|
+
|
|
7
|
+
module Hatchet
|
|
8
|
+
# TLS configuration for client connections
|
|
9
|
+
#
|
|
10
|
+
# @example Basic TLS setup
|
|
11
|
+
# tls = TLSConfig.new(strategy: "tls", server_name: "api.hatchet.com")
|
|
12
|
+
#
|
|
13
|
+
# @example Custom certificate setup
|
|
14
|
+
# tls = TLSConfig.new(
|
|
15
|
+
# strategy: "mtls",
|
|
16
|
+
# cert_file: "/path/to/client.crt",
|
|
17
|
+
# key_file: "/path/to/client.key",
|
|
18
|
+
# root_ca_file: "/path/to/ca.crt"
|
|
19
|
+
# )
|
|
20
|
+
class TLSConfig
|
|
21
|
+
# @!attribute strategy
|
|
22
|
+
# @return [String] TLS strategy ("tls", "mtls", "insecure")
|
|
23
|
+
# @!attribute cert_file
|
|
24
|
+
# @return [String, nil] Path to client certificate file for mTLS
|
|
25
|
+
# @!attribute key_file
|
|
26
|
+
# @return [String, nil] Path to client private key file for mTLS
|
|
27
|
+
# @!attribute root_ca_file
|
|
28
|
+
# @return [String, nil] Path to root CA certificate file
|
|
29
|
+
# @!attribute server_name
|
|
30
|
+
# @return [String] Server name for TLS verification
|
|
31
|
+
attr_accessor :strategy, :cert_file, :key_file, :root_ca_file, :server_name
|
|
32
|
+
|
|
33
|
+
# Initialize TLS configuration
|
|
34
|
+
#
|
|
35
|
+
# @param options [Hash] TLS configuration options
|
|
36
|
+
# @option options [String] :strategy TLS strategy (default: "tls")
|
|
37
|
+
# @option options [String] :cert_file Path to client certificate file
|
|
38
|
+
# @option options [String] :key_file Path to client private key file
|
|
39
|
+
# @option options [String] :root_ca_file Path to root CA certificate file
|
|
40
|
+
# @option options [String] :server_name Server name for TLS verification
|
|
41
|
+
def initialize(**options)
|
|
42
|
+
@strategy = options[:strategy] || env_var("HATCHET_CLIENT_TLS_STRATEGY") || "tls"
|
|
43
|
+
@cert_file = options[:cert_file] || env_var("HATCHET_CLIENT_TLS_CERT_FILE")
|
|
44
|
+
@key_file = options[:key_file] || env_var("HATCHET_CLIENT_TLS_KEY_FILE")
|
|
45
|
+
@root_ca_file = options[:root_ca_file] || env_var("HATCHET_CLIENT_TLS_ROOT_CA_FILE")
|
|
46
|
+
@server_name = options[:server_name] || env_var("HATCHET_CLIENT_TLS_SERVER_NAME") || ""
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def env_var(name)
|
|
52
|
+
ENV[name]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Healthcheck configuration for worker health monitoring
|
|
57
|
+
#
|
|
58
|
+
# @example Enable healthcheck on custom port
|
|
59
|
+
# healthcheck = HealthcheckConfig.new(enabled: true, port: 8080)
|
|
60
|
+
class HealthcheckConfig
|
|
61
|
+
# @!attribute port
|
|
62
|
+
# @return [Integer] Port number for healthcheck endpoint
|
|
63
|
+
# @!attribute enabled
|
|
64
|
+
# @return [Boolean] Whether healthcheck is enabled
|
|
65
|
+
attr_accessor :port, :enabled
|
|
66
|
+
|
|
67
|
+
# Initialize healthcheck configuration
|
|
68
|
+
#
|
|
69
|
+
# @param options [Hash] Healthcheck configuration options
|
|
70
|
+
# @option options [Integer] :port Port number for healthcheck endpoint (default: 8001)
|
|
71
|
+
# @option options [Boolean] :enabled Whether healthcheck is enabled (default: false)
|
|
72
|
+
def initialize(**options)
|
|
73
|
+
@port = parse_int(options[:port] || env_var("HATCHET_CLIENT_WORKER_HEALTHCHECK_PORT")) || 8001
|
|
74
|
+
@enabled = parse_bool(options[:enabled] || env_var("HATCHET_CLIENT_WORKER_HEALTHCHECK_ENABLED")) || false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def env_var(name)
|
|
80
|
+
ENV[name]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_int(value)
|
|
84
|
+
return nil if value.nil?
|
|
85
|
+
return value if value.is_a?(Integer)
|
|
86
|
+
return nil if value.respond_to?(:empty?) && value.empty?
|
|
87
|
+
|
|
88
|
+
Integer(value)
|
|
89
|
+
rescue ArgumentError
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def parse_bool(value)
|
|
94
|
+
return nil if value.nil?
|
|
95
|
+
return value if [true, false].include?(value)
|
|
96
|
+
|
|
97
|
+
%w[true 1 yes on].include?(value.to_s.downcase)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# OpenTelemetry configuration for observability
|
|
102
|
+
#
|
|
103
|
+
# @example Exclude sensitive attributes from telemetry
|
|
104
|
+
# otel = OpenTelemetryConfig.new(excluded_attributes: ["password", "api_key"])
|
|
105
|
+
class OpenTelemetryConfig
|
|
106
|
+
# @!attribute excluded_attributes
|
|
107
|
+
# @return [Array<String>] List of attribute names to exclude from telemetry
|
|
108
|
+
attr_accessor :excluded_attributes
|
|
109
|
+
|
|
110
|
+
# Initialize OpenTelemetry configuration
|
|
111
|
+
#
|
|
112
|
+
# @param options [Hash] OpenTelemetry configuration options
|
|
113
|
+
# @option options [Array<String>] :excluded_attributes List of attribute names to exclude from telemetry
|
|
114
|
+
def initialize(**options)
|
|
115
|
+
@excluded_attributes = options[:excluded_attributes] ||
|
|
116
|
+
parse_json_array(
|
|
117
|
+
env_var("HATCHET_CLIENT_OPENTELEMETRY_EXCLUDED_ATTRIBUTES")
|
|
118
|
+
) || []
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def env_var(name)
|
|
124
|
+
ENV[name]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_json_array(value)
|
|
128
|
+
return nil if value.nil? || value.empty?
|
|
129
|
+
|
|
130
|
+
JSON.parse(value)
|
|
131
|
+
rescue JSON::ParserError
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Configuration class for Hatchet client settings
|
|
137
|
+
#
|
|
138
|
+
# This class manages all configuration options for the Hatchet client, including
|
|
139
|
+
# token authentication, connection settings, and various client behaviors.
|
|
140
|
+
# Configuration can be set via constructor options, environment variables, or
|
|
141
|
+
# extracted from JWT tokens.
|
|
142
|
+
#
|
|
143
|
+
# @example Basic configuration
|
|
144
|
+
# config = Config.new(token: "your-jwt-token")
|
|
145
|
+
#
|
|
146
|
+
# @example Full configuration with all options
|
|
147
|
+
# config = Config.new(
|
|
148
|
+
# token: "your-jwt-token",
|
|
149
|
+
# host_port: "localhost:7070",
|
|
150
|
+
# server_url: "https://api.hatchet.com",
|
|
151
|
+
# namespace: "production",
|
|
152
|
+
# tenant_id: "custom-tenant",
|
|
153
|
+
# worker_preset_labels: { "env" => "prod", "region" => "us-east-1" }
|
|
154
|
+
# )
|
|
155
|
+
#
|
|
156
|
+
# @example Configuration via environment variables
|
|
157
|
+
# ENV["HATCHET_CLIENT_TOKEN"] = "your-jwt-token"
|
|
158
|
+
# ENV["HATCHET_CLIENT_HOST_PORT"] = "localhost:7070"
|
|
159
|
+
# config = Config.new # Will load from environment
|
|
160
|
+
class Config
|
|
161
|
+
DEFAULT_HOST_PORT = "localhost:7070"
|
|
162
|
+
DEFAULT_SERVER_URL = "https://app.dev.hatchet-tools.com"
|
|
163
|
+
DEFAULT_GRPC_MAX_MESSAGE_LENGTH = 4 * 1024 * 1024 # 4MB
|
|
164
|
+
|
|
165
|
+
ENV_FILE_NAMES = [".env", ".env.hatchet", ".env.dev", ".env.local"].freeze
|
|
166
|
+
|
|
167
|
+
# @!attribute token
|
|
168
|
+
# @return [String] JWT token for authentication
|
|
169
|
+
# @!attribute tenant_id
|
|
170
|
+
# @return [String] Tenant ID (extracted from JWT 'sub' field if not provided)
|
|
171
|
+
# @!attribute host_port
|
|
172
|
+
# @return [String] gRPC server host and port
|
|
173
|
+
# @!attribute server_url
|
|
174
|
+
# @return [String] Server URL for HTTP requests
|
|
175
|
+
# @!attribute namespace
|
|
176
|
+
# @return [String] Namespace prefix for resource names
|
|
177
|
+
# @!attribute logger
|
|
178
|
+
# @return [Logger] Logger instance
|
|
179
|
+
# @!attribute listener_v2_timeout
|
|
180
|
+
# @return [Integer, nil] Timeout for listener v2 in milliseconds
|
|
181
|
+
# @!attribute grpc_max_recv_message_length
|
|
182
|
+
# @return [Integer] Maximum gRPC receive message length in bytes
|
|
183
|
+
# @!attribute grpc_max_send_message_length
|
|
184
|
+
# @return [Integer] Maximum gRPC send message length in bytes
|
|
185
|
+
# @!attribute worker_preset_labels
|
|
186
|
+
# @return [Hash<String, String>] Hash of preset labels for workers
|
|
187
|
+
# @!attribute enable_force_kill_sync_threads
|
|
188
|
+
# @return [Boolean] Enable force killing of sync threads
|
|
189
|
+
# @!attribute enable_thread_pool_monitoring
|
|
190
|
+
# @return [Boolean] Enable thread pool monitoring
|
|
191
|
+
# @!attribute terminate_worker_after_num_tasks
|
|
192
|
+
# @return [Integer, nil] Terminate worker after this many tasks
|
|
193
|
+
# @!attribute disable_log_capture
|
|
194
|
+
# @return [Boolean] Disable log capture
|
|
195
|
+
# @!attribute grpc_enable_fork_support
|
|
196
|
+
# @return [Boolean] Enable gRPC fork support
|
|
197
|
+
# @!attribute tls_config
|
|
198
|
+
# @return [TLSConfig] TLS configuration
|
|
199
|
+
# @!attribute healthcheck
|
|
200
|
+
# @return [HealthcheckConfig] Healthcheck configuration
|
|
201
|
+
# @!attribute otel
|
|
202
|
+
# @return [OpenTelemetryConfig] OpenTelemetry configuration
|
|
203
|
+
attr_accessor :token, :tenant_id, :host_port, :server_url, :namespace,
|
|
204
|
+
:logger, :listener_v2_timeout, :grpc_max_recv_message_length,
|
|
205
|
+
:grpc_max_send_message_length, :worker_preset_labels,
|
|
206
|
+
:enable_force_kill_sync_threads, :enable_thread_pool_monitoring,
|
|
207
|
+
:terminate_worker_after_num_tasks, :disable_log_capture,
|
|
208
|
+
:grpc_enable_fork_support, :tls_config, :healthcheck, :otel
|
|
209
|
+
|
|
210
|
+
# Initialize a new configuration instance
|
|
211
|
+
#
|
|
212
|
+
# Configuration values are loaded in the following priority order:
|
|
213
|
+
# 1. Explicit constructor options (highest priority)
|
|
214
|
+
# 2. Environment variables (HATCHET_CLIENT_*)
|
|
215
|
+
# 3. JWT token payload (for tenant_id from 'sub' field)
|
|
216
|
+
# 4. Default values (lowest priority)
|
|
217
|
+
#
|
|
218
|
+
# @param options [Hash] Configuration options
|
|
219
|
+
# @option options [String] :token JWT token for authentication (required)
|
|
220
|
+
# @option options [String] :tenant_id Override tenant ID (extracted from JWT 'sub' field if not provided)
|
|
221
|
+
# @option options [String] :host_port gRPC server host and port (default: "localhost:7070")
|
|
222
|
+
# @option options [String] :server_url Server URL for HTTP requests (default: "https://app.dev.hatchet-tools.com")
|
|
223
|
+
# @option options [String] :namespace Namespace prefix for resource names (default: "")
|
|
224
|
+
# @option options [Logger] :logger Custom logger instance (default: Logger.new($stdout))
|
|
225
|
+
# @option options [Integer] :listener_v2_timeout Timeout for listener v2 in milliseconds
|
|
226
|
+
# @option options [Integer] :grpc_max_recv_message_length Maximum gRPC receive message length in bytes (default: 4MB)
|
|
227
|
+
# @option options [Integer] :grpc_max_send_message_length Maximum gRPC send message length in bytes (default: 4MB)
|
|
228
|
+
# @option options [Hash<String, String>] :worker_preset_labels Hash of preset labels for workers
|
|
229
|
+
# @option options [Boolean] :enable_force_kill_sync_threads Enable force killing of sync threads (default: false)
|
|
230
|
+
# @option options [Boolean] :enable_thread_pool_monitoring Enable thread pool monitoring (default: false)
|
|
231
|
+
# @option options [Integer] :terminate_worker_after_num_tasks Terminate worker after this many tasks
|
|
232
|
+
# @option options [Boolean] :disable_log_capture Disable log capture (default: false)
|
|
233
|
+
# @option options [Boolean] :grpc_enable_fork_support Enable gRPC fork support (default: false)
|
|
234
|
+
# @option options [TLSConfig] :tls_config Custom TLS configuration
|
|
235
|
+
# @option options [HealthcheckConfig] :healthcheck Custom healthcheck configuration
|
|
236
|
+
# @option options [OpenTelemetryConfig] :otel Custom OpenTelemetry configuration
|
|
237
|
+
#
|
|
238
|
+
# @raise [Error] if token is missing, empty, or not a valid JWT
|
|
239
|
+
def initialize(**options)
|
|
240
|
+
load_env_files
|
|
241
|
+
@explicitly_set = options.keys.to_set
|
|
242
|
+
|
|
243
|
+
@token = options[:token] || env_var("HATCHET_CLIENT_TOKEN") || ""
|
|
244
|
+
@tenant_id = options[:tenant_id] || env_var("HATCHET_CLIENT_TENANT_ID") || ""
|
|
245
|
+
@host_port = options[:host_port] || env_var("HATCHET_CLIENT_HOST_PORT") || DEFAULT_HOST_PORT
|
|
246
|
+
@server_url = options[:server_url] || env_var("HATCHET_CLIENT_SERVER_URL") || DEFAULT_SERVER_URL
|
|
247
|
+
@namespace = options[:namespace] || env_var("HATCHET_CLIENT_NAMESPACE") || ""
|
|
248
|
+
@logger = options[:logger] || Logger.new($stdout)
|
|
249
|
+
|
|
250
|
+
@listener_v2_timeout = parse_int(options[:listener_v2_timeout] || env_var("HATCHET_CLIENT_LISTENER_V2_TIMEOUT"))
|
|
251
|
+
@grpc_max_recv_message_length = parse_int(
|
|
252
|
+
options[:grpc_max_recv_message_length] ||
|
|
253
|
+
env_var("HATCHET_CLIENT_GRPC_MAX_RECV_MESSAGE_LENGTH")
|
|
254
|
+
) || DEFAULT_GRPC_MAX_MESSAGE_LENGTH
|
|
255
|
+
@grpc_max_send_message_length = parse_int(
|
|
256
|
+
options[:grpc_max_send_message_length] ||
|
|
257
|
+
env_var("HATCHET_CLIENT_GRPC_MAX_SEND_MESSAGE_LENGTH")
|
|
258
|
+
) || DEFAULT_GRPC_MAX_MESSAGE_LENGTH
|
|
259
|
+
|
|
260
|
+
@worker_preset_labels = options[:worker_preset_labels] ||
|
|
261
|
+
parse_hash(env_var("HATCHET_CLIENT_WORKER_PRESET_LABELS")) || {}
|
|
262
|
+
@enable_force_kill_sync_threads = parse_bool(
|
|
263
|
+
options[:enable_force_kill_sync_threads] ||
|
|
264
|
+
env_var("HATCHET_CLIENT_ENABLE_FORCE_KILL_SYNC_THREADS")
|
|
265
|
+
) || false
|
|
266
|
+
@enable_thread_pool_monitoring = parse_bool(
|
|
267
|
+
options[:enable_thread_pool_monitoring] ||
|
|
268
|
+
env_var("HATCHET_CLIENT_ENABLE_THREAD_POOL_MONITORING")
|
|
269
|
+
) || false
|
|
270
|
+
@terminate_worker_after_num_tasks = parse_int(
|
|
271
|
+
options[:terminate_worker_after_num_tasks] ||
|
|
272
|
+
env_var("HATCHET_CLIENT_TERMINATE_WORKER_AFTER_NUM_TASKS")
|
|
273
|
+
)
|
|
274
|
+
@disable_log_capture = parse_bool(
|
|
275
|
+
options[:disable_log_capture] ||
|
|
276
|
+
env_var("HATCHET_CLIENT_DISABLE_LOG_CAPTURE")
|
|
277
|
+
) || false
|
|
278
|
+
@grpc_enable_fork_support = parse_bool(
|
|
279
|
+
options[:grpc_enable_fork_support] ||
|
|
280
|
+
env_var("HATCHET_CLIENT_GRPC_ENABLE_FORK_SUPPORT")
|
|
281
|
+
) || false
|
|
282
|
+
|
|
283
|
+
# Initialize nested configurations
|
|
284
|
+
@tls_config = options[:tls_config] || TLSConfig.new
|
|
285
|
+
@healthcheck = options[:healthcheck] || HealthcheckConfig.new
|
|
286
|
+
@otel = options[:otel] || OpenTelemetryConfig.new
|
|
287
|
+
|
|
288
|
+
validate!
|
|
289
|
+
apply_token_defaults if valid_jwt_token?
|
|
290
|
+
apply_address_defaults if valid_jwt_token?
|
|
291
|
+
normalize_namespace!
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def validate!
|
|
295
|
+
raise Error, "Hatchet Token is required. Please set HATCHET_CLIENT_TOKEN in your environment." if token.nil? || token.empty?
|
|
296
|
+
|
|
297
|
+
return if valid_jwt_token?
|
|
298
|
+
|
|
299
|
+
raise Error,
|
|
300
|
+
"Hatchet Token must be a valid JWT."
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Apply namespace prefix to a resource name
|
|
304
|
+
#
|
|
305
|
+
# @param resource_name [String, nil] The resource name to namespace
|
|
306
|
+
# @param namespace_override [String, nil] Optional namespace to use instead of the configured one
|
|
307
|
+
# @return [String, nil] The namespaced resource name, or nil if resource_name is nil
|
|
308
|
+
#
|
|
309
|
+
# @example Apply default namespace
|
|
310
|
+
# config = Config.new(token: "token", namespace: "prod")
|
|
311
|
+
# config.apply_namespace("workflow") #=> "prod_workflow"
|
|
312
|
+
#
|
|
313
|
+
# @example Apply custom namespace
|
|
314
|
+
# config.apply_namespace("workflow", namespace_override: "staging_") #=> "staging_workflow"
|
|
315
|
+
#
|
|
316
|
+
# @example Skip namespace if already present
|
|
317
|
+
# config.apply_namespace("prod_workflow") #=> "prod_workflow"
|
|
318
|
+
def apply_namespace(resource_name, namespace_override: nil)
|
|
319
|
+
return resource_name if resource_name.nil?
|
|
320
|
+
|
|
321
|
+
namespace_to_use = namespace_override || namespace
|
|
322
|
+
return resource_name if namespace_to_use.empty?
|
|
323
|
+
return resource_name if resource_name.start_with?(namespace_to_use)
|
|
324
|
+
|
|
325
|
+
"#{namespace_to_use}#{resource_name}"
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def hash
|
|
329
|
+
to_h.hash
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Convert configuration to a hash representation
|
|
333
|
+
#
|
|
334
|
+
# @return [Hash<Symbol, Object>] Hash containing all configuration values
|
|
335
|
+
def to_h
|
|
336
|
+
{
|
|
337
|
+
token: token,
|
|
338
|
+
tenant_id: tenant_id,
|
|
339
|
+
host_port: host_port,
|
|
340
|
+
server_url: server_url,
|
|
341
|
+
namespace: namespace,
|
|
342
|
+
listener_v2_timeout: listener_v2_timeout,
|
|
343
|
+
grpc_max_recv_message_length: grpc_max_recv_message_length,
|
|
344
|
+
grpc_max_send_message_length: grpc_max_send_message_length,
|
|
345
|
+
worker_preset_labels: worker_preset_labels,
|
|
346
|
+
enable_force_kill_sync_threads: enable_force_kill_sync_threads,
|
|
347
|
+
enable_thread_pool_monitoring: enable_thread_pool_monitoring,
|
|
348
|
+
terminate_worker_after_num_tasks: terminate_worker_after_num_tasks,
|
|
349
|
+
disable_log_capture: disable_log_capture,
|
|
350
|
+
grpc_enable_fork_support: grpc_enable_fork_support
|
|
351
|
+
}
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
def valid_jwt_token?
|
|
357
|
+
!token.nil? && !token.empty? && token.start_with?("ey")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def apply_token_defaults
|
|
361
|
+
return unless tenant_id.empty?
|
|
362
|
+
|
|
363
|
+
extracted_tenant_id = extract_tenant_id_from_jwt
|
|
364
|
+
@tenant_id = extracted_tenant_id || ""
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def apply_address_defaults
|
|
368
|
+
jwt_server_url, jwt_host_port = extract_addresses_from_jwt
|
|
369
|
+
|
|
370
|
+
@host_port = jwt_host_port if jwt_host_port && !explicitly_set?(:host_port)
|
|
371
|
+
@server_url = jwt_server_url if jwt_server_url && !explicitly_set?(:server_url)
|
|
372
|
+
|
|
373
|
+
# Set TLS server name if not already set
|
|
374
|
+
return unless tls_config.server_name.empty?
|
|
375
|
+
|
|
376
|
+
tls_config.server_name = host_port.split(":").first || "localhost"
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def normalize_namespace!
|
|
380
|
+
return if namespace.empty?
|
|
381
|
+
|
|
382
|
+
@namespace = namespace.downcase
|
|
383
|
+
@namespace += "_" unless @namespace.end_with?("_")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def load_env_files
|
|
387
|
+
ENV_FILE_NAMES.each do |env_file|
|
|
388
|
+
next unless File.exist?(env_file)
|
|
389
|
+
|
|
390
|
+
File.foreach(env_file) do |line|
|
|
391
|
+
line = line.strip
|
|
392
|
+
next if line.empty? || line.start_with?("#")
|
|
393
|
+
|
|
394
|
+
key, value = line.split("=", 2)
|
|
395
|
+
next unless key && value
|
|
396
|
+
|
|
397
|
+
ENV[key] ||= value.gsub(/\A['"]|['"]\z/, "") # Remove surrounding quotes
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def env_var(name)
|
|
403
|
+
ENV[name]
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def parse_int(value)
|
|
407
|
+
return nil if value.nil?
|
|
408
|
+
return value if value.is_a?(Integer)
|
|
409
|
+
return nil if value.respond_to?(:empty?) && value.empty?
|
|
410
|
+
|
|
411
|
+
Integer(value)
|
|
412
|
+
rescue ArgumentError
|
|
413
|
+
nil
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def parse_bool(value)
|
|
417
|
+
return nil if value.nil?
|
|
418
|
+
return value if [true, false].include?(value)
|
|
419
|
+
|
|
420
|
+
%w[true 1 yes on].include?(value.to_s.downcase)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def parse_hash(value)
|
|
424
|
+
return nil if value.nil? || value.empty?
|
|
425
|
+
|
|
426
|
+
# Simple key=value,key2=value2 parsing
|
|
427
|
+
result = {}
|
|
428
|
+
value.split(",").each do |pair|
|
|
429
|
+
key, val = pair.split("=", 2)
|
|
430
|
+
result[key.strip] = val&.strip if key && val
|
|
431
|
+
end
|
|
432
|
+
result
|
|
433
|
+
rescue StandardError
|
|
434
|
+
nil
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def explicitly_set?(attr)
|
|
438
|
+
@explicitly_set.include?(attr)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def extract_tenant_id_from_jwt
|
|
442
|
+
return nil unless valid_jwt_token?
|
|
443
|
+
|
|
444
|
+
payload = decode_jwt_payload
|
|
445
|
+
payload&.dig("sub")
|
|
446
|
+
rescue StandardError
|
|
447
|
+
nil
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def extract_addresses_from_jwt
|
|
451
|
+
return [nil, nil] unless valid_jwt_token?
|
|
452
|
+
|
|
453
|
+
payload = decode_jwt_payload
|
|
454
|
+
return [nil, nil] unless payload
|
|
455
|
+
|
|
456
|
+
server_url = payload["server_url"]
|
|
457
|
+
grpc_broadcast_address = payload["grpc_broadcast_address"]
|
|
458
|
+
|
|
459
|
+
[server_url, grpc_broadcast_address]
|
|
460
|
+
rescue StandardError
|
|
461
|
+
[nil, nil]
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def decode_jwt_payload
|
|
465
|
+
return nil unless valid_jwt_token?
|
|
466
|
+
|
|
467
|
+
# JWT has three parts separated by dots: header.payload.signature
|
|
468
|
+
parts = token.split(".")
|
|
469
|
+
return nil unless parts.length == 3
|
|
470
|
+
|
|
471
|
+
# Decode the payload (second part)
|
|
472
|
+
payload_part = parts[1]
|
|
473
|
+
# Add padding if needed for Base64 decoding
|
|
474
|
+
payload_part += "=" * (4 - payload_part.length % 4) if payload_part.length % 4 != 0
|
|
475
|
+
|
|
476
|
+
decoded_payload = Base64.decode64(payload_part)
|
|
477
|
+
JSON.parse(decoded_payload)
|
|
478
|
+
rescue StandardError
|
|
479
|
+
nil
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
data/lib/hatchet/version.rb
CHANGED
data/lib/hatchet-sdk.rb
CHANGED
|
@@ -1,15 +1,65 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
2
3
|
|
|
3
4
|
require_relative "hatchet/version"
|
|
5
|
+
require_relative "hatchet/config"
|
|
4
6
|
|
|
7
|
+
# Ruby SDK for Hatchet workflow engine
|
|
8
|
+
#
|
|
9
|
+
# @see https://docs.hatchet.run for Hatchet documentation
|
|
5
10
|
module Hatchet
|
|
11
|
+
# Base error class for all Hatchet-related errors
|
|
6
12
|
class Error < StandardError; end
|
|
7
13
|
|
|
14
|
+
# The main client for interacting with Hatchet services.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic usage with API token
|
|
17
|
+
# hatchet = Hatchet::Client.new()
|
|
18
|
+
#
|
|
19
|
+
# @example With custom configuration
|
|
20
|
+
# hatchet = Hatchet::Client.new(
|
|
21
|
+
# token: "your-jwt-token",
|
|
22
|
+
# namespace: "production"
|
|
23
|
+
# )
|
|
8
24
|
class Client
|
|
9
|
-
|
|
25
|
+
# @return [Config] The configuration object used by this client
|
|
26
|
+
attr_reader :config
|
|
10
27
|
|
|
11
|
-
|
|
12
|
-
|
|
28
|
+
# Initialize a new Hatchet client with the given configuration options.
|
|
29
|
+
#
|
|
30
|
+
# @param options [Hash] Configuration options for the client
|
|
31
|
+
# @option options [String] :token The JWT token for authentication (required)
|
|
32
|
+
# @option options [String] :tenant_id Override tenant ID (extracted from JWT token 'sub' field if not provided)
|
|
33
|
+
# @option options [String] :host_port gRPC server host and port (default: "localhost:7070")
|
|
34
|
+
# @option options [String] :server_url Server URL for HTTP requests (default: "https://app.dev.hatchet-tools.com")
|
|
35
|
+
# @option options [String] :namespace Namespace prefix for resource names (default: "")
|
|
36
|
+
# @option options [Logger] :logger Custom logger instance (default: Logger.new($stdout))
|
|
37
|
+
# @option options [Integer] :listener_v2_timeout Timeout for listener v2 in milliseconds
|
|
38
|
+
# @option options [Integer] :grpc_max_recv_message_length Maximum gRPC receive message length (default: 4MB)
|
|
39
|
+
# @option options [Integer] :grpc_max_send_message_length Maximum gRPC send message length (default: 4MB)
|
|
40
|
+
# @option options [Hash] :worker_preset_labels Hash of preset labels for workers
|
|
41
|
+
# @option options [Boolean] :enable_force_kill_sync_threads Enable force killing of sync threads (default: false)
|
|
42
|
+
# @option options [Boolean] :enable_thread_pool_monitoring Enable thread pool monitoring (default: false)
|
|
43
|
+
# @option options [Integer] :terminate_worker_after_num_tasks Terminate worker after this many tasks
|
|
44
|
+
# @option options [Boolean] :disable_log_capture Disable log capture (default: false)
|
|
45
|
+
# @option options [Boolean] :grpc_enable_fork_support Enable gRPC fork support (default: false)
|
|
46
|
+
# @option options [TLSConfig] :tls_config Custom TLS configuration
|
|
47
|
+
# @option options [HealthcheckConfig] :healthcheck Custom healthcheck configuration
|
|
48
|
+
# @option options [OpenTelemetryConfig] :otel Custom OpenTelemetry configuration
|
|
49
|
+
#
|
|
50
|
+
# @raise [Error] if token or configuration is missing or invalid
|
|
51
|
+
#
|
|
52
|
+
# @example Initialize with minimal configuration
|
|
53
|
+
# client = Hatchet::Client.new()
|
|
54
|
+
#
|
|
55
|
+
# @example Initialize with custom options
|
|
56
|
+
# client = Hatchet::Client.new(
|
|
57
|
+
# token: "eyJhbGciOiJIUzI1NiJ9...",
|
|
58
|
+
# namespace: "my_app",
|
|
59
|
+
# worker_preset_labels: { "env" => "production", "version" => "1.0.0" }
|
|
60
|
+
# )
|
|
61
|
+
def initialize(**options)
|
|
62
|
+
@config = Config.new(**options)
|
|
13
63
|
end
|
|
14
64
|
end
|
|
15
65
|
end
|
data/sig/hatchet-sdk.rbs
CHANGED
|
@@ -1,4 +1,106 @@
|
|
|
1
1
|
module Hatchet
|
|
2
2
|
VERSION: String
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Client
|
|
8
|
+
attr_reader config: Config
|
|
9
|
+
|
|
10
|
+
def initialize: (
|
|
11
|
+
?token: String,
|
|
12
|
+
?tenant_id: String,
|
|
13
|
+
?host_port: String,
|
|
14
|
+
?server_url: String,
|
|
15
|
+
?namespace: String,
|
|
16
|
+
?logger: Logger,
|
|
17
|
+
?listener_v2_timeout: Integer,
|
|
18
|
+
?grpc_max_recv_message_length: Integer,
|
|
19
|
+
?grpc_max_send_message_length: Integer,
|
|
20
|
+
?worker_preset_labels: Hash[String, String],
|
|
21
|
+
?enable_force_kill_sync_threads: bool,
|
|
22
|
+
?enable_thread_pool_monitoring: bool,
|
|
23
|
+
?terminate_worker_after_num_tasks: Integer,
|
|
24
|
+
?disable_log_capture: bool,
|
|
25
|
+
?grpc_enable_fork_support: bool,
|
|
26
|
+
?tls_config: TLSConfig,
|
|
27
|
+
?healthcheck: HealthcheckConfig,
|
|
28
|
+
?otel: OpenTelemetryConfig
|
|
29
|
+
) -> void
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Config
|
|
33
|
+
attr_accessor token: String
|
|
34
|
+
attr_accessor tenant_id: String
|
|
35
|
+
attr_accessor host_port: String
|
|
36
|
+
attr_accessor server_url: String
|
|
37
|
+
attr_accessor namespace: String
|
|
38
|
+
attr_accessor logger: Logger
|
|
39
|
+
attr_accessor listener_v2_timeout: Integer?
|
|
40
|
+
attr_accessor grpc_max_recv_message_length: Integer
|
|
41
|
+
attr_accessor grpc_max_send_message_length: Integer
|
|
42
|
+
attr_accessor worker_preset_labels: Hash[String, String]
|
|
43
|
+
attr_accessor enable_force_kill_sync_threads: bool
|
|
44
|
+
attr_accessor enable_thread_pool_monitoring: bool
|
|
45
|
+
attr_accessor terminate_worker_after_num_tasks: Integer?
|
|
46
|
+
attr_accessor disable_log_capture: bool
|
|
47
|
+
attr_accessor grpc_enable_fork_support: bool
|
|
48
|
+
attr_accessor tls_config: TLSConfig
|
|
49
|
+
attr_accessor healthcheck: HealthcheckConfig
|
|
50
|
+
attr_accessor otel: OpenTelemetryConfig
|
|
51
|
+
|
|
52
|
+
def initialize: (
|
|
53
|
+
?token: String,
|
|
54
|
+
?tenant_id: String,
|
|
55
|
+
?host_port: String,
|
|
56
|
+
?server_url: String,
|
|
57
|
+
?namespace: String,
|
|
58
|
+
?logger: Logger,
|
|
59
|
+
?listener_v2_timeout: Integer,
|
|
60
|
+
?grpc_max_recv_message_length: Integer,
|
|
61
|
+
?grpc_max_send_message_length: Integer,
|
|
62
|
+
?worker_preset_labels: Hash[String, String],
|
|
63
|
+
?enable_force_kill_sync_threads: bool,
|
|
64
|
+
?enable_thread_pool_monitoring: bool,
|
|
65
|
+
?terminate_worker_after_num_tasks: Integer,
|
|
66
|
+
?disable_log_capture: bool,
|
|
67
|
+
?grpc_enable_fork_support: bool,
|
|
68
|
+
?tls_config: TLSConfig,
|
|
69
|
+
?healthcheck: HealthcheckConfig,
|
|
70
|
+
?otel: OpenTelemetryConfig
|
|
71
|
+
) -> void
|
|
72
|
+
|
|
73
|
+
def apply_namespace: (String? resource_name, ?namespace_override: String?) -> String?
|
|
74
|
+
def hash: () -> Integer
|
|
75
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class TLSConfig
|
|
79
|
+
attr_accessor strategy: String
|
|
80
|
+
attr_accessor cert_file: String?
|
|
81
|
+
attr_accessor key_file: String?
|
|
82
|
+
attr_accessor root_ca_file: String?
|
|
83
|
+
attr_accessor server_name: String
|
|
84
|
+
|
|
85
|
+
def initialize: (
|
|
86
|
+
?strategy: String,
|
|
87
|
+
?cert_file: String,
|
|
88
|
+
?key_file: String,
|
|
89
|
+
?root_ca_file: String,
|
|
90
|
+
?server_name: String
|
|
91
|
+
) -> void
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class HealthcheckConfig
|
|
95
|
+
attr_accessor port: Integer
|
|
96
|
+
attr_accessor enabled: bool
|
|
97
|
+
|
|
98
|
+
def initialize: (?port: Integer, ?enabled: bool) -> void
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class OpenTelemetryConfig
|
|
102
|
+
attr_accessor excluded_attributes: Array[String]
|
|
103
|
+
|
|
104
|
+
def initialize: (?excluded_attributes: Array[String]) -> void
|
|
105
|
+
end
|
|
106
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hatchet-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- gabriel ruttner
|
|
@@ -54,6 +54,7 @@ files:
|
|
|
54
54
|
- README.md
|
|
55
55
|
- Rakefile
|
|
56
56
|
- lib/hatchet-sdk.rb
|
|
57
|
+
- lib/hatchet/config.rb
|
|
57
58
|
- lib/hatchet/version.rb
|
|
58
59
|
- sig/hatchet-sdk.rbs
|
|
59
60
|
homepage: https://hatchet.run
|