Skip to content

Commit 3da8fed

Browse files
authored
Merge pull request #7 from FoxNoseTech/feat/flux-introspection-sdk
feat: add Flux introspection support and API folder route descriptions (v0.4.0)
2 parents d26812b + 5c99b29 commit 3da8fed

File tree

11 files changed

+365
-8
lines changed

11 files changed

+365
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This is the official Python SDK for FoxNose Management and Flux APIs.
1616
- **Sync and async** - Both synchronous and asynchronous clients
1717
- **Automatic retries** - Configurable retry with exponential backoff
1818
- **JWT authentication** - Built-in token refresh support
19+
- **Flux introspection** - Discover routes and live schema via `/_router` and `/_schema`
1920

2021
## Documentation
2122

docs/changelog.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,24 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [Unreleased]
8+
## [0.4.0] - 2026-02-25
9+
10+
### Added
11+
12+
- **Flux introspection methods** on sync and async clients:
13+
- `get_router()` calls `GET /{api_prefix}/_router`
14+
- `get_schema(folder_path)` calls `GET /{api_prefix}/{folder_path}/_schema`
15+
- **API folder route description support** in Management clients:
16+
- `add_api_folder()` and `update_api_folder()` now accept:
17+
- `description_get_one`
18+
- `description_get_many`
19+
- `description_search`
20+
- `description_schema`
21+
- **`APIFolderSummary` model fields** for route descriptions:
22+
- `description_get_one`
23+
- `description_get_many`
24+
- `description_search`
25+
- `description_schema`
926

1027
## [0.3.0] - 2026-02-10
1128

@@ -62,7 +79,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6279
- Error handling guide
6380
- Code examples
6481

65-
[Unreleased]: https://github.com/foxnose/python-sdk/compare/v0.3.0...HEAD
82+
[0.4.0]: https://github.com/foxnose/python-sdk/compare/v0.3.0...v0.4.0
6683
[0.3.0]: https://github.com/foxnose/python-sdk/compare/v0.2.0...v0.3.0
6784
[0.2.0]: https://github.com/foxnose/python-sdk/compare/v0.1.0...v0.2.0
6885
[0.1.0]: https://github.com/foxnose/python-sdk/releases/tag/v0.1.0

docs/flux-client.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,34 @@ for item in results["results"]:
8989
print(item["data"]["title"])
9090
```
9191

92+
## Introspection Endpoints
93+
94+
Use Flux introspection to discover available routes and live schema metadata at runtime.
95+
96+
### Router Introspection
97+
98+
```python
99+
router = client.get_router()
100+
print(router["api"])
101+
print(f"Discovered routes: {len(router['routes'])}")
102+
```
103+
104+
### Schema Introspection
105+
106+
```python
107+
schema = client.get_schema("blog-posts")
108+
print(schema["path"])
109+
print(schema["searchable_fields"])
110+
print(schema["json_schema"]["type"])
111+
```
112+
113+
Async usage:
114+
115+
```python
116+
router = await client.get_router()
117+
schema = await client.get_schema("blog-posts")
118+
```
119+
92120
## Query Parameters
93121

94122
### Filtering

docs/management-client.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,35 @@ def publish_all(client, folder: FolderRef):
8585
# ...
8686
```
8787

88+
## API Folder Route Descriptions
89+
90+
When connecting a folder to a Flux API, you can configure per-route descriptions used by Flux `/_router` introspection.
91+
92+
```python
93+
connection = client.add_api_folder(
94+
api_key="api-key",
95+
folder_key="folder-key",
96+
allowed_methods=["get_many", "get_one"],
97+
description_get_one="Get one article by key",
98+
description_get_many="List published articles",
99+
description_search="Search published articles",
100+
description_schema="Read article schema",
101+
)
102+
103+
print(connection.description_schema)
104+
```
105+
106+
To update descriptions later:
107+
108+
```python
109+
updated = client.update_api_folder(
110+
"api-key",
111+
"folder-key",
112+
description_get_many="Public article feed",
113+
description_search="Search feed items",
114+
)
115+
```
116+
88117
## Folder Operations
89118

90119
### List Folders

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "foxnose-sdk"
7-
version = "0.3.0"
7+
version = "0.4.0"
88
description = "Official Python client for FoxNose Management and Flux APIs"
99
readme = "README.md"
1010
license = {text = "Apache-2.0"}

src/foxnose_sdk/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,4 @@
154154
"APIRef",
155155
]
156156

157-
__version__ = "0.2.0"
157+
__version__ = "0.4.0"

src/foxnose_sdk/flux/client.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ def search(
8383
path = self._build_path(folder_path, suffix="/_search")
8484
return self._transport.request("POST", path, json_body=body)
8585

86+
def get_router(self) -> Any:
87+
"""Return available routes and contracts under the configured API prefix."""
88+
path = f"/{self.api_prefix}/_router"
89+
return self._transport.request("GET", path)
90+
91+
def get_schema(self, folder_path: str) -> Any:
92+
"""Return live JSON Schema and metadata for the given folder path."""
93+
path = self._build_path(folder_path, suffix="/_schema")
94+
return self._transport.request("GET", path)
95+
8696
def close(self) -> None:
8797
self._transport.close()
8898

@@ -153,5 +163,15 @@ async def search(
153163
path = self._build_path(folder_path, suffix="/_search")
154164
return await self._transport.arequest("POST", path, json_body=body)
155165

166+
async def get_router(self) -> Any:
167+
"""Return available routes and contracts under the configured API prefix."""
168+
path = f"/{self.api_prefix}/_router"
169+
return await self._transport.arequest("GET", path)
170+
171+
async def get_schema(self, folder_path: str) -> Any:
172+
"""Return live JSON Schema and metadata for the given folder path."""
173+
path = self._build_path(folder_path, suffix="/_schema")
174+
return await self._transport.arequest("GET", path)
175+
156176
async def aclose(self) -> None:
157177
await self._transport.aclose()

src/foxnose_sdk/management/client.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -543,19 +543,35 @@ def add_api_folder(
543543
folder_key: FolderRef,
544544
*,
545545
allowed_methods: list[str] | None = None,
546+
description_get_one: str | None = None,
547+
description_get_many: str | None = None,
548+
description_search: str | None = None,
549+
description_schema: str | None = None,
546550
) -> APIFolderSummary:
547551
"""Add a folder to an API.
548552
549553
Args:
550554
api_key: Unique identifier of the API.
551555
folder_key: Unique identifier of the folder to add.
552556
allowed_methods: HTTP methods allowed for this folder (e.g., ["GET", "POST"]).
557+
description_get_one: Optional short description for the get-one route.
558+
description_get_many: Optional short description for the list route.
559+
description_search: Optional short description for the search route.
560+
description_schema: Optional short description for the schema route.
553561
"""
554562
api_key = _resolve_key(api_key)
555563
folder_key = _resolve_key(folder_key)
556564
payload: dict[str, Any] = {"folder": folder_key}
557-
if allowed_methods:
565+
if allowed_methods is not None:
558566
payload["allowed_methods"] = allowed_methods
567+
if description_get_one is not None:
568+
payload["description_get_one"] = description_get_one
569+
if description_get_many is not None:
570+
payload["description_get_many"] = description_get_many
571+
if description_search is not None:
572+
payload["description_search"] = description_search
573+
if description_schema is not None:
574+
payload["description_schema"] = description_schema
559575
data = self.request(
560576
"POST", f"{self._api_folders_root(api_key)}/", json_body=payload
561577
)
@@ -581,19 +597,35 @@ def update_api_folder(
581597
folder_key: FolderRef,
582598
*,
583599
allowed_methods: list[str] | None = None,
600+
description_get_one: str | None = None,
601+
description_get_many: str | None = None,
602+
description_search: str | None = None,
603+
description_schema: str | None = None,
584604
) -> APIFolderSummary:
585605
"""Update a folder's configuration within an API.
586606
587607
Args:
588608
api_key: Unique identifier of the API.
589609
folder_key: Unique identifier of the folder.
590610
allowed_methods: HTTP methods allowed for this folder.
611+
description_get_one: Optional short description for the get-one route.
612+
description_get_many: Optional short description for the list route.
613+
description_search: Optional short description for the search route.
614+
description_schema: Optional short description for the schema route.
591615
"""
592616
api_key = _resolve_key(api_key)
593617
folder_key = _resolve_key(folder_key)
594618
payload: dict[str, Any] = {}
595619
if allowed_methods is not None:
596620
payload["allowed_methods"] = allowed_methods
621+
if description_get_one is not None:
622+
payload["description_get_one"] = description_get_one
623+
if description_get_many is not None:
624+
payload["description_get_many"] = description_get_many
625+
if description_search is not None:
626+
payload["description_search"] = description_search
627+
if description_schema is not None:
628+
payload["description_schema"] = description_schema
597629
data = self.request(
598630
"PUT", f"{self._api_folders_root(api_key)}/{folder_key}/", json_body=payload
599631
)
@@ -2515,12 +2547,24 @@ async def add_api_folder(
25152547
folder_key: FolderRef,
25162548
*,
25172549
allowed_methods: list[str] | None = None,
2550+
description_get_one: str | None = None,
2551+
description_get_many: str | None = None,
2552+
description_search: str | None = None,
2553+
description_schema: str | None = None,
25182554
) -> APIFolderSummary:
25192555
api_key = _resolve_key(api_key)
25202556
folder_key = _resolve_key(folder_key)
25212557
payload: dict[str, Any] = {"folder": folder_key}
2522-
if allowed_methods:
2558+
if allowed_methods is not None:
25232559
payload["allowed_methods"] = allowed_methods
2560+
if description_get_one is not None:
2561+
payload["description_get_one"] = description_get_one
2562+
if description_get_many is not None:
2563+
payload["description_get_many"] = description_get_many
2564+
if description_search is not None:
2565+
payload["description_search"] = description_search
2566+
if description_schema is not None:
2567+
payload["description_schema"] = description_schema
25242568
data = await self.request(
25252569
"POST", f"{self._api_folders_root(api_key)}/", json_body=payload
25262570
)
@@ -2542,12 +2586,24 @@ async def update_api_folder(
25422586
folder_key: FolderRef,
25432587
*,
25442588
allowed_methods: list[str] | None = None,
2589+
description_get_one: str | None = None,
2590+
description_get_many: str | None = None,
2591+
description_search: str | None = None,
2592+
description_schema: str | None = None,
25452593
) -> APIFolderSummary:
25462594
api_key = _resolve_key(api_key)
25472595
folder_key = _resolve_key(folder_key)
25482596
payload: dict[str, Any] = {}
25492597
if allowed_methods is not None:
25502598
payload["allowed_methods"] = allowed_methods
2599+
if description_get_one is not None:
2600+
payload["description_get_one"] = description_get_one
2601+
if description_get_many is not None:
2602+
payload["description_get_many"] = description_get_many
2603+
if description_search is not None:
2604+
payload["description_search"] = description_search
2605+
if description_schema is not None:
2606+
payload["description_schema"] = description_schema
25512607
data = await self.request(
25522608
"PUT", f"{self._api_folders_root(api_key)}/{folder_key}/", json_body=payload
25532609
)

src/foxnose_sdk/management/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ class APIFolderSummary(BaseModel):
297297
api: str | None = None
298298
path: str | None = None
299299
allowed_methods: list[str] | None = None
300+
description_get_one: str | None = None
301+
description_get_many: str | None = None
302+
description_search: str | None = None
303+
description_schema: str | None = None
300304
created_at: datetime | None = None
301305

302306

0 commit comments

Comments
 (0)