pyfortiaigate 0.1.0__tar.gz
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.
- pyfortiaigate-0.1.0/.env.example +27 -0
- pyfortiaigate-0.1.0/.github/workflows/ci.yml +53 -0
- pyfortiaigate-0.1.0/.github/workflows/docs.yml +41 -0
- pyfortiaigate-0.1.0/.github/workflows/release.yml +36 -0
- pyfortiaigate-0.1.0/.gitignore +13 -0
- pyfortiaigate-0.1.0/CHANGELOG.md +35 -0
- pyfortiaigate-0.1.0/LICENSE +21 -0
- pyfortiaigate-0.1.0/PKG-INFO +113 -0
- pyfortiaigate-0.1.0/README.md +69 -0
- pyfortiaigate-0.1.0/docs/actions.md +121 -0
- pyfortiaigate-0.1.0/docs/changelog.md +1 -0
- pyfortiaigate-0.1.0/docs/guide/approval.md +45 -0
- pyfortiaigate-0.1.0/docs/guide/auth.md +46 -0
- pyfortiaigate-0.1.0/docs/guide/gateway.md +36 -0
- pyfortiaigate-0.1.0/docs/guide/reliability.md +40 -0
- pyfortiaigate-0.1.0/docs/index.md +75 -0
- pyfortiaigate-0.1.0/docs/reference/client.md +3 -0
- pyfortiaigate-0.1.0/docs/reference/helpers.md +39 -0
- pyfortiaigate-0.1.0/mkdocs.yml +80 -0
- pyfortiaigate-0.1.0/pyproject.toml +78 -0
- pyfortiaigate-0.1.0/src/pyfortiaigate/__init__.py +37 -0
- pyfortiaigate-0.1.0/src/pyfortiaigate/client.py +942 -0
- pyfortiaigate-0.1.0/src/pyfortiaigate/py.typed +0 -0
- pyfortiaigate-0.1.0/tests/__init__.py +0 -0
- pyfortiaigate-0.1.0/tests/conftest.py +148 -0
- pyfortiaigate-0.1.0/tests/live/__init__.py +0 -0
- pyfortiaigate-0.1.0/tests/live/conftest.py +27 -0
- pyfortiaigate-0.1.0/tests/live/test_live_readonly.py +59 -0
- pyfortiaigate-0.1.0/tests/live/test_live_write.py +264 -0
- pyfortiaigate-0.1.0/tests/test_auth.py +191 -0
- pyfortiaigate-0.1.0/tests/test_docs.py +21 -0
- pyfortiaigate-0.1.0/tests/test_endpoints.py +328 -0
- pyfortiaigate-0.1.0/tests/test_env_loader.py +32 -0
- pyfortiaigate-0.1.0/tests/test_gateway.py +77 -0
- pyfortiaigate-0.1.0/tests/test_helpers.py +189 -0
- pyfortiaigate-0.1.0/tests/test_transport.py +192 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# pyfortiaigate live tests: copy to .env and fill in. .env is gitignored.
|
|
2
|
+
# Variables already set in your shell win over this file.
|
|
3
|
+
# The live tests only READ (me, list calls, one page of audit logs).
|
|
4
|
+
|
|
5
|
+
# Set to 1 to run tests/live once the values below are filled in (skipped otherwise)
|
|
6
|
+
FAIG_LIVE=
|
|
7
|
+
|
|
8
|
+
# Management URL: https://host, a bare host, or https://host/api (e.g. https://faig.example.com)
|
|
9
|
+
FAIG_URL=
|
|
10
|
+
|
|
11
|
+
# Session login: the account's email (the built-in admin is "admin")
|
|
12
|
+
FAIG_USERNAME=
|
|
13
|
+
FAIG_PASSWORD=
|
|
14
|
+
|
|
15
|
+
# Or a bearer token instead of a username/password
|
|
16
|
+
FAIG_TOKEN=
|
|
17
|
+
|
|
18
|
+
# TLS: a CA file that signed the appliance's certificate (preferred), or
|
|
19
|
+
# FAIG_VERIFY_TLS=false to skip checking (isolated labs / localhost tunnels only)
|
|
20
|
+
FAIG_CA_BUNDLE=
|
|
21
|
+
FAIG_VERIFY_TLS=
|
|
22
|
+
|
|
23
|
+
# Write tests (tests/live/test_live_write.py): the workspace they may write in.
|
|
24
|
+
# They create pytest-* objects there and delete them afterwards. Blank = skipped.
|
|
25
|
+
FAIG_WRITE_WORKSPACE=
|
|
26
|
+
# Optional: model for the two gateway chat calls (default: workspace's first OpenAI model)
|
|
27
|
+
FAIG_TEST_MODEL=
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ]
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v5
|
|
22
|
+
with:
|
|
23
|
+
# hatch-vcs derives the version from tags.
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v6
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
cache: pip
|
|
30
|
+
cache-dependency-path: '**/pyproject.toml'
|
|
31
|
+
|
|
32
|
+
- name: Install
|
|
33
|
+
run: |
|
|
34
|
+
python -m pip install --upgrade pip
|
|
35
|
+
pip install -e ".[test,typecheck,lint]"
|
|
36
|
+
|
|
37
|
+
- name: Lint (ruff format check + lint)
|
|
38
|
+
run: |
|
|
39
|
+
ruff format --check .
|
|
40
|
+
ruff check .
|
|
41
|
+
|
|
42
|
+
- name: Type check (mypy --strict)
|
|
43
|
+
run: mypy src
|
|
44
|
+
|
|
45
|
+
- name: Test (coverage floor in pyproject.toml)
|
|
46
|
+
run: pytest -q --cov --cov-report=term-missing
|
|
47
|
+
|
|
48
|
+
# The docs toolchain needs a current Python; one build is enough.
|
|
49
|
+
- name: Docs build
|
|
50
|
+
if: matrix.python-version == '3.12'
|
|
51
|
+
run: |
|
|
52
|
+
pip install -e ".[docs]"
|
|
53
|
+
mkdocs build --strict
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
pages: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: pages
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v5
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0
|
|
24
|
+
- uses: actions/setup-python@v6
|
|
25
|
+
with:
|
|
26
|
+
python-version: '3.12'
|
|
27
|
+
- run: pip install -e ".[docs]"
|
|
28
|
+
- run: mkdocs build --strict
|
|
29
|
+
- uses: actions/upload-pages-artifact@v3
|
|
30
|
+
with:
|
|
31
|
+
path: site
|
|
32
|
+
|
|
33
|
+
deploy:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment:
|
|
37
|
+
name: github-pages
|
|
38
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
39
|
+
steps:
|
|
40
|
+
- id: deployment
|
|
41
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [ created ]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
uses: ./.github/workflows/ci.yml
|
|
13
|
+
|
|
14
|
+
deploy:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
needs: [ test ]
|
|
17
|
+
environment: release
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read # checkout
|
|
20
|
+
id-token: write # PyPI trusted publishing (OIDC)
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v5
|
|
23
|
+
with:
|
|
24
|
+
# hatch-vcs derives the package version from the release tag.
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-python@v6
|
|
28
|
+
with:
|
|
29
|
+
python-version: '3.11'
|
|
30
|
+
|
|
31
|
+
- run: pip install build
|
|
32
|
+
|
|
33
|
+
- run: python -m build
|
|
34
|
+
|
|
35
|
+
- name: Publish to PyPI (trusted publishing, no token)
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-10-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `FortiAIGate` client for the 8.x management API and OpenAI-compatible
|
|
14
|
+
gateway: users and invitations, workspaces, detectors, guardrails, policies,
|
|
15
|
+
the approval workflow, virtual keys, providers, model groups, DLP labels,
|
|
16
|
+
audit logs, `check_key` and `chat_completion`.
|
|
17
|
+
- Session login with re-login on expiry, a CSRF token per mutation, bearer
|
|
18
|
+
token auth, and `verify` as a CA bundle path.
|
|
19
|
+
- Transport retries that never resend a mutation that may have landed, on one
|
|
20
|
+
keep-alive connection.
|
|
21
|
+
- Body builders (`regex_detector_body`, `dlp_detector_body`, `guardrail_body`)
|
|
22
|
+
and `approve_new`.
|
|
23
|
+
- `delete_detector`, `delete_guardrail`, `delete_policy`; `key_update_body`.
|
|
24
|
+
- Live test suites: read-only, and opt-in writes confined to one workspace
|
|
25
|
+
(`FAIG_WRITE_WORKSPACE`) that delete everything they create. Settings load
|
|
26
|
+
from a gitignored `.env` (template: `.env.example`).
|
|
27
|
+
|
|
28
|
+
### Verified live on FortiAIGate 8.0.2
|
|
29
|
+
|
|
30
|
+
- `me()` reads `GET /api/v1/user` (`/api/v1/me` does not exist).
|
|
31
|
+
- `create_policy` takes `provider_ids`: a policy that grants no model is
|
|
32
|
+
refused with HTTP 422.
|
|
33
|
+
- `update_virtual_key` sends only the fields a key PUT accepts, with
|
|
34
|
+
`policyIds` as plain strings; resending the whole record is refused.
|
|
35
|
+
- `revoke_virtual_key` sends the `reason` the API requires.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dylan Spille
|
|
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.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pyfortiaigate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the FortiAIGate 8.x management and gateway API, standard library only
|
|
5
|
+
Project-URL: Homepage, https://github.com/ftnt-dspille/pyfortiaigate
|
|
6
|
+
Project-URL: Repository, https://github.com/ftnt-dspille/pyfortiaigate.git
|
|
7
|
+
Project-URL: Issues, https://github.com/ftnt-dspille/pyfortiaigate/issues
|
|
8
|
+
Author: Dylan Spille
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai-gateway,api,fortiaigate,fortinet,guardrails
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
|
|
26
|
+
Requires-Dist: mkdocs<2,>=1.6; extra == 'dev'
|
|
27
|
+
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff==0.16.10; extra == 'dev'
|
|
32
|
+
Provides-Extra: docs
|
|
33
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
34
|
+
Requires-Dist: mkdocs<2,>=1.6; extra == 'docs'
|
|
35
|
+
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
|
|
36
|
+
Provides-Extra: lint
|
|
37
|
+
Requires-Dist: ruff==0.16.10; extra == 'lint'
|
|
38
|
+
Provides-Extra: test
|
|
39
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'test'
|
|
40
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
41
|
+
Provides-Extra: typecheck
|
|
42
|
+
Requires-Dist: mypy>=1.13; extra == 'typecheck'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# pyfortiaigate
|
|
46
|
+
|
|
47
|
+
Python client for the **FortiAIGate 8.x** management API and its
|
|
48
|
+
OpenAI-compatible gateway. Standard library only: no dependencies, and the
|
|
49
|
+
client is one self-contained module you can copy onto a host that cannot
|
|
50
|
+
`pip install`.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pyfortiaigate
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from pyfortiaigate import FortiAIGate, approve_new, regex_detector_body
|
|
58
|
+
|
|
59
|
+
with FortiAIGate("https://faig.example.com", "admin", "<password>") as faig:
|
|
60
|
+
ws = faig.workspace_by_name("team-a")
|
|
61
|
+
det = faig.create_detector(
|
|
62
|
+
ws["id"], regex_detector_body("no-ssn", [("ssn", r"\d{3}-\d{2}-\d{4}", "block")])
|
|
63
|
+
)
|
|
64
|
+
approve_new(faig, ws["id"], "detectors", det) # draft -> pending -> active
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What it handles for you
|
|
68
|
+
|
|
69
|
+
- **Auth:** session login (logs in on first call, again when the session
|
|
70
|
+
expires) or a bearer token; a fresh CSRF token for every mutation.
|
|
71
|
+
- **Flaky networks:** transport failures are retried with backoff. A request
|
|
72
|
+
that was never answered is retried for every method; a connection dropped
|
|
73
|
+
after the request went out is retried only for GET, so a mutation is never
|
|
74
|
+
sent twice.
|
|
75
|
+
- **One connection:** requests reuse a keep-alive HTTPS connection, which
|
|
76
|
+
matters through tunnels where every new connect costs seconds.
|
|
77
|
+
- **Undocumented behaviour:** body builders encode what the API silently
|
|
78
|
+
requires (a regex detector's flat `rules` list, DLP `disposition`, at least
|
|
79
|
+
one detector per guardrail).
|
|
80
|
+
|
|
81
|
+
## Coverage
|
|
82
|
+
|
|
83
|
+
Users and invitations, workspaces, detectors (custom and built-in), guardrails
|
|
84
|
+
(including scan tests), policies, the approval workflow, virtual keys (create,
|
|
85
|
+
update, rotate, revoke), AI providers, model groups, DLP labels, audit logs,
|
|
86
|
+
and the gateway (`check_key`, `chat_completion`).
|
|
87
|
+
|
|
88
|
+
Full reference: **https://ftnt-dspille.github.io/pyfortiaigate/**
|
|
89
|
+
|
|
90
|
+
## TLS
|
|
91
|
+
|
|
92
|
+
Verification is on by default. For an appliance with a self-signed
|
|
93
|
+
certificate, pass its CA: `FortiAIGate(url, user, pw, verify="/path/ca.pem")`.
|
|
94
|
+
`verify=False` turns checking off and is for isolated labs only.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
uv venv && uv pip install -e ".[dev]"
|
|
100
|
+
.venv/bin/pytest # offline suite, coverage enforced
|
|
101
|
+
.venv/bin/ruff format --check . && .venv/bin/ruff check .
|
|
102
|
+
.venv/bin/mypy src # strict
|
|
103
|
+
.venv/bin/mkdocs serve # docs at http://127.0.0.1:8000
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Live tests are opt-in and read-only. Fill in `.env` (gitignored; copied from
|
|
107
|
+
`.env.example`), set `FAIG_LIVE=1`, then:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
.venv/bin/pytest tests/live -v
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Unofficial; not affiliated with or supported by Fortinet. MIT licensed.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# pyfortiaigate
|
|
2
|
+
|
|
3
|
+
Python client for the **FortiAIGate 8.x** management API and its
|
|
4
|
+
OpenAI-compatible gateway. Standard library only: no dependencies, and the
|
|
5
|
+
client is one self-contained module you can copy onto a host that cannot
|
|
6
|
+
`pip install`.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install pyfortiaigate
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from pyfortiaigate import FortiAIGate, approve_new, regex_detector_body
|
|
14
|
+
|
|
15
|
+
with FortiAIGate("https://faig.example.com", "admin", "<password>") as faig:
|
|
16
|
+
ws = faig.workspace_by_name("team-a")
|
|
17
|
+
det = faig.create_detector(
|
|
18
|
+
ws["id"], regex_detector_body("no-ssn", [("ssn", r"\d{3}-\d{2}-\d{4}", "block")])
|
|
19
|
+
)
|
|
20
|
+
approve_new(faig, ws["id"], "detectors", det) # draft -> pending -> active
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## What it handles for you
|
|
24
|
+
|
|
25
|
+
- **Auth:** session login (logs in on first call, again when the session
|
|
26
|
+
expires) or a bearer token; a fresh CSRF token for every mutation.
|
|
27
|
+
- **Flaky networks:** transport failures are retried with backoff. A request
|
|
28
|
+
that was never answered is retried for every method; a connection dropped
|
|
29
|
+
after the request went out is retried only for GET, so a mutation is never
|
|
30
|
+
sent twice.
|
|
31
|
+
- **One connection:** requests reuse a keep-alive HTTPS connection, which
|
|
32
|
+
matters through tunnels where every new connect costs seconds.
|
|
33
|
+
- **Undocumented behaviour:** body builders encode what the API silently
|
|
34
|
+
requires (a regex detector's flat `rules` list, DLP `disposition`, at least
|
|
35
|
+
one detector per guardrail).
|
|
36
|
+
|
|
37
|
+
## Coverage
|
|
38
|
+
|
|
39
|
+
Users and invitations, workspaces, detectors (custom and built-in), guardrails
|
|
40
|
+
(including scan tests), policies, the approval workflow, virtual keys (create,
|
|
41
|
+
update, rotate, revoke), AI providers, model groups, DLP labels, audit logs,
|
|
42
|
+
and the gateway (`check_key`, `chat_completion`).
|
|
43
|
+
|
|
44
|
+
Full reference: **https://ftnt-dspille.github.io/pyfortiaigate/**
|
|
45
|
+
|
|
46
|
+
## TLS
|
|
47
|
+
|
|
48
|
+
Verification is on by default. For an appliance with a self-signed
|
|
49
|
+
certificate, pass its CA: `FortiAIGate(url, user, pw, verify="/path/ca.pem")`.
|
|
50
|
+
`verify=False` turns checking off and is for isolated labs only.
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv venv && uv pip install -e ".[dev]"
|
|
56
|
+
.venv/bin/pytest # offline suite, coverage enforced
|
|
57
|
+
.venv/bin/ruff format --check . && .venv/bin/ruff check .
|
|
58
|
+
.venv/bin/mypy src # strict
|
|
59
|
+
.venv/bin/mkdocs serve # docs at http://127.0.0.1:8000
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Live tests are opt-in and read-only. Fill in `.env` (gitignored; copied from
|
|
63
|
+
`.env.example`), set `FAIG_LIVE=1`, then:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
.venv/bin/pytest tests/live -v
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Unofficial; not affiliated with or supported by Fortinet. MIT licensed.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Actions
|
|
2
|
+
|
|
3
|
+
Every client method and the API call it makes. `{ws}` is a workspace id; list
|
|
4
|
+
methods return the records (unwrapped from `{"items": [...]}`), the rest return
|
|
5
|
+
the decoded JSON response.
|
|
6
|
+
|
|
7
|
+
## Session & raw calls
|
|
8
|
+
|
|
9
|
+
| Method | HTTP | Notes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `login(username, password)` | `POST /api/v1/login` | Optional: the client logs in on its first call. Falls back to the legacy `/api/login`. |
|
|
12
|
+
| `close()` | `POST /api/v1/logout` | Also runs on leaving a `with` block. |
|
|
13
|
+
| `me()` | `GET /api/v1/user` | The logged-in user: `email`, `role`, `memberships`, ... |
|
|
14
|
+
| `request(method, path, body, params)` | any | Raw management call with auth, CSRF and retries. |
|
|
15
|
+
| `get(path, params)` | `GET` | |
|
|
16
|
+
| `post(path, body)` | `POST` | Sends `{}` when there is no body. |
|
|
17
|
+
| `put(path, body)` | `PUT` | |
|
|
18
|
+
| `patch(path, body)` | `PATCH` | |
|
|
19
|
+
| `delete(path)` | `DELETE` | |
|
|
20
|
+
|
|
21
|
+
## Users & invitations
|
|
22
|
+
|
|
23
|
+
| Method | HTTP | Notes |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `users()` | `GET /api/v1/admin/users` | |
|
|
26
|
+
| `user(user_id)` | `GET /api/v1/admin/users/{id}` | |
|
|
27
|
+
| `user_by_email(email)` | `GET /api/v1/admin/users` | Case-insensitive match; `None` if absent. |
|
|
28
|
+
| `invite(email, role)` | `POST /api/v1/admin/user-invitations` | Users exist only via invitation; no email is sent. |
|
|
29
|
+
| `accept_invite(invitation_id, email, password, display_name)` | `POST /api/v1/invitations/{id}/accept` | Unauthenticated; sets the password. |
|
|
30
|
+
| `delete_user(user_id)` | `DELETE /api/v1/admin/users/{id}` | |
|
|
31
|
+
| `make_workspace_admin(user_id, ws_id)` | `GET` + `PUT /api/v1/admin/users/{id}` | Keeps the user's other memberships. |
|
|
32
|
+
|
|
33
|
+
## Workspaces
|
|
34
|
+
|
|
35
|
+
| Method | HTTP | Notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `workspaces()` | `GET /api/v1/admin/workspaces` | Every workspace (org admin). |
|
|
38
|
+
| `my_workspaces()` | `GET /api/v1/me/workspaces` | Workspaces the caller belongs to. |
|
|
39
|
+
| `workspace_by_name(name)` | `GET /api/v1/admin/workspaces` | |
|
|
40
|
+
| `create_workspace(name, admin_ids, provider_ids, description)` | `POST /api/v1/admin/workspaces` | Admins must be existing users. |
|
|
41
|
+
| `delete_workspace(ws_id)` | `DELETE /api/v1/admin/workspaces/{ws}` | |
|
|
42
|
+
| `workspace_members(ws_id)` | `GET /api/v1/workspaces/{ws}/members` | |
|
|
43
|
+
| `workspace_budget(ws_id)` | `GET /api/v1/workspaces/{ws}/budget` | |
|
|
44
|
+
| `workspace_analytics(ws_id)` | `GET /api/v1/workspaces/{ws}/analytics` | |
|
|
45
|
+
|
|
46
|
+
## Detectors
|
|
47
|
+
|
|
48
|
+
| Method | HTTP | Notes |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `detectors(ws_id=None)` | `GET /api/v1/workspaces/{ws}/detectors` or `GET /api/v1/admin/detectors` | Custom detectors. |
|
|
51
|
+
| `builtin_detectors()` | `GET /api/v1/admin/built-in-detectors` | Identified by slug id. |
|
|
52
|
+
| `detector_by_name(ws_id, name)` | `GET /api/v1/workspaces/{ws}/detectors` | |
|
|
53
|
+
| `create_detector(ws_id, body)` | `POST /api/v1/workspaces/{ws}/detectors` | Build `body` with `regex_detector_body` / `dlp_detector_body`. |
|
|
54
|
+
| `update_detector(ws_id, uuid, body)` | `PUT /api/v1/workspaces/{ws}/detectors/{uuid}` | An active detector changes at once. |
|
|
55
|
+
| `delete_detector(uuid)` | `DELETE /api/v1/admin/detectors/{uuid}` | |
|
|
56
|
+
| `test_detector(ws_id, uuid, text)` | `POST /api/v1/workspaces/{ws}/detectors/{uuid}/test` | |
|
|
57
|
+
|
|
58
|
+
## Guardrails
|
|
59
|
+
|
|
60
|
+
| Method | HTTP | Notes |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `guardrails(ws_id=None)` | `GET /api/v1/workspaces/{ws}/guardrails` or `GET /api/v1/admin/guardrails` | |
|
|
63
|
+
| `guardrail(gid)` | `GET /api/v1/admin/guardrails/{id}` | |
|
|
64
|
+
| `guardrail_by_name(ws_id, name)` | as `guardrails` | `ws_id=None` searches org guardrails. |
|
|
65
|
+
| `create_guardrail(ws_id, name, detector_ids, description)` | `POST /api/v1/workspaces/{ws}/guardrails` | Needs at least one detector. |
|
|
66
|
+
| `update_guardrail(ws_id, gid, name, detector_ids, description)` | `PUT /api/v1/workspaces/{ws}/guardrails/{id}` | |
|
|
67
|
+
| `delete_guardrail(gid, ws_id=None)` | `DELETE /api/v1/workspaces/{ws}/guardrails/{id}` or `DELETE /api/v1/admin/guardrails/{id}` | |
|
|
68
|
+
| `test_guardrail(text, detector_ids, guardrail_id=, deny_suspicious_characters=)` | `POST /api/v1/admin/guardrails/test` | Scans text without an LLM. |
|
|
69
|
+
|
|
70
|
+
## Policies
|
|
71
|
+
|
|
72
|
+
| Method | HTTP | Notes |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `policies(ws_id=None)` | `GET /api/v1/workspaces/{ws}/policies` or `GET /api/v1/admin/policies` | |
|
|
75
|
+
| `policy(pid)` | `GET /api/v1/admin/policies/{id}` | |
|
|
76
|
+
| `policy_by_name(ws_id, name)` | as `policies` | |
|
|
77
|
+
| `create_policy(ws_id, name, input_guardrail_ids, output_guardrail_ids, provider_ids=, description=, response_scan_mode=)` | `POST /api/v1/workspaces/{ws}/policies` | `provider_ids` grants all of those providers' models; some builds require at least one. |
|
|
78
|
+
|
|
79
|
+
| `delete_policy(pid, ws_id=None)` | `DELETE /api/v1/workspaces/{ws}/policies/{id}` or `DELETE /api/v1/admin/policies/{id}` | |
|
|
80
|
+
|
|
81
|
+
## Approval workflow
|
|
82
|
+
|
|
83
|
+
| Method | HTTP | Notes |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `submit(ws_id, kind, obj_id)` | `POST /api/v1/workspaces/{ws}/{kind}/{id}/submit` | `kind`: `detectors`, `guardrails`, `policies`. |
|
|
86
|
+
| `approve(kind, obj_id)` | `POST /api/v1/admin/{kind}/{id}/approve` | |
|
|
87
|
+
| `reject(kind, obj_id, reason)` | `POST /api/v1/admin/{kind}/{id}/reject` | |
|
|
88
|
+
| `review_queue()` | `GET /api/v1/admin/review-queue` | Map an item's `type` with `QUEUE_KIND`. |
|
|
89
|
+
|
|
90
|
+
See also [`approve_new`](reference/helpers.md#pyfortiaigate.approve_new).
|
|
91
|
+
|
|
92
|
+
## Virtual keys
|
|
93
|
+
|
|
94
|
+
| Method | HTTP | Notes |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `virtual_keys(ws_id=None, status=)` | `GET /api/v1/workspaces/{ws}/virtual-keys` or `GET /api/v1/admin/virtual-keys` | |
|
|
97
|
+
| `virtual_key(key_id)` | `GET /api/v1/admin/virtual-keys/{id}` | |
|
|
98
|
+
| `create_virtual_key(ws_id, name, policy_ids=, provider_ids=, expires_at=, tpm=, rpm=)` | `POST /api/v1/workspaces/{ws}/virtual-keys` | The secret is returned once. |
|
|
99
|
+
| `update_virtual_key(key_id, expires_at=, tpm=, rpm=, policy_ids=)` | `GET` + `PUT /api/v1/admin/virtual-keys/{id}` | Resends the record's writable fields with your changes. |
|
|
100
|
+
| `rotate_virtual_key(key_id)` | `POST /api/v1/admin/virtual-keys/{id}/rotate` | New secret, once. |
|
|
101
|
+
| `revoke_virtual_key(key_id, reason)` | `POST /api/v1/admin/virtual-keys/{id}/revoke` | The secret stops working; a reason is required. |
|
|
102
|
+
| `delete_virtual_key(key_id)` | `DELETE /api/v1/admin/virtual-keys/{id}` | |
|
|
103
|
+
|
|
104
|
+
## Providers, models & audit
|
|
105
|
+
|
|
106
|
+
| Method | HTTP | Notes |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `ai_providers()` | `GET /api/v1/admin/ai-providers` | |
|
|
109
|
+
| `model_groups()` | `GET /api/v1/model-groups` | Member public names are what `chat_completion` takes as `model`. |
|
|
110
|
+
| `dlp_labels()` | `GET /api/v1/admin/dlp-labels` | Category names for `dlp_detector_body`. |
|
|
111
|
+
| `audit_logs(offset, limit)` | `GET /api/v1/admin/audit-logs` | |
|
|
112
|
+
| `audit_log_filter_options()` | `GET /api/v1/admin/audit-logs/filter-options` | |
|
|
113
|
+
|
|
114
|
+
## Gateway (data plane)
|
|
115
|
+
|
|
116
|
+
Authenticated with a virtual key secret, never the session.
|
|
117
|
+
|
|
118
|
+
| Method | HTTP | Notes |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `check_key(api_key)` | `GET /ai/openai/v1/models` | `{"valid", "model_count", "http_status"}`. |
|
|
121
|
+
| `chat_completion(api_key, model, messages, max_tokens=, temperature=, **extra)` | `POST /ai/openai/v1/chat/completions` | A guardrail block is a 200; test it with `blocked()`. |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--8<-- "CHANGELOG.md"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Approval workflow
|
|
2
|
+
|
|
3
|
+
Detectors, guardrails and policies created inside a workspace start as
|
|
4
|
+
**draft** and only take effect once an org admin approves them:
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
draft --submit--> pending --approve--> active
|
|
8
|
+
--reject---> rejected
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Editing an **active** guardrail or detector applies immediately, with no
|
|
12
|
+
re-approval.
|
|
13
|
+
|
|
14
|
+
## As the org admin
|
|
15
|
+
|
|
16
|
+
`approve_new` walks a freshly created object to active in one call:
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
det = faig.create_detector(ws_id, body)
|
|
20
|
+
approve_new(faig, ws_id, "detectors", det)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If another approver gets there first (the object is no longer pending), that is
|
|
24
|
+
not an error.
|
|
25
|
+
|
|
26
|
+
## Reviewing what others submitted
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pyfortiaigate import QUEUE_KIND
|
|
30
|
+
|
|
31
|
+
for item in faig.review_queue():
|
|
32
|
+
faig.approve(QUEUE_KIND[item["type"]], item["id"])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Body builders
|
|
36
|
+
|
|
37
|
+
The API accepts bodies that store fine and then do nothing. The builders encode
|
|
38
|
+
the shapes that work:
|
|
39
|
+
|
|
40
|
+
- `regex_detector_body(name, [(rule_name, pattern, disposition)])` fills the
|
|
41
|
+
flat `rules` list the engine matches on, not just the editor's `regexRules`
|
|
42
|
+
(which alone matches nothing), and refuses an empty rule list.
|
|
43
|
+
- `dlp_detector_body(name, categories)` uses `disposition` (not `action`);
|
|
44
|
+
category names come from `dlp_labels()`.
|
|
45
|
+
- `guardrail_body(name, detector_ids)` refuses a guardrail with no detectors.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Authentication & TLS
|
|
2
|
+
|
|
3
|
+
## Session login
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
faig = FortiAIGate("https://faig.example.com", "a@example.com", "<password>")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The login name is the account's **email** (the built-in administrator is
|
|
10
|
+
`admin`). The client logs in on its first call, not in the constructor, so
|
|
11
|
+
creating a client never touches the network. Call `login()` to fail fast.
|
|
12
|
+
|
|
13
|
+
Sessions expire (about 24 minutes on 8.0.2). A `401` means nothing was applied,
|
|
14
|
+
so the client logs in again and resends the request once. A second `401` raises
|
|
15
|
+
`FaigError`.
|
|
16
|
+
|
|
17
|
+
Every mutation (POST, PUT, PATCH, DELETE) carries a fresh `X-CSRF-Token`
|
|
18
|
+
fetched just before it. A `403` that names the CSRF token is retried once with
|
|
19
|
+
a new token.
|
|
20
|
+
|
|
21
|
+
## Bearer token
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
faig = FortiAIGate("https://faig.example.com", token="<token>")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No login and no CSRF exchange; a `401` is raised as-is.
|
|
28
|
+
|
|
29
|
+
## Virtual keys are not API credentials
|
|
30
|
+
|
|
31
|
+
A virtual key (`faig_...`) authenticates on the **gateway** only. Pass it to
|
|
32
|
+
`check_key` or `chat_completion`; it is never accepted on `/api/v1/*`.
|
|
33
|
+
|
|
34
|
+
## TLS
|
|
35
|
+
|
|
36
|
+
| `verify=` | Behaviour |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `True` (default) | System trust store. |
|
|
39
|
+
| `"/path/ca.pem"` | Trust the CA that signed the appliance's certificate. Use this for a self-signed appliance. |
|
|
40
|
+
| `False` | No verification. Only for an isolated lab you control. |
|
|
41
|
+
|
|
42
|
+
## Errors
|
|
43
|
+
|
|
44
|
+
Every HTTP status >= 400 raises `FaigError` with `.status`, `.method`, `.path`
|
|
45
|
+
and the raw response `.body`. The message uses the response's `error`,
|
|
46
|
+
`message` or `detail` field when there is one.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Virtual keys & the gateway
|
|
2
|
+
|
|
3
|
+
## Creating a key
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
key = faig.create_virtual_key(ws_id, "app", policy_ids=[policy_id], provider_ids=["openai"])
|
|
7
|
+
secret = secret_from_response(key) # returned ONCE; store it now
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The secret is never readable again. It embeds the key's id, so code that only
|
|
11
|
+
holds the secret can still manage the key:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
faig.revoke_virtual_key(key_id_from_secret(secret))
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`rotate_virtual_key` returns a new secret, once, the same way.
|
|
18
|
+
`update_virtual_key` reads the current record and resends it with your changes,
|
|
19
|
+
because the API replaces the whole record; `policy_ids` replaces the attached
|
|
20
|
+
policy list.
|
|
21
|
+
|
|
22
|
+
## Using a key
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
faig.check_key(secret)
|
|
26
|
+
# {"valid": True, "model_count": 4, "http_status": 200}
|
|
27
|
+
|
|
28
|
+
reply = faig.chat_completion(secret, "gpt-4o-mini", [{"role": "user", "content": "hi"}])
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The gateway applies the key's policies. **A guardrail block is not an error:**
|
|
32
|
+
the gateway answers HTTP 200 with `finish_reason: "content_filter"`. Test for it
|
|
33
|
+
with `blocked(reply)`.
|
|
34
|
+
|
|
35
|
+
If the gateway is served from a different host than the management console,
|
|
36
|
+
pass `gateway_url=` to the client.
|