aiofortiosapi 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,31 @@
1
+ """aiofortiosapi — async FortiOS REST API client for Home Assistant."""
2
+
3
+ from .client import FortiOSClient
4
+ from .const import DEFAULT_ONLINE_THRESHOLD
5
+ from .exceptions import (
6
+ FortiOSAuthenticationError,
7
+ FortiOSConnectionError,
8
+ FortiOSError,
9
+ FortiOSNotFoundError,
10
+ FortiOSResponseError,
11
+ )
12
+ from .models import DetectedDevice, ResourceUsage, SystemStatus
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "FortiOSClient",
18
+ # exceptions
19
+ "FortiOSError",
20
+ "FortiOSConnectionError",
21
+ "FortiOSAuthenticationError",
22
+ "FortiOSNotFoundError",
23
+ "FortiOSResponseError",
24
+ # models
25
+ "SystemStatus",
26
+ "ResourceUsage",
27
+ "DetectedDevice",
28
+ # constants
29
+ "DEFAULT_ONLINE_THRESHOLD",
30
+ "__version__",
31
+ ]
@@ -0,0 +1,200 @@
1
+ """FortiOSClient — async HTTP client for the FortiOS REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import time
9
+ from typing import Any
10
+
11
+ import aiohttp
12
+
13
+ from .const import (
14
+ DEFAULT_ONLINE_THRESHOLD,
15
+ DEFAULT_PORT,
16
+ DEFAULT_TIMEOUT,
17
+ EP_DETECTED_DEVICES,
18
+ EP_RESOURCE_USAGE,
19
+ EP_SYSTEM_STATUS,
20
+ )
21
+ from .exceptions import (
22
+ FortiOSAuthenticationError,
23
+ FortiOSConnectionError,
24
+ FortiOSNotFoundError,
25
+ FortiOSResponseError,
26
+ )
27
+ from .models import DetectedDevice, ResourceUsage, SystemStatus
28
+
29
+ _LOGGER = logging.getLogger(__name__)
30
+
31
+ # FortiOS error envelopes carry the useful string under different keys
32
+ # depending on firmware and failure mode (e.g. bad token, trusted-host
33
+ # violation, CMDB error). Checked in priority order.
34
+ _ERROR_DETAIL_KEYS: tuple[str, ...] = (
35
+ "cli_error",
36
+ "message",
37
+ "error",
38
+ "http_message",
39
+ )
40
+ _ERROR_DETAIL_MAX_CHARS = 200
41
+
42
+
43
+ def _extract_error_detail(body_text: str) -> str:
44
+ """Return the most useful human-readable string from an error body.
45
+
46
+ Falls back to the raw body (truncated) when the payload is not JSON or
47
+ carries none of the known detail keys.
48
+ """
49
+ try:
50
+ parsed: Any = json.loads(body_text)
51
+ except ValueError:
52
+ return body_text.strip()[:_ERROR_DETAIL_MAX_CHARS]
53
+ if isinstance(parsed, dict):
54
+ for key in _ERROR_DETAIL_KEYS:
55
+ if value := parsed.get(key):
56
+ return str(value)[:_ERROR_DETAIL_MAX_CHARS]
57
+ return body_text.strip()[:_ERROR_DETAIL_MAX_CHARS]
58
+
59
+
60
+ class FortiOSClient:
61
+ """Async client for the FortiOS REST API.
62
+
63
+ The caller (e.g. Home Assistant) owns the aiohttp.ClientSession lifecycle.
64
+ This class never creates or closes the session it receives.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ host: str,
70
+ token: str,
71
+ *,
72
+ session: aiohttp.ClientSession,
73
+ port: int = DEFAULT_PORT,
74
+ verify_ssl: bool = True,
75
+ vdom: str | None = None,
76
+ request_timeout: float = DEFAULT_TIMEOUT,
77
+ device_online_threshold: float = DEFAULT_ONLINE_THRESHOLD,
78
+ ) -> None:
79
+ self._host = host
80
+ self._token = token
81
+ self._session = session
82
+ self._port = port
83
+ self._verify_ssl = verify_ssl
84
+ self._vdom = vdom
85
+ self._request_timeout = request_timeout
86
+ self._device_online_threshold = device_online_threshold
87
+
88
+ # ------------------------------------------------------------------
89
+ # Internal request plumbing
90
+ # ------------------------------------------------------------------
91
+
92
+ def _build_url(self, path: str) -> str:
93
+ return f"https://{self._host}:{self._port}/{path}"
94
+
95
+ async def _request(
96
+ self,
97
+ method: str,
98
+ path: str,
99
+ *,
100
+ params: dict[str, Any] | None = None,
101
+ ) -> dict[str, Any]:
102
+ """Single choke-point for all HTTP.
103
+
104
+ Builds URL, injects auth header, merges vdom param, applies timeout,
105
+ and maps all error conditions to the exception hierarchy.
106
+ """
107
+ url = self._build_url(path)
108
+ merged_params: dict[str, Any] = dict(params) if params else {}
109
+ if self._vdom is not None:
110
+ merged_params["vdom"] = self._vdom
111
+
112
+ headers = {"Authorization": f"Bearer {self._token}"}
113
+ ssl: bool = self._verify_ssl
114
+
115
+ _LOGGER.debug("FortiOS %s %s params=%s", method, url, list(merged_params.keys()))
116
+
117
+ try:
118
+ async with (
119
+ asyncio.timeout(self._request_timeout),
120
+ self._session.request(
121
+ method,
122
+ url,
123
+ headers=headers,
124
+ params=merged_params or None,
125
+ ssl=ssl,
126
+ ) as response,
127
+ ):
128
+ if response.status in (401, 403):
129
+ raise FortiOSAuthenticationError(
130
+ f"Authentication failed (HTTP {response.status})"
131
+ )
132
+ if response.status == 404:
133
+ raise FortiOSNotFoundError(f"Endpoint not found: {path}")
134
+
135
+ if response.status >= 400:
136
+ detail = _extract_error_detail(await response.text())
137
+ raise FortiOSResponseError(f"HTTP {response.status} from {url}: {detail}")
138
+
139
+ try:
140
+ data: dict[str, Any] = await response.json()
141
+ except (aiohttp.ClientError, ValueError) as exc:
142
+ raise FortiOSResponseError(f"Invalid JSON from {url}") from exc
143
+
144
+ return data
145
+ except TimeoutError as exc:
146
+ raise FortiOSConnectionError(f"Request timed out: {url}") from exc
147
+ except aiohttp.ClientError as exc:
148
+ raise FortiOSConnectionError(f"Connection error: {exc}") from exc
149
+
150
+ # ------------------------------------------------------------------
151
+ # Public API methods
152
+ # ------------------------------------------------------------------
153
+
154
+ async def get(
155
+ self,
156
+ path: str,
157
+ *,
158
+ params: dict[str, Any] | None = None,
159
+ ) -> dict[str, Any]:
160
+ """Perform a raw GET and return the parsed JSON envelope.
161
+
162
+ Escape hatch for endpoints without a dedicated typed method: returns
163
+ the full ``{"results": ..., "status": ..., ...}`` dict so the caller
164
+ can extract whatever it needs. Inherits Bearer auth, timeout, and
165
+ the typed exception hierarchy from ``_request``.
166
+ """
167
+ return await self._request("GET", path, params=params)
168
+
169
+ async def get_system_status(self) -> SystemStatus:
170
+ """Return basic device identity: hostname, version, serial, model."""
171
+ raw = await self._request("GET", EP_SYSTEM_STATUS)
172
+ return SystemStatus.from_api(raw)
173
+
174
+ async def get_resource_usage(self) -> ResourceUsage:
175
+ """Return CPU, memory, session count, and uptime."""
176
+ raw = await self._request("GET", EP_RESOURCE_USAGE)
177
+ return ResourceUsage.from_api(raw)
178
+
179
+ async def get_detected_devices(self) -> list[DetectedDevice]:
180
+ """Return devices seen on the network (for device_tracker).
181
+
182
+ ``is_online`` uses the API-reported flag; when a firmware omits it,
183
+ it is derived from ``last_seen`` freshness using the client's
184
+ ``device_online_threshold``.
185
+ """
186
+ raw = await self._request("GET", EP_DETECTED_DEVICES)
187
+ return DetectedDevice.list_from_api(
188
+ raw,
189
+ now=time.time(),
190
+ online_threshold=self._device_online_threshold,
191
+ )
192
+
193
+ async def async_validate(self) -> SystemStatus:
194
+ """Validate credentials cheaply by calling get_system_status.
195
+
196
+ Raises FortiOSAuthenticationError for bad tokens, or
197
+ FortiOSConnectionError for unreachable devices. Used by the HA
198
+ config_flow to confirm a token works before saving the entry.
199
+ """
200
+ return await self.get_system_status()
aiofortiosapi/const.py ADDED
@@ -0,0 +1,29 @@
1
+ """Constants: endpoint paths and defaults.
2
+
3
+ WARNING: FortiOS monitor endpoint paths vary by firmware version and must be
4
+ verified against the target version's API reference (FNDN — Fortinet Developer
5
+ Network). The paths below are known-good starting points for FortiOS 6.4 –
6
+ 8.x (verified on v8.0.1); do NOT assume they exist on older or newer
7
+ firmware without checking.
8
+ """
9
+
10
+ API_BASE = "api/v2"
11
+
12
+ EP_SYSTEM_STATUS = f"{API_BASE}/monitor/system/status"
13
+ EP_RESOURCE_USAGE = f"{API_BASE}/monitor/system/resource/usage"
14
+
15
+ # 7.0+: older firmware used monitor/user/device/select
16
+ EP_DETECTED_DEVICES = f"{API_BASE}/monitor/user/device/query"
17
+
18
+ # Future endpoints (add intentionally, verify paths per target version):
19
+ # monitor/vpn/ipsec — IPsec/VPN tunnel state
20
+ # monitor/system/interface — per-interface traffic stats
21
+ # monitor/system/ha-statistics — HA cluster status
22
+
23
+ DEFAULT_PORT: int = 443
24
+ DEFAULT_TIMEOUT: float = 10.0
25
+
26
+ # Fallback window for deriving device online state: when an API response
27
+ # omits the is_online field, a device counts as online if it was last seen
28
+ # within this many seconds (mirrors HA device_tracker "consider_home").
29
+ DEFAULT_ONLINE_THRESHOLD: float = 300.0
@@ -0,0 +1,35 @@
1
+ """Exception hierarchy for aiofortiosapi.
2
+
3
+ Map to Home Assistant config entry states:
4
+ FortiOSConnectionError -> ConfigEntryNotReady (retry)
5
+ FortiOSAuthenticationError -> ConfigEntryAuthFailed (reauth)
6
+ FortiOSNotFoundError -> inspect / log
7
+ FortiOSResponseError -> log / raise
8
+ """
9
+
10
+
11
+ class FortiOSError(Exception):
12
+ """Base exception for all aiofortiosapi errors."""
13
+
14
+
15
+ class FortiOSConnectionError(FortiOSError):
16
+ """Transport-level or timeout failure.
17
+
18
+ Raised for aiohttp.ClientError and asyncio.TimeoutError.
19
+ Home Assistant should map this to ConfigEntryNotReady.
20
+ """
21
+
22
+
23
+ class FortiOSAuthenticationError(FortiOSError):
24
+ """HTTP 401 or 403 response.
25
+
26
+ Home Assistant should map this to ConfigEntryAuthFailed.
27
+ """
28
+
29
+
30
+ class FortiOSNotFoundError(FortiOSError):
31
+ """HTTP 404 response — endpoint does not exist on this FortiOS version."""
32
+
33
+
34
+ class FortiOSResponseError(FortiOSError):
35
+ """Unexpected or unparseable response (bad JSON, 5xx, other 4xx)."""
@@ -0,0 +1,192 @@
1
+ """Frozen dataclasses for parsed FortiOS API responses.
2
+
3
+ FortiOS wraps all payloads as:
4
+ {"results": <dict|list>, "status": "success", "version": "v7.6.1",
5
+ "serial": "FGT60F...", "vdom": "root", ...}
6
+
7
+ Each model's from_api() accepts the full envelope dict and parses defensively:
8
+ missing keys, null values, and unexpected shapes all fall back to safe
9
+ defaults instead of raising.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+ from dataclasses import dataclass
16
+ from typing import Any
17
+
18
+ from .const import DEFAULT_ONLINE_THRESHOLD
19
+
20
+
21
+ def _first(lst: Any, key: str, default: Any = None) -> Any:
22
+ """Extract key from the first element of a list of dicts, or default.
23
+
24
+ FortiOS returns cpu/memory/session stats as single-element lists of
25
+ objects; tolerate scalar lists, empty lists, and non-list values.
26
+ """
27
+ if isinstance(lst, list) and lst and isinstance(lst[0], dict):
28
+ return lst[0].get(key, default)
29
+ return default
30
+
31
+
32
+ def _as_str(value: Any) -> str:
33
+ """Coerce an optional API string field to str; null and non-str become ''."""
34
+ return value if isinstance(value, str) else ""
35
+
36
+
37
+ def _to_int(value: Any, default: int = 0) -> int:
38
+ try:
39
+ return int(value)
40
+ except (TypeError, ValueError):
41
+ return default
42
+
43
+
44
+ def _to_float(value: Any, default: float = 0.0) -> float:
45
+ try:
46
+ return float(value)
47
+ except (TypeError, ValueError):
48
+ return default
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class SystemStatus:
53
+ """Parsed response from monitor/system/status."""
54
+
55
+ hostname: str
56
+ version: str
57
+ serial: str
58
+ model: str
59
+
60
+ @classmethod
61
+ def from_api(cls, raw: dict[str, Any]) -> SystemStatus:
62
+ results: Any = raw.get("results")
63
+ if not isinstance(results, dict):
64
+ results = {}
65
+ serial = _as_str(raw.get("serial") or results.get("Serial-Number"))
66
+ version = _as_str(raw.get("version") or results.get("Version"))
67
+ hostname = _as_str(results.get("Hostname") or results.get("hostname"))
68
+ # Model name: use FGVM/FGT prefix from serial when not explicit
69
+ model = _as_str(results.get("Model-Name") or results.get("model") or serial[:6])
70
+ return cls(hostname=hostname, version=version, serial=serial, model=model)
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class ResourceUsage:
75
+ """Parsed response from monitor/system/resource/usage.
76
+
77
+ Real FortiOS (verified on v8.0.1) returns each metric as a single-element
78
+ list with a ``current`` value, and splits sessions/setup-rate into IPv4
79
+ and IPv6 buckets:
80
+
81
+ ``{"cpu": [{"current": 0}], "mem": [{"current": 34}],
82
+ "session": [{"current": 279}], "session6": [{"current": 57}],
83
+ "setuprate": [{"current": 7}], "setuprate6": [{"current": 0}], ...}``
84
+
85
+ ``sessions`` and ``session_setup_rate`` are the sum of the v4 + v6
86
+ buckets.
87
+ """
88
+
89
+ cpu_percent: float
90
+ memory_percent: float
91
+ sessions: int # session + session6
92
+ session_setup_rate: int # setuprate + setuprate6
93
+
94
+ @classmethod
95
+ def from_api(cls, raw: dict[str, Any]) -> ResourceUsage:
96
+ results: Any = raw.get("results")
97
+ if not isinstance(results, dict):
98
+ results = {}
99
+ return cls(
100
+ cpu_percent=_to_float(_first(results.get("cpu"), "current")),
101
+ memory_percent=_to_float(_first(results.get("mem"), "current")),
102
+ sessions=(
103
+ _to_int(_first(results.get("session"), "current"))
104
+ + _to_int(_first(results.get("session6"), "current"))
105
+ ),
106
+ session_setup_rate=(
107
+ _to_int(_first(results.get("setuprate"), "current"))
108
+ + _to_int(_first(results.get("setuprate6"), "current"))
109
+ ),
110
+ )
111
+
112
+
113
+ @dataclass(frozen=True, slots=True)
114
+ class DetectedDevice:
115
+ """A single device from monitor/user/device/query.
116
+
117
+ Consumed by the Home Assistant device_tracker platform.
118
+
119
+ FortiOS reports an ``is_online`` flag for detected devices, which is used
120
+ verbatim. When a firmware omits the field, ``is_online`` is derived from
121
+ ``last_seen`` freshness instead: True when the device was seen within
122
+ ``online_threshold`` seconds of the reference time (default
123
+ ``DEFAULT_ONLINE_THRESHOLD``, mirroring HA's consider_home behaviour).
124
+ """
125
+
126
+ mac: str
127
+ hostname: str
128
+ ip: str # primary IP; may be empty string if unknown
129
+ os_name: str
130
+ interface: str
131
+ last_seen: int # unix timestamp; 0 when not available
132
+ is_online: bool # API-reported; derived from last_seen when absent
133
+
134
+ @classmethod
135
+ def _from_entry(
136
+ cls,
137
+ entry: dict[str, Any],
138
+ *,
139
+ now: float,
140
+ online_threshold: float,
141
+ ) -> DetectedDevice:
142
+ # IPs may come as a list, a plain string, or null
143
+ ips: Any = entry.get("ipv4_address", entry.get("ip"))
144
+ if isinstance(ips, list):
145
+ ip = _as_str(ips[0]) if ips else ""
146
+ elif isinstance(ips, str):
147
+ ip = ips
148
+ else:
149
+ ip = ""
150
+
151
+ last_seen = _to_int(entry.get("last_seen"))
152
+
153
+ api_flag = entry.get("is_online")
154
+ if api_flag is None:
155
+ # Field absent on this firmware: fall back to freshness.
156
+ is_online = last_seen > 0 and (now - float(last_seen)) <= online_threshold
157
+ else:
158
+ is_online = bool(api_flag)
159
+
160
+ return cls(
161
+ mac=_as_str(entry.get("mac")),
162
+ hostname=_as_str(entry.get("hostname") or entry.get("name")),
163
+ ip=ip,
164
+ os_name=_as_str(entry.get("os_name") or entry.get("os")),
165
+ interface=_as_str(entry.get("interface")),
166
+ last_seen=last_seen,
167
+ is_online=is_online,
168
+ )
169
+
170
+ @classmethod
171
+ def list_from_api(
172
+ cls,
173
+ raw: dict[str, Any],
174
+ *,
175
+ now: float | None = None,
176
+ online_threshold: float = DEFAULT_ONLINE_THRESHOLD,
177
+ ) -> list[DetectedDevice]:
178
+ """Parse the device list envelope.
179
+
180
+ ``now`` and ``online_threshold`` only affect the freshness-based
181
+ fallback used when an entry carries no ``is_online`` field. ``now``
182
+ defaults to the current wall clock; pass it explicitly in tests.
183
+ """
184
+ results = raw.get("results", [])
185
+ if not isinstance(results, list):
186
+ return []
187
+ reference_now = time.time() if now is None else now
188
+ return [
189
+ cls._from_entry(e, now=reference_now, online_threshold=online_threshold)
190
+ for e in results
191
+ if isinstance(e, dict)
192
+ ]
aiofortiosapi/py.typed ADDED
File without changes
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: aiofortiosapi
3
+ Version: 0.1.0
4
+ Summary: Async FortiOS REST API client for Home Assistant
5
+ Project-URL: Homepage, https://github.com/kimfrellsen/aiofortiosapi
6
+ Project-URL: Repository, https://github.com/kimfrellsen/aiofortiosapi
7
+ Project-URL: Changelog, https://github.com/kimfrellsen/aiofortiosapi/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/kimfrellsen/aiofortiosapi/issues
9
+ Author-email: Kim <kim@frellsen.se>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-client,asyncio,fortigate,fortinet,fortios,home-assistant
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: System :: Networking :: Monitoring
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: aiohttp>=3.9
24
+ Provides-Extra: test
25
+ Requires-Dist: aresponses>=3; extra == 'test'
26
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
27
+ Requires-Dist: pytest-cov>=5; extra == 'test'
28
+ Requires-Dist: pytest>=8; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # aiofortiosapi
32
+
33
+ Async Python client for the FortiOS REST API, built for the [Home Assistant](https://www.home-assistant.io/) `fortios` integration.
34
+
35
+ > **Community project** — not affiliated with or supported by Fortinet TAC.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install aiofortiosapi
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ The library requires an injected `aiohttp.ClientSession` — Home Assistant provides one via
46
+ `async_get_clientsession(hass)`. You own the session lifecycle; this library never creates or closes it.
47
+
48
+ ```python
49
+ import asyncio
50
+ import aiohttp
51
+ from aiofortiosapi import FortiOSClient, FortiOSAuthenticationError, FortiOSConnectionError
52
+
53
+ async def main() -> None:
54
+ session = aiohttp.ClientSession()
55
+ try:
56
+ client = FortiOSClient(
57
+ host="192.168.1.1",
58
+ token="your-rest-api-token",
59
+ session=session,
60
+ verify_ssl=False, # set True in production with a valid cert
61
+ )
62
+ status = await client.get_system_status()
63
+ print(status.hostname, status.version)
64
+
65
+ usage = await client.get_resource_usage()
66
+ print(f"CPU {usage.cpu_percent}% MEM {usage.memory_percent}%")
67
+
68
+ devices = await client.get_detected_devices()
69
+ for d in devices:
70
+ print(d.mac, d.hostname, d.ip, "online" if d.is_online else "offline")
71
+ except FortiOSAuthenticationError:
72
+ print("Bad token — re-enter credentials")
73
+ except FortiOSConnectionError:
74
+ print("Cannot reach the FortiGate — check host/port")
75
+ finally:
76
+ await session.close()
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ## Generating a FortiOS REST API token
82
+
83
+ 1. In the FortiGate GUI go to **System → Administrators → Create New → REST API Admin**.
84
+ 2. Set a **Trusted Host** (the IP of your Home Assistant instance) to restrict token use.
85
+ 3. Assign a read-only profile (`prof_admin` or custom).
86
+ 4. Copy the generated token — it is shown only once.
87
+
88
+ The library uses `Authorization: Bearer <token>` (not the legacy `?access_token=` query string).
89
+
90
+ ### Device online state
91
+
92
+ `DetectedDevice.is_online` uses the flag reported by FortiOS. On firmware that
93
+ omits the field, it falls back to deriving online state from `last_seen`
94
+ freshness: a device counts as online when seen within
95
+ `DEFAULT_ONLINE_THRESHOLD` seconds (300). Tune the fallback per client:
96
+
97
+ ```python
98
+ client = FortiOSClient(..., device_online_threshold=600)
99
+ ```
100
+
101
+ ## Scope
102
+
103
+ `aiofortiosapi` is intentionally minimal:
104
+
105
+ - **Three typed monitor endpoints** (`get_system_status`, `get_resource_usage`,
106
+ `get_detected_devices`) used by the Home Assistant integration.
107
+ - A generic `get(path)` for any other endpoint that returns the raw JSON envelope.
108
+ - **No** config-write, CMDB, file upload, SSH fallback, or CLI helpers.
109
+ - **No** session/cookie login flow — Bearer token only.
110
+
111
+ This keeps the dependency tree small (runtime dep: `aiohttp` only) and passes HA integration
112
+ quality review requirements.
113
+
114
+ ## Exception hierarchy
115
+
116
+ | Exception | When raised | HA mapping |
117
+ |---|---|---|
118
+ | `FortiOSConnectionError` | Transport/timeout failure | `ConfigEntryNotReady` |
119
+ | `FortiOSAuthenticationError` | HTTP 401 / 403 | `ConfigEntryAuthFailed` |
120
+ | `FortiOSNotFoundError` | HTTP 404 | log / inspect |
121
+ | `FortiOSResponseError` | Bad JSON, 5xx, other 4xx | log / raise |
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,10 @@
1
+ aiofortiosapi/__init__.py,sha256=OVDrncRnDTp8NawKHHbgLMndKzOatMvgWJPzNMsD7JE,734
2
+ aiofortiosapi/client.py,sha256=D-Dv4PWwoqAedc2W2s-ZSxwXkwgDoHIJL69vZSILCrg,6954
3
+ aiofortiosapi/const.py,sha256=RH-WBpYQaRNr6U5EvqRCtBoLQ2IkdUMp1pLR_jNjUGs,1205
4
+ aiofortiosapi/exceptions.py,sha256=26VmAw_n3I3Cr4eeCDA9zwGS9oLJeyt1XmDtDfWSMqE,1009
5
+ aiofortiosapi/models.py,sha256=HTy3zpGDXfsbWc3M8mJ06uo6zmFdS9tHExVeZQvLNfY,6534
6
+ aiofortiosapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ aiofortiosapi-0.1.0.dist-info/METADATA,sha256=c0NaGu3jINvsjxI2rIojOgj-tQZyDeuWjjWGDhSFoLg,4557
8
+ aiofortiosapi-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ aiofortiosapi-0.1.0.dist-info/licenses/LICENSE,sha256=5Kxo88fmjRi-gGYqqoUnD2FTR1IfW7lmpyWiVi-GTmw,1078
10
+ aiofortiosapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kim <kim@frellsen.se>
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.