Skip to content

Commit 4f8ca5c

Browse files
committed
Add functions admin CRUD methods
1 parent df5c9df commit 4f8ca5c

3 files changed

Lines changed: 256 additions & 0 deletions

File tree

insforge/functions/client.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,80 @@
55

66
from .._base_client import BaseClient
77
from .._utils import quote_path_segment
8+
from .models import FunctionCreateRequest
89
from .models import FunctionDetails
10+
from .models import FunctionDeleteResponse
11+
from .models import FunctionMutationResponse
912
from .models import FunctionMetadata
13+
from .models import FunctionUpdateRequest
1014

1115

1216
class FunctionsClient:
1317
def __init__(self, client: BaseClient) -> None:
1418
self._client = client
1519

20+
async def create_function(
21+
self,
22+
*,
23+
name: str,
24+
slug: str | None = None,
25+
code: str,
26+
description: str | None = None,
27+
status: str | None = None,
28+
access_token: str | None = None,
29+
) -> FunctionMutationResponse:
30+
payload = FunctionCreateRequest(
31+
name=name,
32+
slug=slug,
33+
code=code,
34+
description=description,
35+
status=status,
36+
).model_dump(exclude_none=True)
37+
response = await self._client._request_json(
38+
"POST",
39+
"/api/functions",
40+
json=payload,
41+
access_token=access_token,
42+
)
43+
return FunctionMutationResponse.model_validate(response)
44+
45+
async def update_function(
46+
self,
47+
slug: str,
48+
*,
49+
name: str | None = None,
50+
code: str | None = None,
51+
description: str | None = None,
52+
status: str | None = None,
53+
access_token: str | None = None,
54+
) -> FunctionMutationResponse:
55+
payload = FunctionUpdateRequest(
56+
name=name,
57+
code=code,
58+
description=description,
59+
status=status,
60+
).model_dump(exclude_none=True)
61+
response = await self._client._request_json(
62+
"PUT",
63+
f"/api/functions/{quote_path_segment(slug)}",
64+
json=payload,
65+
access_token=access_token,
66+
)
67+
return FunctionMutationResponse.model_validate(response)
68+
69+
async def delete_function(
70+
self,
71+
slug: str,
72+
*,
73+
access_token: str | None = None,
74+
) -> FunctionDeleteResponse:
75+
response = await self._client._request_json(
76+
"DELETE",
77+
f"/api/functions/{quote_path_segment(slug)}",
78+
access_token=access_token,
79+
)
80+
return FunctionDeleteResponse.model_validate(response)
81+
1682
async def list_functions(self, *, access_token: str | None = None) -> list[FunctionMetadata]:
1783
payload = await self._client._request_json(
1884
"GET",

insforge/functions/models.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,36 @@ class FunctionMetadata(BaseModel):
2121

2222
class FunctionDetails(FunctionMetadata):
2323
code: str
24+
25+
26+
class FunctionCreateRequest(BaseModel):
27+
model_config = ConfigDict(extra="ignore")
28+
29+
name: str
30+
slug: str | None = None
31+
code: str
32+
description: str | None = None
33+
status: str | None = None
34+
35+
36+
class FunctionUpdateRequest(BaseModel):
37+
model_config = ConfigDict(extra="ignore")
38+
39+
name: str | None = None
40+
code: str | None = None
41+
description: str | None = None
42+
status: str | None = None
43+
44+
45+
class FunctionMutationResponse(BaseModel):
46+
model_config = ConfigDict(extra="ignore")
47+
48+
success: bool
49+
function: FunctionMetadata
50+
51+
52+
class FunctionDeleteResponse(BaseModel):
53+
model_config = ConfigDict(extra="ignore")
54+
55+
success: bool
56+
message: str | None = None

tests/functions/test_functions_client.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import httpx
44

55
from insforge import InsforgeClient
6+
from insforge.functions.models import FunctionDeleteResponse
67
from insforge.functions.models import FunctionDetails
8+
from insforge.functions.models import FunctionMutationResponse
79
from insforge.functions.models import FunctionMetadata
810

911

@@ -208,3 +210,158 @@ async def fake_request(method: str, url: httpx.URL, **kwargs: object) -> httpx.R
208210
assert isinstance(result, FunctionDetails)
209211
assert result.slug == "hello-world"
210212
assert result.code.startswith("export default")
213+
214+
215+
def test_create_function_uses_api_path_and_returns_typed_mutation_response() -> None:
216+
async def scenario() -> tuple[object, dict[str, object]]:
217+
captured: dict[str, object] = {}
218+
219+
async def fake_request(method: str, url: httpx.URL, **kwargs: object) -> httpx.Response:
220+
captured["method"] = method
221+
captured["url"] = str(url)
222+
captured["kwargs"] = kwargs
223+
return httpx.Response(
224+
201,
225+
json={
226+
"success": True,
227+
"function": {
228+
"id": "123e4567-e89b-12d3-a456-426614174000",
229+
"slug": "hello-world",
230+
"name": "Hello World Function",
231+
"description": "Returns a greeting message",
232+
"status": "active",
233+
"created_at": "2024-01-21T10:30:00Z",
234+
"updated_at": "2024-01-21T10:35:00Z",
235+
"deployed_at": "2024-01-21T10:35:00Z",
236+
},
237+
},
238+
)
239+
240+
async with InsforgeClient(
241+
base_url="https://example.com",
242+
api_key="ins_test",
243+
) as client:
244+
client.http_client.request = fake_request # type: ignore[method-assign]
245+
result = await client.functions.create_function(
246+
name="Hello World Function",
247+
slug="hello-world",
248+
code="export default async function () { return new Response('hi'); }",
249+
description="Returns a greeting message",
250+
status="active",
251+
access_token="admin_token",
252+
)
253+
return result, captured
254+
255+
result, captured = asyncio.run(scenario())
256+
257+
assert captured["method"] == "POST"
258+
assert captured["url"] == "https://example.com/api/functions"
259+
assert captured["kwargs"]["headers"]["X-API-Key"] == "ins_test"
260+
assert captured["kwargs"]["headers"]["Authorization"] == "Bearer admin_token"
261+
assert captured["kwargs"]["json"] == {
262+
"name": "Hello World Function",
263+
"slug": "hello-world",
264+
"code": "export default async function () { return new Response('hi'); }",
265+
"description": "Returns a greeting message",
266+
"status": "active",
267+
}
268+
assert isinstance(result, FunctionMutationResponse)
269+
assert result.success is True
270+
assert isinstance(result.function, FunctionMetadata)
271+
assert result.function.slug == "hello-world"
272+
273+
274+
def test_update_function_uses_api_path_and_returns_typed_mutation_response() -> None:
275+
async def scenario() -> tuple[object, dict[str, object]]:
276+
captured: dict[str, object] = {}
277+
278+
async def fake_request(method: str, url: httpx.URL, **kwargs: object) -> httpx.Response:
279+
captured["method"] = method
280+
captured["url"] = str(url)
281+
captured["kwargs"] = kwargs
282+
return httpx.Response(
283+
200,
284+
json={
285+
"success": True,
286+
"function": {
287+
"id": "123e4567-e89b-12d3-a456-426614174000",
288+
"slug": "hello-world",
289+
"name": "Hello World Function v2",
290+
"description": "Returns a greeting message",
291+
"status": "active",
292+
"created_at": "2024-01-21T10:30:00Z",
293+
"updated_at": "2024-01-21T11:00:00Z",
294+
"deployed_at": "2024-01-21T10:35:00Z",
295+
},
296+
},
297+
)
298+
299+
async with InsforgeClient(
300+
base_url="https://example.com",
301+
api_key="ins_test",
302+
) as client:
303+
client.http_client.request = fake_request # type: ignore[method-assign]
304+
result = await client.functions.update_function(
305+
"hello-world",
306+
name="Hello World Function v2",
307+
code="export default async function () { return new Response('hi'); }",
308+
description="Returns a greeting message",
309+
status="active",
310+
access_token="admin_token",
311+
)
312+
return result, captured
313+
314+
result, captured = asyncio.run(scenario())
315+
316+
assert captured["method"] == "PUT"
317+
assert captured["url"] == "https://example.com/api/functions/hello-world"
318+
assert captured["kwargs"]["headers"]["X-API-Key"] == "ins_test"
319+
assert captured["kwargs"]["headers"]["Authorization"] == "Bearer admin_token"
320+
assert captured["kwargs"]["json"] == {
321+
"name": "Hello World Function v2",
322+
"code": "export default async function () { return new Response('hi'); }",
323+
"description": "Returns a greeting message",
324+
"status": "active",
325+
}
326+
assert isinstance(result, FunctionMutationResponse)
327+
assert result.success is True
328+
assert result.function.name == "Hello World Function v2"
329+
330+
331+
def test_delete_function_uses_api_path_and_returns_typed_delete_response() -> None:
332+
async def scenario() -> tuple[object, dict[str, object]]:
333+
captured: dict[str, object] = {}
334+
335+
async def fake_request(method: str, url: httpx.URL, **kwargs: object) -> httpx.Response:
336+
captured["method"] = method
337+
captured["url"] = str(url)
338+
captured["kwargs"] = kwargs
339+
return httpx.Response(
340+
200,
341+
json={
342+
"success": True,
343+
"message": "Function hello-world deleted successfully",
344+
},
345+
)
346+
347+
async with InsforgeClient(
348+
base_url="https://example.com",
349+
api_key="ins_test",
350+
) as client:
351+
client.http_client.request = fake_request # type: ignore[method-assign]
352+
result = await client.functions.delete_function(
353+
"hello-world",
354+
access_token="admin_token",
355+
)
356+
return result, captured
357+
358+
result, captured = asyncio.run(scenario())
359+
360+
assert captured["method"] == "DELETE"
361+
assert captured["url"] == "https://example.com/api/functions/hello-world"
362+
assert captured["kwargs"]["headers"]["X-API-Key"] == "ins_test"
363+
assert captured["kwargs"]["headers"]["Authorization"] == "Bearer admin_token"
364+
assert captured["kwargs"].get("json") is None
365+
assert isinstance(result, FunctionDeleteResponse)
366+
assert result.success is True
367+
assert result.message == "Function hello-world deleted successfully"

0 commit comments

Comments
 (0)