fal 0.0.1 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 294f1aa2fea312c2b0a42b2fe97753e45a8d8807083d5a60a3c739b7bdd722f6
4
- data.tar.gz: 93fb4193c178ffd22bfddeffab3926a4288afd89e42f9bd418d9a71ce45c2645
3
+ metadata.gz: f22dff48b2f5121df441220f3fffe41f3e548c8a2c229dd0f1c7d3c19072b74f
4
+ data.tar.gz: 8e5c092fb5246f8ea7ee88b80123e63d5ed7384f37b1cfc61474bcd3daef1431
5
5
  SHA512:
6
- metadata.gz: 3d328838cdf0dc3eff20b2275ecff9528d90bf8d549272b663f0b985b7fd2d3744e4a60e5b621a96a7a3f9a549b506e28f83776c5c84514d9d541fc376ed27d0
7
- data.tar.gz: 112f8673d40fd9195c1b157656e2f1c9dc8208e14c460207672cefd8e2264fac2f9857df17a11d2d174f9617c7771700f71e71ed797d6f1c820fcd9613cd472d
6
+ metadata.gz: '05697e66efb4f7d26fb110d707627755395244fa5a9fca86d3624e4bd6d33cae7a42ccbe625cd14dd8646b4a934496e2f169247a387ad046cf049ece487eb0d4'
7
+ data.tar.gz: b7bd9bf3bddb0f1a62ed61bb9d681a5531f4e00ff628cbc4a77b720e4367ef9a6d7a45253bb4777654e078c1e55e67ab6aaa95cebb2bf83ebc0e4a28df5b49c0
data/.env.example ADDED
@@ -0,0 +1 @@
1
+ FAL_KEY=
data/Gemfile CHANGED
@@ -7,3 +7,4 @@ gemspec
7
7
  gem "minitest", "~> 5.0"
8
8
  gem "rake", "~> 13.0"
9
9
  gem "rubocop", "~> 1.21"
10
+ gem "dotenv"
data/Gemfile.lock CHANGED
@@ -1,13 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fal (0.0.1)
4
+ fal (0.0.3)
5
5
  faraday (>= 1)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
10
  ast (2.4.2)
11
+ dotenv (3.1.8)
11
12
  faraday (2.14.0)
12
13
  faraday-net_http (>= 2.0, < 3.5)
13
14
  json
@@ -53,6 +54,7 @@ PLATFORMS
53
54
  x86_64-darwin-23
54
55
 
55
56
  DEPENDENCIES
57
+ dotenv
56
58
  fal!
57
59
  minitest (~> 5.0)
58
60
  rake (~> 13.0)
data/README.md CHANGED
@@ -9,3 +9,168 @@ Install the gem and add to the application"s Gemfile by executing:
9
9
  If bundler is not being used to manage dependencies, install the gem by executing:
10
10
 
11
11
  $ gem install fal
12
+
13
+ ## Usage
14
+
15
+ ### Configuration
16
+
17
+ Configure the client once at boot (e.g., in Rails an initializer) using `Fal.configure`.
18
+
19
+ ```ruby
20
+ Fal.configure do |config|
21
+ config.api_key = "your-key" # Optional. Defaults to ENV["FAL_KEY"] if not set.
22
+ config.queue_base = "https://queue.fal.run" # Optional (default: https://queue.fal.run)
23
+ config.sync_base = "https://fal.run" # Optional (default: https://fal.run)
24
+ config.request_timeout = 120 # Optional (default: 120)
25
+ end
26
+ ```
27
+
28
+ ### Create a queued request
29
+
30
+ The Queue API is the recommended way to call models on fal. Provide a `model_id` in "namespace/name" format and an input payload.
31
+
32
+ ```ruby
33
+ model_id = "fal-ai/fast-sdxl"
34
+
35
+ request = Fal::Request.create!(
36
+ model_id: model_id,
37
+ input: { prompt: "a cat" }
38
+ )
39
+
40
+ request.id # => request_id from fal
41
+ request.status # => "IN_QUEUE" | "IN_PROGRESS" | "COMPLETED"
42
+ ```
43
+
44
+ You can also specify a webhook URL to be notified when the request is finished.
45
+
46
+ ```ruby
47
+ request = Fal::Request.create!(
48
+ model_id: model_id,
49
+ input: { prompt: "a cat playing piano" },
50
+ webhook_url: "https://example.com/fal/webhook"
51
+ )
52
+ ```
53
+
54
+ ### Get request status (find and reload)
55
+
56
+ Fetch the current status by id:
57
+
58
+ ```ruby
59
+ status = Fal::Request.find_by!(id: request.id, model_id: model_id)
60
+ status.in_queue? # => true/false
61
+ status.in_progress? # => true/false
62
+ status.completed? # => true/false
63
+ ```
64
+
65
+ Reload an instance in-place, optionally including logs.
66
+
67
+ ```ruby
68
+ request.reload! # refreshes state
69
+ request.reload!(logs: true)
70
+ request.logs # => array of log entries (if provided by model and logs=1)
71
+ # When status is COMPLETED, reload! will also fetch and set request.response
72
+ ```
73
+
74
+ Status constants are available for direct comparisons:
75
+
76
+ ```ruby
77
+ Fal::Request::Status::IN_QUEUE
78
+ Fal::Request::Status::IN_PROGRESS
79
+ Fal::Request::Status::COMPLETED
80
+ ```
81
+
82
+ ### Fetch the response payload after completion
83
+
84
+ Call `reload!` to populate `request.response`:
85
+
86
+ ```ruby
87
+ # poll until completed
88
+ until request.completed?
89
+ request.reload!
90
+ sleep 1
91
+ end
92
+
93
+ request.response # => model-specific response body
94
+ ```
95
+
96
+ ### Cancel a request
97
+
98
+ Requests that are still in the queue can be cancelled:
99
+
100
+ ```ruby
101
+ request.cancel! # => { "status" => "CANCELLATION_REQUESTED" }
102
+ ```
103
+
104
+ ### Webhooks
105
+
106
+ fal can POST a webhook to your server when a request completes. Use `Fal::WebhookRequest` to parse the incoming payload.
107
+
108
+ ```ruby
109
+ # rails controller example
110
+ class FalWebhooksController < ApplicationController
111
+ skip_before_action :verify_authenticity_token
112
+
113
+ def create
114
+ webhook = Fal::WebhookRequest.from_rack_request(request)
115
+
116
+ if webhook.success?
117
+ # webhook.response contains the model-specific payload
118
+ # webhook.logs, webhook.metrics may also be present
119
+ head :ok
120
+ else
121
+ Rails.logger.error("fal webhook error: #{webhook.error} detail=#{webhook.error_detail}")
122
+ head :ok
123
+ end
124
+ end
125
+ end
126
+ ```
127
+
128
+ ### Error handling
129
+
130
+ HTTP and API errors raise typed exceptions:
131
+
132
+ - `Fal::UnauthorizedError` (401)
133
+ - `Fal::ForbiddenError` (403)
134
+ - `Fal::NotFoundError` (404)
135
+ - `Fal::ServerError` (other non-success)
136
+
137
+ Rescue them as needed:
138
+
139
+ ```ruby
140
+ begin
141
+ Fal::Request.create!(model_id: model_id, input: { prompt: "hi" })
142
+ rescue Fal::UnauthorizedError
143
+ # handle invalid/missing FAL_KEY
144
+ end
145
+ ```
146
+
147
+ ### Stream synchronous responses
148
+
149
+ Use `stream!` for SSE streaming from synchronous endpoints. It yields each chunk’s data Hash and returns a `Fal::Request` whose `response` contains the last chunk’s payload.
150
+
151
+ ```ruby
152
+ model_id = "fal-ai/flux/dev"
153
+
154
+ last = Fal::Request.stream!(model_id: model_id, input: { prompt: "a cat" }) do |chunk|
155
+ # chunk is a Hash, e.g. { images: [...] }
156
+ puts chunk
157
+ end
158
+
159
+ last.completed? # => true/false
160
+ last.response # => last streamed data hash (e.g., { "response" => { ... } } or final payload)
161
+ ```
162
+
163
+ ### Development
164
+
165
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
166
+
167
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to rubygems.org.
168
+
169
+ For local development, copy the example environment file and set your API key so `bin/console` can load it automatically:
170
+
171
+ ```
172
+ cp .env.example .env
173
+ echo 'FAL_KEY=your_api_key_here' >> .env
174
+ ```
175
+
176
+ The console uses dotenv to load `.env`, so `Fal.configure` will default to `ENV["FAL_KEY"]`.
data/lib/fal/client.rb ADDED
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fal
4
+ class Client
5
+ attr_accessor :configuration
6
+
7
+ def initialize(configuration = Fal.configuration)
8
+ @configuration = configuration
9
+ end
10
+
11
+ def post(path, payload = {}, headers: {})
12
+ response = connection.post(build_url(path)) do |request|
13
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
14
+ request.headers["Content-Type"] = "application/json"
15
+ request.headers["Accept"] = "application/json"
16
+ request.headers.merge!(headers)
17
+ request.body = payload.compact.to_json
18
+ end
19
+
20
+ handle_error(response) unless response.success?
21
+
22
+ parse_json(response.body)
23
+ end
24
+
25
+ # Perform a POST to the streaming (sync) base with SSE/text-event-stream handling.
26
+ # The provided on_data Proc will be used to receive chunked data.
27
+ # @param path [String]
28
+ # @param payload [Hash]
29
+ # @param on_data [Proc] called with chunks as they arrive
30
+ # @return [void]
31
+ def post_stream(path, payload = {}, on_data:)
32
+ url = build_sync_url(path)
33
+ connection.post(url) do |request|
34
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
35
+ request.headers["Accept"] = "text/event-stream"
36
+ request.headers["Cache-Control"] = "no-store"
37
+ request.headers["Content-Type"] = "application/json"
38
+ request.body = payload.compact.to_json
39
+ request.options.on_data = on_data
40
+ end
41
+ end
42
+
43
+ def get(path, query: nil, headers: {})
44
+ url = build_url(path)
45
+ url = "#{url}?#{URI.encode_www_form(query)}" if query && !query.empty?
46
+
47
+ response = connection.get(url) do |request|
48
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
49
+ request.headers["Accept"] = "application/json"
50
+ request.headers.merge!(headers)
51
+ end
52
+
53
+ handle_error(response) unless response.success?
54
+
55
+ parse_json(response.body)
56
+ end
57
+
58
+ def put(path)
59
+ response = connection.put(build_url(path)) do |request|
60
+ request.headers["Authorization"] = "Key #{@configuration.api_key}" if @configuration.api_key
61
+ request.headers["Content-Type"] = "application/json"
62
+ request.headers["Accept"] = "application/json"
63
+ request.body = {}.to_json
64
+ end
65
+
66
+ handle_error(response) unless response.success?
67
+
68
+ parse_json(response.body)
69
+ end
70
+
71
+ def handle_error(response)
72
+ case response.status
73
+ when 401
74
+ raise UnauthorizedError, response.body
75
+ when 403
76
+ raise ForbiddenError, response.body
77
+ when 404
78
+ raise NotFoundError, response.body
79
+ else
80
+ raise ServerError, response.body
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def parse_json(body)
87
+ return nil if body.nil? || body.strip.empty?
88
+
89
+ JSON.parse(body)
90
+ end
91
+
92
+ def build_url(path)
93
+ "#{@configuration.queue_base}#{path}"
94
+ end
95
+
96
+ def build_sync_url(path)
97
+ "#{@configuration.sync_base}#{path}"
98
+ end
99
+
100
+ def connection
101
+ Faraday.new do |faraday|
102
+ faraday.request :url_encoded
103
+ faraday.options.timeout = @configuration.request_timeout
104
+ faraday.options.open_timeout = @configuration.request_timeout
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fal
4
+ # Represents a queued request submitted to a fal model endpoint.
5
+ # Provides helpers to create, query status, cancel, and fetch response payloads
6
+ # using the Queue API described in the fal docs.
7
+ # See: https://docs.fal.ai/model-apis/model-endpoints/queue
8
+ class Request
9
+ # Request status values returned by the Queue API.
10
+ module Status
11
+ # @return [String]
12
+ IN_QUEUE = "IN_QUEUE"
13
+ # @return [String]
14
+ IN_PROGRESS = "IN_PROGRESS"
15
+ # @return [String]
16
+ COMPLETED = "COMPLETED"
17
+ end
18
+
19
+ # @return [String] The request identifier (request_id)
20
+ attr_reader :id
21
+ # @return [String] The current status, one of Fal::Request::Status constants
22
+ attr_reader :status
23
+ # @return [Integer, nil] The current position in the queue, if available
24
+ attr_reader :queue_position
25
+ # @return [Array<Hash>, nil] Log entries when requested via logs=1
26
+ attr_reader :logs
27
+ # @return [Hash, nil] Response payload when status is COMPLETED
28
+ attr_reader :response
29
+ # @return [String] The model identifier used when creating this request
30
+ attr_reader :model_id
31
+
32
+ # @param attributes [Hash] Raw attributes from fal Queue API
33
+ # @param model_id [String] Model ID in "namespace/name" format
34
+ # @param client [Fal::Client] HTTP client to use for subsequent calls
35
+ def initialize(attributes, model_id:, client: Fal.client)
36
+ @client = client
37
+ @model_id = model_id
38
+ reset_attributes(attributes)
39
+ end
40
+
41
+ class << self
42
+ # Create a new queued request for a model.
43
+ # Corresponds to POST https://queue.fal.run/{model_id}
44
+ # Optionally appends fal_webhook query param per docs.
45
+ # @param model_id [String]
46
+ # @param input [Hash]
47
+ # @param webhook_url [String, nil]
48
+ # @param client [Fal::Client]
49
+ # @return [Fal::Request]
50
+ def create!(model_id:, input:, webhook_url: nil, client: Fal.client)
51
+ path = "/#{model_id}"
52
+ body = input || {}
53
+ path = "#{path}?fal_webhook=#{CGI.escape(webhook_url)}" if webhook_url
54
+ attrs = client.post(path, body)
55
+ new(attrs, model_id: model_id, client: client)
56
+ end
57
+
58
+ # Find the current status for a given request.
59
+ # Corresponds to GET https://queue.fal.run/{model_id}/requests/{request_id}/status
60
+ # @param id [String]
61
+ # @param model_id [String]
62
+ # @param logs [Boolean] include logs if true
63
+ # @param client [Fal::Client]
64
+ # @return [Fal::Request]
65
+ def find_by!(id:, model_id:, logs: false, client: Fal.client)
66
+ model_id_without_subpath = model_id.split("/").slice(0, 2).join("/")
67
+ attrs = client.get("/#{model_id_without_subpath}/requests/#{id}/status", query: (logs ? { logs: 1 } : nil))
68
+ new(attrs, model_id: model_id, client: client)
69
+ end
70
+
71
+ # Stream a synchronous request using SSE and yield response chunks as they arrive.
72
+ # It returns a Fal::Request initialized with the last streamed data in the response field.
73
+ # @param model_id [String]
74
+ # @param input [Hash]
75
+ # @param client [Fal::Client]
76
+ # @yield [chunk] yields each parsed chunk Hash from the stream
77
+ # @yieldparam chunk [Hash]
78
+ # @return [Fal::Request]
79
+ def stream!(model_id:, input:, client: Fal.client, &block)
80
+ path = "/#{model_id}/stream"
81
+ last_data = nil
82
+
83
+ Stream.new(path: path, input: input, client: client).each do |event|
84
+ data = event["data"]
85
+ last_data = data
86
+ block&.call(data)
87
+ end
88
+
89
+ # Wrap last chunk into a Request-like object for convenience
90
+ # Build attributes from last event, using inner response if available
91
+ response_payload = if last_data&.key?("response")
92
+ last_data["response"]
93
+ else
94
+ last_data
95
+ end
96
+ attrs = {
97
+ "request_id" => last_data && last_data["request_id"],
98
+ "status" => last_data && last_data["status"],
99
+ "response" => response_payload
100
+ }.compact
101
+ new(attrs, model_id: model_id, client: client)
102
+ end
103
+ end
104
+
105
+ # @return [String] The model ID without the subpath
106
+ def model_id_without_subpath
107
+ @model_id.split("/").slice(0, 2).join("/")
108
+ end
109
+
110
+ # Reload the current status from the Queue API.
111
+ # @param logs [Boolean] include logs if true
112
+ # @return [Fal::Request]
113
+ def reload!(logs: false)
114
+ if @status == Status::IN_PROGRESS || @status == Status::IN_QUEUE
115
+ attrs = @client.get("/#{model_id_without_subpath}/requests/#{@id}/status", query: (logs ? { logs: 1 } : nil))
116
+ reset_attributes(attrs)
117
+ end
118
+
119
+ @response = @client.get("/#{model_id_without_subpath}/requests/#{@id}") if @status == Status::COMPLETED
120
+
121
+ self
122
+ end
123
+
124
+ # Attempt to cancel the request if still in queue.
125
+ # @return [Hash] cancellation response
126
+ def cancel!
127
+ @client.put("/#{model_id_without_subpath}/requests/#{@id}/cancel")
128
+ end
129
+
130
+ # @return [Boolean]
131
+ def in_queue?
132
+ @status == Status::IN_QUEUE
133
+ end
134
+
135
+ # @return [Boolean]
136
+ def in_progress?
137
+ @status == Status::IN_PROGRESS
138
+ end
139
+
140
+ # @return [Boolean]
141
+ def completed?
142
+ @status == Status::COMPLETED
143
+ end
144
+
145
+ private
146
+
147
+ # Normalize attributes from different Queue API responses.
148
+ # @param attributes [Hash]
149
+ # @return [void]
150
+ def reset_attributes(attributes)
151
+ @id = attributes["request_id"] || @id
152
+ # Default to IN_QUEUE if no status provided and no previous status
153
+ @status = attributes["status"] || @status || Status::IN_QUEUE
154
+ @queue_position = attributes["queue_position"]
155
+ @logs = attributes["logs"]
156
+ @response = attributes["response"]
157
+ end
158
+ end
159
+ end
data/lib/fal/stream.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fal
4
+ # Streaming helper for Server-Sent Events from fal.run synchronous endpoints.
5
+ # It parses SSE lines and yields decoded event hashes with symbolized keys.
6
+ class Stream
7
+ # @return [String] endpoint path under fal.run, e.g. "/fal-ai/flux/dev/stream"
8
+ attr_reader :path
9
+
10
+ # @param path [String] full path under sync_base (leading slash), ex: "/fal-ai/flux/dev/stream"
11
+ # @param input [Hash] request input payload
12
+ # @param client [Fal::Client] HTTP client
13
+ def initialize(path:, input:, client: Fal.client)
14
+ @path = path
15
+ @input = input
16
+ @client = client
17
+ end
18
+
19
+ # Stream events; yields a Hash for each event data chunk. Blocks until stream ends.
20
+ # @yield [event] yields decoded event hash
21
+ # @yieldparam event [Hash]
22
+ # @return [void]
23
+ def each(&block)
24
+ buffer = ""
25
+ decoder = SSEDecoder.new
26
+
27
+ @client.post_stream(@path, @input, on_data: proc do |chunk, _total_bytes|
28
+ buffer = (buffer + chunk).gsub(/\r\n?/, "\n")
29
+ lines = buffer.split("\n", -1)
30
+ buffer = lines.pop || ""
31
+ lines.each do |line|
32
+ event = decoder.decode(line)
33
+ block.call(event) if event
34
+ end
35
+ end)
36
+ end
37
+
38
+ # Minimal SSE decoder for parsing standard server-sent event stream lines.
39
+ class SSEDecoder
40
+ def initialize
41
+ @event = ""
42
+ @data = ""
43
+ @id = nil
44
+ @retry = nil
45
+ end
46
+
47
+ # @param line [String]
48
+ # @return [Hash, nil]
49
+ def decode(line)
50
+ return flush_event if line.empty?
51
+ return if line.start_with?(":")
52
+
53
+ field, _, value = line.partition(":")
54
+ value = value.lstrip
55
+
56
+ case field
57
+ when "event"
58
+ @event = value
59
+ when "data"
60
+ @data += "#{value}\n"
61
+ when "id"
62
+ @id = value
63
+ when "retry"
64
+ @retry = value.to_i
65
+ end
66
+
67
+ nil
68
+ end
69
+
70
+ private
71
+
72
+ def flush_event
73
+ return if @data.empty?
74
+
75
+ data = @data.chomp
76
+ parsed = JSON.parse(data)
77
+
78
+ event = { "data" => parsed }
79
+ event["event"] = @event unless @event.empty?
80
+ event["id"] = @id if @id
81
+ event["retry"] = @retry if @retry
82
+
83
+ @event = ""
84
+ @data = ""
85
+ @id = nil
86
+ @retry = nil
87
+
88
+ event
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/fal/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fal
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.3"
5
5
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fal
4
+ # WebhookRequest parses incoming webhook payloads from fal and exposes
5
+ # convenient helpers to inspect success/error and access the response payload.
6
+ # Follows Rails-like naming with predicate helpers.
7
+ class WebhookRequest
8
+ # Webhook status values.
9
+ module Status
10
+ # @return [String]
11
+ OK = "OK"
12
+ # @return [String]
13
+ ERROR = "ERROR"
14
+ end
15
+
16
+ # @return [String, nil] The request identifier
17
+ attr_reader :request_id
18
+ # @return [String, nil] The gateway request identifier, when present
19
+ attr_reader :gateway_request_id
20
+ # @return [String, nil] Webhook status (OK/ERROR) when provided
21
+ attr_reader :status
22
+ # @return [String, nil] Error message when provided
23
+ attr_reader :error
24
+ # @return [Hash, nil] Model-specific response payload
25
+ attr_reader :response
26
+ # @return [Array<Hash>, nil] Log entries, when present
27
+ attr_reader :logs
28
+ # @return [Hash, nil] Metrics, when present
29
+ attr_reader :metrics
30
+ # @return [Hash] The raw parsed payload
31
+ attr_reader :raw
32
+
33
+ # Initialize from a parsed payload Hash (string keys expected, tolerant of symbol keys).
34
+ # @param attributes [Hash]
35
+ def initialize(attributes)
36
+ @raw = attributes
37
+ reset_attributes(attributes)
38
+ end
39
+
40
+ class << self
41
+ # Build from a JSON string body.
42
+ # @param json [String]
43
+ # @return [Fal::WebhookRequest]
44
+ def from_json(json)
45
+ new(JSON.parse(json))
46
+ end
47
+
48
+ # Build from a Rack::Request.
49
+ # @param req [#body]
50
+ # @return [Fal::WebhookRequest]
51
+ def from_rack_request(req)
52
+ body = req.body.read
53
+ req.body.rewind if req.body.respond_to?(:rewind)
54
+ from_json(body)
55
+ end
56
+
57
+ # Build from a Hash payload.
58
+ # @param payload [Hash]
59
+ # @return [Fal::WebhookRequest]
60
+ def from_hash(payload)
61
+ new(payload)
62
+ end
63
+ end
64
+
65
+ # @return [Boolean]
66
+ def success?
67
+ @status == Status::OK || (
68
+ @status.nil? && @error.nil?
69
+ )
70
+ end
71
+
72
+ # @return [Boolean]
73
+ def error?
74
+ !success?
75
+ end
76
+
77
+ # Back-compat alias matching older naming (payload vs response).
78
+ # @return [Hash, nil]
79
+ def payload
80
+ @response
81
+ end
82
+
83
+ # @return [String, nil] Any nested error detail
84
+ def error_detail = @response&.dig(:detail)
85
+
86
+ private
87
+
88
+ def reset_attributes(attributes)
89
+ attributes = Fal.deep_symbolize_keys(attributes)
90
+ @request_id = attributes[:request_id]
91
+ @gateway_request_id = attributes[:gateway_request_id]
92
+ @status = attributes[:status]
93
+ @error = attributes[:error]
94
+ @response = attributes[:payload]
95
+ @logs = attributes[:logs]
96
+ @metrics = attributes[:metrics]
97
+ end
98
+ end
99
+ end
data/lib/fal.rb CHANGED
@@ -2,6 +2,99 @@
2
2
 
3
3
  require "faraday"
4
4
  require "time"
5
+ require "json"
6
+ require "cgi"
7
+ require "uri"
8
+
9
+ require_relative "fal/version"
5
10
 
6
11
  module Fal
12
+ # Base error class for all fal-related errors.
13
+ class Error < StandardError; end
14
+ # Raised when a request is unauthorized (HTTP 401).
15
+ class UnauthorizedError < Error; end
16
+ # Raised when a requested resource is not found (HTTP 404).
17
+ class NotFoundError < Error; end
18
+ # Raised when the server returns an unexpected error (HTTP 5xx or others).
19
+ class ServerError < Error; end
20
+ # Raised when the client is misconfigured.
21
+ class ConfigurationError < Error; end
22
+ # Raised when access is forbidden (HTTP 403).
23
+ class ForbiddenError < Error; end
24
+
25
+ # Global configuration for the fal client.
26
+ class Configuration
27
+ DEFAULT_QUEUE_BASE = "https://queue.fal.run"
28
+ DEFAULT_SYNC_BASE = "https://fal.run"
29
+ DEFAULT_REQUEST_TIMEOUT = 120
30
+
31
+ # API key used for authenticating with fal endpoints.
32
+ # Defaults to ENV["FAL_KEY"].
33
+ # @return [String]
34
+ attr_accessor :api_key
35
+
36
+ # Base URL for fal queue endpoints.
37
+ # @return [String]
38
+ attr_accessor :queue_base
39
+
40
+ # Base URL for synchronous streaming endpoints (fal.run).
41
+ # @return [String]
42
+ attr_accessor :sync_base
43
+
44
+ # Timeout in seconds for opening and processing HTTP requests.
45
+ # @return [Integer]
46
+ attr_accessor :request_timeout
47
+
48
+ # Initialize configuration with sensible defaults.
49
+ # @return [Fal::Configuration]
50
+ def initialize
51
+ @api_key = ENV.fetch("FAL_KEY", nil)
52
+ @queue_base = DEFAULT_QUEUE_BASE
53
+ @sync_base = DEFAULT_SYNC_BASE
54
+ @request_timeout = DEFAULT_REQUEST_TIMEOUT
55
+ end
56
+ end
57
+
58
+ class << self
59
+ # The global configuration instance.
60
+ # @return [Fal::Configuration]
61
+ attr_accessor :configuration
62
+
63
+ # Configure the fal client.
64
+ # @yield [Fal::Configuration] the configuration object to mutate
65
+ # @return [void]
66
+ def configure
67
+ self.configuration ||= Configuration.new
68
+ yield(configuration)
69
+ end
70
+
71
+ # Global client accessor using the configured settings.
72
+ # @return [Fal::Client]
73
+ def client
74
+ configuration = self.configuration || Configuration.new
75
+ @client ||= Fal::Client.new(configuration)
76
+ end
77
+
78
+ # Deep symbolize keys of a Hash or Array.
79
+ # @param obj [Hash, Array]
80
+ # @return [Hash, Array]
81
+ def deep_symbolize_keys(obj)
82
+ case obj
83
+ when Hash
84
+ obj.each_with_object({}) do |(k, v), result|
85
+ key = k.is_a?(String) ? k.to_sym : k
86
+ result[key] = deep_symbolize_keys(v)
87
+ end
88
+ when Array
89
+ obj.map { |e| deep_symbolize_keys(e) }
90
+ else
91
+ obj
92
+ end
93
+ end
94
+ end
7
95
  end
96
+
97
+ require_relative "fal/client"
98
+ require_relative "fal/request"
99
+ require_relative "fal/stream"
100
+ require_relative "fal/webhook_request"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dylan Player
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-03 00:00:00.000000000 Z
11
+ date: 2025-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -31,6 +31,7 @@ executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
+ - ".env.example"
34
35
  - ".rubocop.yml"
35
36
  - ".ruby-version"
36
37
  - Gemfile
@@ -40,7 +41,11 @@ files:
40
41
  - Rakefile
41
42
  - fal.gemspec
42
43
  - lib/fal.rb
44
+ - lib/fal/client.rb
45
+ - lib/fal/request.rb
46
+ - lib/fal/stream.rb
43
47
  - lib/fal/version.rb
48
+ - lib/fal/webhook_request.rb
44
49
  homepage: https://github.com/851-labs/fal
45
50
  licenses:
46
51
  - MIT