Skip to content

Commit cda2481

Browse files
committed
Add oauth to authentication flow to client creation
1 parent 2832325 commit cda2481

File tree

8 files changed

+579
-131
lines changed

8 files changed

+579
-131
lines changed

lib/tama_ex.ex

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,87 @@ defmodule TamaEx do
44
"""
55

66
@doc """
7-
Creates a new HTTP client with the given base URL.
7+
Creates a new HTTP client with authentication.
8+
9+
This function performs OAuth2 client credentials authentication and returns
10+
an authenticated HTTP client. The base_url should be the root URL of the API server.
11+
12+
## Parameters
13+
- base_url - The root base URL for the API server (e.g., "https://api.example.com")
14+
- client_id - The OAuth2 client ID
15+
- client_secret - The OAuth2 client secret
16+
- options - Optional configuration (e.g., scopes)
17+
18+
## Returns
19+
- `{:ok, %{client: client, expires_in: seconds}}` on successful authentication
20+
- `{:error, reason}` on authentication failure
21+
22+
## Examples
23+
24+
# Create client with root URL
25+
{:ok, %{client: client}} = TamaEx.client("https://api.example.com", "client_id", "client_secret")
26+
27+
# Add namespace for specific operations
28+
namespaced_client = TamaEx.put_namespace(client, "provision")
29+
30+
"""
31+
def client(
32+
base_url,
33+
client_id,
34+
client_secret,
35+
options \\ []
36+
) do
37+
scopes = Keyword.get(options, :scopes) || ["provision.all"]
38+
39+
token = Base.url_encode64("#{client_id}:#{client_secret}", padding: false)
40+
41+
body = %{
42+
"grant_type" => "client_credentials",
43+
"scope" => Enum.join(scopes, " ")
44+
}
45+
46+
case Req.new(
47+
base_url: base_url,
48+
headers: [{"authorization", "Bearer #{token}"}]
49+
)
50+
|> Req.post(url: "/auth/tokens", json: body) do
51+
{:ok, %Req.Response{status: 200, body: token}} ->
52+
headers = [{"authorization", "Bearer #{token["access_token"]}"}]
53+
54+
{:ok,
55+
%{client: Req.new(base_url: base_url, headers: headers), expires_in: token["expires_in"]}}
56+
57+
{:ok, %Req.Response{body: body}} ->
58+
{:error, body}
59+
60+
{:error, reason} ->
61+
{:error, reason}
62+
end
63+
end
64+
65+
@doc """
66+
Adds a namespace to the client's base URL.
67+
68+
## Parameters
69+
- client - The HTTP client
70+
- namespace - The namespace to append (e.g., "provision", "ingest")
71+
72+
## Returns
73+
- Updated client with namespaced base URL
874
975
## Examples
1076
11-
iex> client = TamaEx.client(base_url: "https://api.example.com")
12-
iex> client.options[:base_url]
13-
"https://api.example.com"
77+
iex> client = Req.new(base_url: "https://api.example.com")
78+
iex> namespaced_client = TamaEx.put_namespace(client, "provision")
79+
iex> namespaced_client.options[:base_url]
80+
"https://api.example.com/provision"
1481
1582
"""
16-
def client(base_url: base_url) do
17-
Req.new(base_url: base_url)
83+
def put_namespace(%Req.Request{} = client, namespace) when is_binary(namespace) do
84+
current_base_url = client.options[:base_url] || ""
85+
new_base_url = "#{current_base_url}/#{namespace}"
86+
87+
Req.merge(client, base_url: new_base_url)
1888
end
1989

2090
@doc """
@@ -70,12 +140,12 @@ defmodule TamaEx do
70140
71141
## Examples
72142
73-
iex> client = TamaEx.client(base_url: "https://api.example.com/provision")
143+
iex> client = %Req.Request{options: %{base_url: "https://api.example.com/provision"}}
74144
iex> {:ok, validated_client} = TamaEx.validate_client(client, ["provision"])
75145
iex> validated_client == client
76146
true
77147
78-
iex> client = TamaEx.client(base_url: "https://api.example.com/ingest")
148+
iex> client = %Req.Request{options: %{base_url: "https://api.example.com/ingest"}}
79149
iex> try do
80150
...> TamaEx.validate_client(client, ["provision"])
81151
...> rescue

lib/tama_ex/perception.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule TamaEx.Perception do
1515
1616
## Examples
1717
18-
iex> client = TamaEx.client(base_url: "https://api.example.com/provision")
18+
iex> client = %Req.Request{options: %{base_url: "https://api.example.com/provision"}}
1919
iex> {:ok, _} = TamaEx.validate_client(client, ["provision"])
2020
iex> is_binary("space_123") and is_binary("my-chain")
2121
true

test/tama_ex/agentic_test.exs

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
defmodule TamaEx.AgenticTest do
22
use ExUnit.Case
3+
import TestHelpers
34
doctest TamaEx.Agentic
45

56
alias TamaEx.Agentic
67

78
setup do
89
bypass = Bypass.open()
9-
client = TamaEx.client(base_url: "http://localhost:#{bypass.port}/agentic")
10-
11-
{:ok, bypass: bypass, client: client}
10+
{:ok, bypass: bypass}
1211
end
1312

1413
describe "create_message/3 - client validation" do
15-
test "validates required client namespace", %{client: _client} do
14+
test "validates required client namespace" do
1615
# Test with wrong namespace
17-
client = TamaEx.client(base_url: "https://api.example.com/provision")
16+
client = mock_client("provision")
1817

1918
body = %{
2019
"recipient" => "user-123",
@@ -51,7 +50,8 @@ defmodule TamaEx.AgenticTest do
5150
end
5251

5352
describe "create_message/3 - message validation" do
54-
test "handles invalid message params", %{client: client} do
53+
test "handles invalid message params" do
54+
client = mock_client("agentic")
5555
# Test with missing required fields
5656
invalid_body = %{
5757
"content" => "Hello, world!"
@@ -63,7 +63,9 @@ defmodule TamaEx.AgenticTest do
6363
end
6464
end
6565

66-
test "validates message structure with missing author", %{client: client} do
66+
test "validates message structure with missing author" do
67+
client = mock_client("agentic")
68+
6769
invalid_body = %{
6870
"recipient" => "user-123",
6971
"content" => "Hello, world!",
@@ -79,7 +81,10 @@ defmodule TamaEx.AgenticTest do
7981
end
8082

8183
describe "create_message/3 - non-streaming requests" do
82-
test "handles successful non-streaming response", %{bypass: bypass, client: client} do
84+
test "handles successful non-streaming response", %{bypass: bypass} do
85+
base_url = "http://localhost:#{bypass.port}"
86+
client = mock_client("agentic", base_url)
87+
8388
Bypass.expect(bypass, "POST", "/agentic/messages", fn conn ->
8489
{:ok, body, conn} = Plug.Conn.read_body(conn)
8590
request_data = Jason.decode!(body)
@@ -138,7 +143,10 @@ defmodule TamaEx.AgenticTest do
138143
assert response_body["message"]["content"] == "Hello, world!"
139144
end
140145

141-
test "handles non-streaming request with custom options", %{bypass: bypass, client: client} do
146+
test "handles non-streaming request with custom options", %{bypass: bypass} do
147+
base_url = "http://localhost:#{bypass.port}"
148+
client = mock_client("agentic", base_url)
149+
142150
Bypass.expect(bypass, "POST", "/agentic/messages", fn conn ->
143151
# Check headers
144152
assert Plug.Conn.get_req_header(conn, "authorization") == ["Bearer test-token"]
@@ -194,7 +202,9 @@ defmodule TamaEx.AgenticTest do
194202
end
195203

196204
describe "create_message/3 - streaming requests" do
197-
test "raises error when stream is true but no callback provided", %{client: client} do
205+
test "raises error when stream is true but no callback provided" do
206+
client = mock_client("agentic")
207+
198208
body = %{
199209
"recipient" => "user-123",
200210
"content" => "Hello, streaming world!",
@@ -228,7 +238,9 @@ defmodule TamaEx.AgenticTest do
228238
end
229239
end
230240

231-
test "handles streaming response with callback", %{bypass: bypass, client: client} do
241+
test "handles streaming response with callback", %{bypass: bypass} do
242+
base_url = "http://localhost:#{bypass.port}"
243+
client = mock_client("agentic", base_url)
232244
test_pid = self()
233245

234246
Bypass.expect(bypass, "POST", "/agentic/messages", fn conn ->
@@ -336,7 +348,9 @@ defmodule TamaEx.AgenticTest do
336348
assert final_chunk["message"]["final_content"] == "Hello streaming world!"
337349
end
338350

339-
test "handles streaming with atom keys in body", %{bypass: bypass, client: client} do
351+
test "handles streaming with atom keys in body", %{bypass: bypass} do
352+
base_url = "http://localhost:#{bypass.port}"
353+
client = mock_client("agentic", base_url)
340354
test_pid = self()
341355

342356
Bypass.expect(bypass, "POST", "/agentic/messages", fn conn ->
@@ -395,7 +409,9 @@ defmodule TamaEx.AgenticTest do
395409
end
396410

397411
describe "data parsing functionality" do
398-
test "handles mixed streaming data formats", %{bypass: bypass, client: client} do
412+
test "handles mixed streaming data formats", %{bypass: bypass} do
413+
base_url = "http://localhost:#{bypass.port}"
414+
client = mock_client("agentic", base_url)
399415
test_pid = self()
400416

401417
Bypass.expect(bypass, "POST", "/agentic/messages", fn conn ->
@@ -449,7 +465,9 @@ defmodule TamaEx.AgenticTest do
449465
end
450466

451467
describe "timeout handling" do
452-
test "uses default timeout when not specified", %{bypass: bypass, client: client} do
468+
test "uses default timeout when not specified", %{bypass: bypass} do
469+
base_url = "http://localhost:#{bypass.port}"
470+
client = mock_client("agentic", base_url)
453471
# We can't directly test the timeout value, but we can verify the request is made
454472
Bypass.expect(bypass, "POST", "/agentic/messages", fn conn ->
455473
# Simulate a request that would benefit from long timeout
@@ -486,7 +504,10 @@ defmodule TamaEx.AgenticTest do
486504
assert response_body["id"] == "timeout-test-123"
487505
end
488506

489-
test "uses custom timeout when specified", %{bypass: bypass, client: client} do
507+
test "uses custom timeout when specified", %{bypass: bypass} do
508+
base_url = "http://localhost:#{bypass.port}"
509+
client = mock_client("agentic", base_url)
510+
490511
Bypass.expect(bypass, "POST", "/agentic/messages", fn conn ->
491512
response_data = %{
492513
id: "custom-timeout-456",

test/tama_ex/memory_test.exs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule TamaEx.MemoryTest do
22
use ExUnit.Case
3+
import TestHelpers
34
doctest TamaEx.Memory
45

56
alias TamaEx.Memory
@@ -16,11 +17,6 @@ defmodule TamaEx.MemoryTest do
1617
}
1718
end
1819

19-
# Test helper for creating a mock client
20-
defp mock_client(namespace) do
21-
TamaEx.client(base_url: "https://api.example.com/#{namespace}")
22-
end
23-
2420
describe "create_entity/3" do
2521
test "creates entity with valid parameters" do
2622
_client = mock_client("ingest")

test/tama_ex/neural_test.exs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule TamaEx.NeuralTest do
22
use ExUnit.Case
3+
import TestHelpers
34
# Disable doctests due to undefined client variable in examples
45
# doctest TamaEx.Neural
56

@@ -29,11 +30,6 @@ defmodule TamaEx.NeuralTest do
2930
}
3031
end
3132

32-
# Test helper for creating a mock client
33-
defp mock_client(namespace) do
34-
TamaEx.client(base_url: "https://api.example.com/#{namespace}")
35-
end
36-
3733
describe "get_space/2" do
3834
test "validates required client namespace" do
3935
# Test with wrong namespace

test/tama_ex/perception_test.exs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
defmodule TamaEx.PerceptionTest do
22
use ExUnit.Case
3+
import TestHelpers
34
doctest TamaEx.Perception
45

56
alias TamaEx.Perception
@@ -18,14 +19,13 @@ defmodule TamaEx.PerceptionTest do
1819
}
1920
end
2021

21-
# Test helper for creating a mock client
22-
defp mock_client(namespace) do
23-
TamaEx.client(base_url: "https://api.example.com/#{namespace}")
24-
end
25-
2622
setup do
27-
bypass = Bypass.open()
28-
client = TamaEx.client(base_url: "http://localhost:#{bypass.port}/perception")
23+
{bypass, base_url} = setup_bypass_with_auth()
24+
25+
{:ok, %{client: base_client}} =
26+
TamaEx.client(base_url, "test_client", "test_secret")
27+
28+
client = TamaEx.put_namespace(base_client, "perception")
2929

3030
{:ok, bypass: bypass, client: client}
3131
end
@@ -266,7 +266,7 @@ defmodule TamaEx.PerceptionTest do
266266

267267
describe "list_concepts/3" do
268268
test "validates required client namespace", %{bypass: bypass} do
269-
client = TamaEx.client(base_url: "http://localhost:#{bypass.port}/ingest")
269+
client = mock_client("ingest", "http://localhost:#{bypass.port}")
270270
entity_id = "entity_123"
271271

272272
assert_raise ArgumentError, ~r/Invalid client namespace/, fn ->
@@ -444,7 +444,7 @@ defmodule TamaEx.PerceptionTest do
444444

445445
test "handles network errors" do
446446
# Use an invalid port to simulate network error
447-
invalid_client = TamaEx.client(base_url: "http://localhost:99999/perception")
447+
invalid_client = mock_client("perception", "http://localhost:99999")
448448

449449
# The connection error may be raised as an exception instead of returning error tuple
450450
result =

0 commit comments

Comments
 (0)