eywa-client 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +370 -0
  4. data/lib/eywa.rb +242 -0
  5. metadata +92 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3cb389c11a653f8a4d9b9e73d49619c2438b62dd12dd61ce0f523541d348f24d
4
+ data.tar.gz: 54cfff0b41310d46354f93ea02f6f9ff022f260e7afff99398533fe21782fe88
5
+ SHA512:
6
+ metadata.gz: d4e3e0bbced5016f5b293222a6e8761b0b2f9f7fd13e2fd0e862fbe9b705db9887ad649c47a2a073ef4ee07f2f5fa5f0e08d945021f71a0500d1940958770802
7
+ data.tar.gz: 689c54a748b37a357d7b8b3bc7d15816ebe313eca02e1c479a001aca06e2c5cc79822e32323cd99f6af0e7755cad1cb19cbdfd0b863d9ce084616370b0cd5a93
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Robert Gersak / Neyho
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,370 @@
1
+ # EYWA Client for Ruby
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/eywa-client.svg)](https://badge.fury.io/rb/eywa-client)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ EYWA client library for Ruby providing JSON-RPC communication, GraphQL queries, and task management for EYWA robots.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'eywa-client'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ ```bash
19
+ bundle install
20
+ ```
21
+
22
+ Or install it yourself as:
23
+
24
+ ```bash
25
+ gem install eywa-client
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```ruby
31
+ require 'eywa'
32
+
33
+ # Initialize the client
34
+ open_pipe
35
+
36
+ # Log messages
37
+ info("Robot started")
38
+
39
+ # Execute GraphQL queries
40
+ result_thread = graphql('
41
+ {
42
+ searchUser(_limit: 10) {
43
+ euuid
44
+ name
45
+ type
46
+ }
47
+ }
48
+ ')
49
+
50
+ result = result_thread.value
51
+ info("Users found", result)
52
+
53
+ # Update task status
54
+ update_task(PROCESSING)
55
+
56
+ # Complete the task
57
+ close_task(SUCCESS)
58
+ ```
59
+
60
+ ## Features
61
+
62
+ - 🚀 **Thread-Based Async** - Ruby native threading for async operations
63
+ - 📊 **GraphQL Integration** - Execute queries and mutations against EYWA datasets
64
+ - 📝 **Comprehensive Logging** - Multiple log levels with metadata support
65
+ - 🔄 **Task Management** - Update status, report progress, handle task lifecycle
66
+ - 🎯 **Thread-Safe** - Mutex protection for concurrent operations
67
+ - 💎 **Ruby Idioms** - Keyword arguments and blocks for handlers
68
+
69
+ ## API Reference
70
+
71
+ ### Initialization
72
+
73
+ #### `open_pipe`
74
+ Initialize stdin/stdout communication with EYWA runtime. Must be called before using other functions.
75
+
76
+ ```ruby
77
+ open_pipe
78
+ ```
79
+
80
+ ### Logging Functions
81
+
82
+ #### `log(event: "INFO", message:, data: nil, duration: nil, coordinates: nil, time: Time.now)`
83
+ Log a message with full control over all parameters.
84
+
85
+ ```ruby
86
+ log(
87
+ event: "INFO",
88
+ message: "Processing item",
89
+ data: { item_id: 123 },
90
+ duration: 1500,
91
+ coordinates: { x: 10, y: 20 }
92
+ )
93
+ ```
94
+
95
+ #### `info()`, `error()`, `warn()`, `debug()`, `trace()`, `exception()`
96
+ Convenience methods for different log levels.
97
+
98
+ ```ruby
99
+ info("User logged in", { user_id: "abc123" })
100
+ error("Failed to process", { error: e.message })
101
+ exception("Unhandled error", { stack: e.backtrace[0..5] })
102
+ ```
103
+
104
+ ### Task Management
105
+
106
+ #### `get_task`
107
+ Get current task information. Returns a Thread.
108
+
109
+ ```ruby
110
+ task_thread = get_task
111
+ begin
112
+ task = task_thread.value
113
+ info("Processing task", { task_id: task["euuid"] })
114
+ rescue => e
115
+ warn("Could not get task", { error: e.message })
116
+ end
117
+ ```
118
+
119
+ #### `update_task(status = PROCESSING)`
120
+ Update the current task status.
121
+
122
+ ```ruby
123
+ update_task(PROCESSING)
124
+ ```
125
+
126
+ #### `close_task(status = SUCCESS)`
127
+ Close the task with a final status and exit the process.
128
+
129
+ ```ruby
130
+ begin
131
+ # Do work...
132
+ close_task(SUCCESS)
133
+ rescue => e
134
+ error("Task failed", { error: e.message })
135
+ close_task(ERROR)
136
+ end
137
+ ```
138
+
139
+ #### `return_task`
140
+ Return control to EYWA without closing the task.
141
+
142
+ ```ruby
143
+ return_task
144
+ ```
145
+
146
+ ### Reporting
147
+
148
+ #### `report(message, data = nil, image = nil)`
149
+ Send a task report with optional data and image.
150
+
151
+ ```ruby
152
+ report("Analysis complete", {
153
+ accuracy: 0.95,
154
+ processed: 1000
155
+ }, chart_image_base64)
156
+ ```
157
+
158
+ ### GraphQL
159
+
160
+ #### `graphql(query, variables = nil)`
161
+ Execute a GraphQL query against the EYWA server. Returns a Thread.
162
+
163
+ ```ruby
164
+ # Simple query
165
+ thread = graphql('{ searchUser { name email } }')
166
+ result = thread.value
167
+
168
+ # Query with variables
169
+ thread = graphql('
170
+ mutation CreateUser($input: UserInput!) {
171
+ syncUser(data: $input) {
172
+ euuid
173
+ name
174
+ }
175
+ }
176
+ ', {
177
+ input: {
178
+ name: "John Doe",
179
+ active: true
180
+ }
181
+ })
182
+
183
+ begin
184
+ result = thread.value
185
+ info("User created", result)
186
+ rescue => e
187
+ error("Creation failed", { error: e.message })
188
+ end
189
+ ```
190
+
191
+ ### JSON-RPC
192
+
193
+ #### `send_request(data)`
194
+ Send a JSON-RPC request and get a Thread for the response.
195
+
196
+ ```ruby
197
+ thread = send_request({
198
+ "method" => "custom.method",
199
+ "params" => { "foo" => "bar" }
200
+ })
201
+
202
+ begin
203
+ result = thread.value
204
+ info("Response received", result)
205
+ rescue => e
206
+ error("Request failed", { error: e.message })
207
+ end
208
+ ```
209
+
210
+ #### `send_notification(data)`
211
+ Send a JSON-RPC notification without expecting a response.
212
+
213
+ ```ruby
214
+ send_notification({
215
+ "method" => "custom.event",
216
+ "params" => { "status" => "ready" }
217
+ })
218
+ ```
219
+
220
+ #### `register_handler(method, &handler)`
221
+ Register a handler for incoming JSON-RPC method calls.
222
+
223
+ ```ruby
224
+ register_handler("custom.ping") do |request|
225
+ info("Received ping", request["params"])
226
+ send_notification({
227
+ "method" => "custom.pong",
228
+ "params" => { "timestamp" => Time.now.to_i }
229
+ })
230
+ end
231
+ ```
232
+
233
+ ## Module Structure
234
+
235
+ The library uses a modular structure with a global client for ease of use:
236
+
237
+ ```ruby
238
+ # Direct usage (recommended)
239
+ info("Hello")
240
+
241
+ # Or use the client instance
242
+ Eywa.client.info("Hello")
243
+
244
+ # Access constants
245
+ SUCCESS # => "SUCCESS"
246
+ ERROR # => "ERROR"
247
+ PROCESSING # => "PROCESSING"
248
+ EXCEPTION # => "EXCEPTION"
249
+ ```
250
+
251
+ ## Complete Example
252
+
253
+ ```ruby
254
+ #!/usr/bin/env ruby
255
+
256
+ require 'eywa'
257
+
258
+ def process_data
259
+ # Get task
260
+ task_thread = get_task
261
+ task = task_thread.value
262
+
263
+ info("Starting task", {
264
+ task_id: task["euuid"],
265
+ message: task["message"]
266
+ })
267
+
268
+ # Update status
269
+ update_task(PROCESSING)
270
+
271
+ # Query data
272
+ result_thread = graphql('
273
+ query GetActiveUsers {
274
+ searchUser(_where: {active: {_eq: true}}) {
275
+ euuid
276
+ name
277
+ email
278
+ }
279
+ }
280
+ ')
281
+
282
+ result = result_thread.value
283
+ users = result.dig("data", "searchUser") || []
284
+
285
+ info("Found users", { count: users.length })
286
+
287
+ # Process users
288
+ users.each do |user|
289
+ debug("Processing user", { user_id: user["euuid"] })
290
+ # ... do something
291
+ end
292
+
293
+ # Report results
294
+ report("Found active users", {
295
+ count: users.length,
296
+ user_names: users.map { |u| u["name"] }
297
+ })
298
+
299
+ info("Task completed")
300
+ rescue => e
301
+ error("Task processing failed", {
302
+ error: e.message,
303
+ backtrace: e.backtrace[0..5]
304
+ })
305
+ raise
306
+ end
307
+
308
+ def main
309
+ # Initialize
310
+ open_pipe
311
+ sleep(0.1)
312
+
313
+ info("Robot started")
314
+
315
+ begin
316
+ process_data
317
+ close_task(SUCCESS)
318
+ rescue => e
319
+ error("Task failed", { error: e.message })
320
+ close_task(ERROR)
321
+ end
322
+ end
323
+
324
+ main
325
+ ```
326
+
327
+ ## Error Handling
328
+
329
+ Threads return exceptions that can be caught:
330
+
331
+ ```ruby
332
+ thread = graphql("{ invalid }")
333
+ begin
334
+ result = thread.value
335
+ rescue => e
336
+ error("GraphQL failed", { error: e.message })
337
+ end
338
+ ```
339
+
340
+ ## Thread Safety
341
+
342
+ All operations are thread-safe:
343
+ - Mutex protection for callbacks and handlers
344
+ - Thread-safe Queue for request/response correlation
345
+ - Safe concurrent access to shared state
346
+
347
+ ## Testing
348
+
349
+ Test your robot locally using the EYWA CLI:
350
+
351
+ ```bash
352
+ eywa run -c 'ruby my_robot.rb'
353
+ ```
354
+
355
+ ## Requirements
356
+
357
+ - Ruby 2.5+
358
+ - No external gem dependencies (uses only standard library)
359
+
360
+ ## License
361
+
362
+ MIT
363
+
364
+ ## Contributing
365
+
366
+ Contributions are welcome! Please feel free to submit a Pull Request.
367
+
368
+ ## Support
369
+
370
+ For issues and questions, please visit the [EYWA repository](https://github.com/neyho/eywa).
data/lib/eywa.rb ADDED
@@ -0,0 +1,242 @@
1
+ require 'securerandom'
2
+ require 'json'
3
+ require 'time'
4
+
5
+ module Eywa
6
+ # Version
7
+ VERSION = "0.3.0"
8
+
9
+ # Task status constants
10
+ SUCCESS = 'SUCCESS'
11
+ ERROR = 'ERROR'
12
+ PROCESSING = 'PROCESSING'
13
+ EXCEPTION = 'EXCEPTION'
14
+
15
+ class Client
16
+ def initialize
17
+ @rpc_callbacks = {}
18
+ @handlers = {}
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ def open_pipe
23
+ Thread.new do
24
+ while (line = STDIN.gets)
25
+ begin
26
+ json = JSON.parse(line)
27
+ handle_data(json)
28
+ rescue JSON::ParserError => e
29
+ STDERR.puts("Failed to parse JSON: #{e.message}")
30
+ rescue => e
31
+ STDERR.puts("Error handling data: #{e.message}")
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def send_request(data)
38
+ id = SecureRandom.uuid
39
+ data['jsonrpc'] = '2.0'
40
+ data['id'] = id
41
+
42
+ # Create a queue for this request
43
+ queue = Queue.new
44
+
45
+ @mutex.synchronize do
46
+ @rpc_callbacks[id] = queue
47
+ end
48
+
49
+ # Send the request
50
+ STDOUT.puts(data.to_json)
51
+ STDOUT.flush
52
+
53
+ # Wait for response in a thread
54
+ Thread.new do
55
+ response = queue.pop
56
+ @mutex.synchronize do
57
+ @rpc_callbacks.delete(id)
58
+ end
59
+
60
+ if response['error']
61
+ raise StandardError.new(response['error']['message'] || response['error'].to_s)
62
+ else
63
+ response['result']
64
+ end
65
+ end
66
+ end
67
+
68
+ def send_notification(data)
69
+ data['jsonrpc'] = '2.0'
70
+ STDOUT.puts(data.to_json)
71
+ STDOUT.flush
72
+ end
73
+
74
+ def register_handler(method, &handler)
75
+ @mutex.synchronize do
76
+ @handlers[method] = handler
77
+ end
78
+ end
79
+
80
+ def log(event: 'INFO', message:, data: nil, duration: nil, coordinates: nil, time: Time.now)
81
+ send_notification(
82
+ 'method' => 'task.log',
83
+ 'params' => {
84
+ 'time' => time.iso8601,
85
+ 'event' => event,
86
+ 'message' => message,
87
+ 'data' => data,
88
+ 'coordinates' => coordinates,
89
+ 'duration' => duration
90
+ }
91
+ )
92
+ end
93
+
94
+ def info(message, data = nil)
95
+ log(event: 'INFO', message: message, data: data)
96
+ end
97
+
98
+ def error(message, data = nil)
99
+ log(event: 'ERROR', message: message, data: data)
100
+ end
101
+
102
+ def warn(message, data = nil)
103
+ log(event: 'WARN', message: message, data: data)
104
+ end
105
+
106
+ def debug(message, data = nil)
107
+ log(event: 'DEBUG', message: message, data: data)
108
+ end
109
+
110
+ def trace(message, data = nil)
111
+ log(event: 'TRACE', message: message, data: data)
112
+ end
113
+
114
+ def exception(message, data = nil)
115
+ log(event: 'EXCEPTION', message: message, data: data)
116
+ end
117
+
118
+ def report(message, data = nil, image = nil)
119
+ send_notification(
120
+ 'method' => 'task.report',
121
+ 'params' => {
122
+ 'message' => message,
123
+ 'data' => data,
124
+ 'image' => image
125
+ }
126
+ )
127
+ end
128
+
129
+ def close_task(status = SUCCESS)
130
+ send_notification(
131
+ 'method' => 'task.close',
132
+ 'params' => {
133
+ 'status' => status
134
+ }
135
+ )
136
+
137
+ exit(status == SUCCESS ? 0 : 1)
138
+ end
139
+
140
+ def update_task(status = PROCESSING)
141
+ send_notification(
142
+ 'method' => 'task.update',
143
+ 'params' => {
144
+ 'status' => status
145
+ }
146
+ )
147
+ end
148
+
149
+ def get_task
150
+ send_request('method' => 'task.get')
151
+ end
152
+
153
+ def return_task
154
+ send_notification('method' => 'task.return')
155
+ exit(0)
156
+ end
157
+
158
+ def graphql(query, variables = nil)
159
+ send_request(
160
+ 'method' => 'eywa.datasets.graphql',
161
+ 'params' => {
162
+ 'query' => query,
163
+ 'variables' => variables
164
+ }
165
+ )
166
+ end
167
+
168
+ private
169
+
170
+ def handle_data(data)
171
+ method = data['method']
172
+ id = data['id']
173
+ result = data['result']
174
+ error = data['error']
175
+
176
+ if method
177
+ handle_request(data)
178
+ elsif (result || error) && id
179
+ handle_response(data)
180
+ else
181
+ STDERR.puts("Received invalid JSON-RPC:\n#{data}")
182
+ end
183
+ end
184
+
185
+ def handle_request(data)
186
+ method = data['method']
187
+
188
+ @mutex.synchronize do
189
+ handler = @handlers[method]
190
+ if handler
191
+ handler.call(data)
192
+ else
193
+ STDERR.puts("Method #{method} doesn't have registered handler")
194
+ end
195
+ end
196
+ end
197
+
198
+ def handle_response(data)
199
+ id = data['id']
200
+
201
+ @mutex.synchronize do
202
+ queue = @rpc_callbacks[id]
203
+ if queue
204
+ queue.push(data)
205
+ else
206
+ STDERR.puts("RPC callback not registered for request with id = #{id}")
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+ # Convenience module for global access
213
+ module GlobalClient
214
+ extend self
215
+
216
+ def client
217
+ @client ||= Client.new
218
+ end
219
+
220
+ # Delegate all methods to the client instance
221
+ def method_missing(method_name, *args, **kwargs, &block)
222
+ if client.respond_to?(method_name)
223
+ client.send(method_name, *args, **kwargs, &block)
224
+ else
225
+ super
226
+ end
227
+ end
228
+
229
+ def respond_to_missing?(method_name, include_private = false)
230
+ client.respond_to?(method_name, include_private) || super
231
+ end
232
+ end
233
+ end
234
+
235
+ # For backward compatibility and ease of use
236
+ include Eywa::GlobalClient
237
+
238
+ # Export constants for easy access
239
+ SUCCESS = Eywa::SUCCESS
240
+ ERROR = Eywa::ERROR
241
+ PROCESSING = Eywa::PROCESSING
242
+ EXCEPTION = Eywa::EXCEPTION
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eywa-client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Robert Gersak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-06-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ description: A Ruby gem that provides a client for EYWA server with JSON-RPC communication,
56
+ GraphQL support, and task management.
57
+ email:
58
+ - robi@neyho.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE
64
+ - README.md
65
+ - lib/eywa.rb
66
+ homepage: https://github.com/neyho/eywa
67
+ licenses:
68
+ - MIT
69
+ metadata:
70
+ homepage_uri: https://github.com/neyho/eywa
71
+ source_code_uri: https://github.com/neyho/eywa
72
+ changelog_uri: https://github.com/neyho/eywa/blob/main/CHANGELOG.md
73
+ post_install_message:
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.5.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubygems_version: 3.0.3.1
89
+ signing_key:
90
+ specification_version: 4
91
+ summary: EYWA client for asynchronous communication with EYWA server
92
+ test_files: []