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.
Files changed (36) hide show
  1. pyfortiaigate-0.1.0/.env.example +27 -0
  2. pyfortiaigate-0.1.0/.github/workflows/ci.yml +53 -0
  3. pyfortiaigate-0.1.0/.github/workflows/docs.yml +41 -0
  4. pyfortiaigate-0.1.0/.github/workflows/release.yml +36 -0
  5. pyfortiaigate-0.1.0/.gitignore +13 -0
  6. pyfortiaigate-0.1.0/CHANGELOG.md +35 -0
  7. pyfortiaigate-0.1.0/LICENSE +21 -0
  8. pyfortiaigate-0.1.0/PKG-INFO +113 -0
  9. pyfortiaigate-0.1.0/README.md +69 -0
  10. pyfortiaigate-0.1.0/docs/actions.md +121 -0
  11. pyfortiaigate-0.1.0/docs/changelog.md +1 -0
  12. pyfortiaigate-0.1.0/docs/guide/approval.md +45 -0
  13. pyfortiaigate-0.1.0/docs/guide/auth.md +46 -0
  14. pyfortiaigate-0.1.0/docs/guide/gateway.md +36 -0
  15. pyfortiaigate-0.1.0/docs/guide/reliability.md +40 -0
  16. pyfortiaigate-0.1.0/docs/index.md +75 -0
  17. pyfortiaigate-0.1.0/docs/reference/client.md +3 -0
  18. pyfortiaigate-0.1.0/docs/reference/helpers.md +39 -0
  19. pyfortiaigate-0.1.0/mkdocs.yml +80 -0
  20. pyfortiaigate-0.1.0/pyproject.toml +78 -0
  21. pyfortiaigate-0.1.0/src/pyfortiaigate/__init__.py +37 -0
  22. pyfortiaigate-0.1.0/src/pyfortiaigate/client.py +942 -0
  23. pyfortiaigate-0.1.0/src/pyfortiaigate/py.typed +0 -0
  24. pyfortiaigate-0.1.0/tests/__init__.py +0 -0
  25. pyfortiaigate-0.1.0/tests/conftest.py +148 -0
  26. pyfortiaigate-0.1.0/tests/live/__init__.py +0 -0
  27. pyfortiaigate-0.1.0/tests/live/conftest.py +27 -0
  28. pyfortiaigate-0.1.0/tests/live/test_live_readonly.py +59 -0
  29. pyfortiaigate-0.1.0/tests/live/test_live_write.py +264 -0
  30. pyfortiaigate-0.1.0/tests/test_auth.py +191 -0
  31. pyfortiaigate-0.1.0/tests/test_docs.py +21 -0
  32. pyfortiaigate-0.1.0/tests/test_endpoints.py +328 -0
  33. pyfortiaigate-0.1.0/tests/test_env_loader.py +32 -0
  34. pyfortiaigate-0.1.0/tests/test_gateway.py +77 -0
  35. pyfortiaigate-0.1.0/tests/test_helpers.py +189 -0
  36. 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,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ dist/
6
+ site/
7
+ .coverage
8
+ coverage.xml
9
+ .ruff_cache/
10
+ .mypy_cache/
11
+ .env
12
+ .env.*
13
+ !.env.example
@@ -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.